mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0c2758b64 | |||
| 5464408ea7 | |||
| 802e538c42 | |||
| 79065514ff | |||
| 51acf2ef08 | |||
| 1f6a330098 | |||
| 28cc1c65af | |||
| 6322c5029d | |||
| 4bb5b97518 | |||
| 99daa76715 | |||
| babe684141 | |||
| 8b6e3bc1d1 | |||
| 95704b11fb | |||
| 4d2583edf5 | |||
| a4b3ee5959 | |||
| 91d9dca34e | |||
| dbde3870da | |||
| b3bcce97da | |||
| a99fac1e31 | |||
| 71d66ae7ee | |||
| 133e6fdc88 | |||
| 1d5cc45ac7 | |||
| a412f629c2 | |||
| 979e5be8ab | |||
| 2789d0cf36 | |||
| 5bcb2da0fe | |||
| 4f23da2a6d | |||
| fe032843f3 | |||
| ff795a7beb | |||
| 40d9370bb6 | |||
| 02f0ed951f | |||
| c2a71c5a51 | |||
| 945bf76897 | |||
| d3b254f9ae | |||
| ee588eeeaa | |||
| dd521aba29 | |||
| a06390a078 | |||
| 2cac1f0f78 | |||
| a10785a701 | |||
| dc730615f2 | |||
| b836cb350a | |||
| 5e85a8bd16 | |||
| f41121f4e5 | |||
| 93a9504824 | |||
| 7abbe91d41 | |||
| cf314ddb58 | |||
| ff97fb6dc8 | |||
| dc48cdce1a | |||
| f130e6d4d7 | |||
| ef5b44b2d0 | |||
| 858ef29868 | |||
| baec3e289e | |||
| df25ed9bde | |||
| 4e1df1a211 | |||
| c3c88cc31e | |||
| fb8ff33203 | |||
| 188f4667cb | |||
| 30c9e2af9b | |||
| 66e087a825 | |||
| 47254113ee | |||
| 026f2d1cc9 | |||
| e9c9bf3cbd | |||
| 5da2109811 | |||
| 28f6c2ed7c | |||
| f6c7f8a96c | |||
| 1d73c2752e | |||
| 5afbb3fd6d | |||
| fd4ab314dc | |||
| 993a9bc4b9 | |||
| 8d1ea7202a | |||
| 9adbab96af | |||
| bdd70e798a | |||
| 5feba7e3a9 | |||
| d3d9460f64 | |||
| 74f3a55c57 | |||
| 3c93d9b300 | |||
| 3c0ea0db65 | |||
| 7c03e2cbd2 | |||
| 8f25704a90 | |||
| 583bad4972 | |||
| b5f340d64c | |||
| 9cd0b7048a | |||
| 4858ce79d0 | |||
| 4090049fff | |||
| e255a7da26 | |||
| c2a3ac3991 | |||
| 2fa09e1cc6 | |||
| 0a742feb98 | |||
| 80a15547e3 | |||
| d26a8c5d44 | |||
| ca619a97bc | |||
| 25a04e0c69 | |||
| d3f9aa2ae7 | |||
| d32fb8e82c | |||
| acd17f6ab6 | |||
| 1ca841d220 | |||
| 39d60d1239 | |||
| 713a2d689e | |||
| babcd9fe2b | |||
| a1403fe968 | |||
| 1485ca0fc7 | |||
| 4bff1ef9df | |||
| 25cfffd0d1 | |||
| 4662e40068 | |||
| 38d78f9844 | |||
| 6f8708d33c | |||
| fa17b0200f | |||
| 14f50d0922 | |||
| 945320880a | |||
| 809d2f3f28 | |||
| 137928ecaf | |||
| 8183af31e2 | |||
| 28f890a701 | |||
| bab7583a97 | |||
| 2e2fa29637 | |||
| 2bef5dbe72 | |||
| 13d25a52a9 | |||
| 9a43a92b1c | |||
| 102b364cc9 | |||
| c18702f297 | |||
| 980bee22a5 | |||
| 80d3d989eb | |||
| 6804a5ff9d | |||
| b5dab1f5da | |||
| 8c6cfa34aa | |||
| 8560436487 | |||
| 619bc95501 | |||
| 781e1fef7b | |||
| 541cf68608 | |||
| eafc89bf73 | |||
| fd451ca158 | |||
| d22e214000 | |||
| 92d6d73f9c | |||
| b02718f86b | |||
| 5a0b7ecb62 | |||
| 07a409ffef | |||
| 58e5e9c9a4 | |||
| 6ca642e572 | |||
| 073c5bfdaa | |||
| fea3569956 | |||
| abdbc76a46 | |||
| f22202b31b | |||
| 4586ed3e51 | |||
| 7475e3e487 | |||
| e025a4cd7b | |||
| 850f804c47 | |||
| 1558280185 | |||
| 15d68a0eed | |||
| e3aed15c08 | |||
| 99713e3439 | |||
| 27c6977746 | |||
| 200f9a3786 | |||
| 92defa26d4 | |||
| c9f1c1ec70 | |||
| 10e4a2940c | |||
| 77f12c7c34 | |||
| ca33ed9f6a | |||
| 6a331b13ba | |||
| 47c878b9c4 | |||
| 61cd4a918d | |||
| 9db4b7fdfb | |||
| 2f59b8ac22 | |||
| f94699e07b | |||
| f7cf597f54 | |||
| aeb8dd7e51 | |||
| 5bf6dd8f85 | |||
| 62b1ab8c98 | |||
| 8de19dc647 | |||
| 8a099d8f6e | |||
| ef400afe00 | |||
| bf596c035e | |||
| 36276e957a | |||
| c69437ba83 | |||
| b2d0c612a5 | |||
| 8e2908f889 | |||
| 3e8d886c25 | |||
| 446c5cf60c | |||
| 13f88b3739 | |||
| 69ee6c1d88 | |||
| 192ae35728 | |||
| 10d547f528 | |||
| b59fd4b7d8 | |||
| c85c8526e8 | |||
| f49d9dcb67 | |||
| c2ac282aca | |||
| 58daf6bfed | |||
| 611b284e20 | |||
| 236a0a164d | |||
| 148eea5899 | |||
| 83c6c8713b | |||
| 2b820d790c | |||
| 36286301f7 | |||
| 9a48fe959d | |||
| f6014cb2b4 | |||
| 9fe21fa906 | |||
| 80a75e91d2 | |||
| 6acc95f756 | |||
| 72412db4c2 | |||
| fa92441415 | |||
| 41916365b6 | |||
| 41429cc6a8 | |||
| da0defcec9 | |||
| ca9902a8d1 | |||
| 995e773096 | |||
| faeba02e57 | |||
| 2fc2f3c927 | |||
| 5cd4fab9dd | |||
| 71b113cad3 | |||
| af2f2910b6 | |||
| f7b1f7cb22 | |||
| 2f5ac3b556 | |||
| 920fa9d23e | |||
| 06471132ef | |||
| 65d232efda | |||
| 0b78db185b | |||
| fc2c7c9718 | |||
| 3079f27205 | |||
| 4c464e1adf | |||
| 1a5e82bc04 | |||
| 8364e409a4 | |||
| 170f0acf4f | |||
| 674eb33edc | |||
| c10852a5e3 | |||
| 653f000294 | |||
| dd2b5e4f3e | |||
| 0e6492342b | |||
| f0126d1af2 | |||
| e2194f9417 | |||
| d0bc4d0d96 | |||
| 4f04f35bd1 |
@@ -7,3 +7,10 @@ guide/public
|
||||
.idea
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
cloudflared.exe
|
||||
!cmd/cloudflared/
|
||||
.DS_Store
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
|
||||
+12
@@ -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
+331
-81
@@ -2,31 +2,99 @@
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
pruneopts = "UT"
|
||||
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8"
|
||||
name = "github.com/anmitsu/go-shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "648efa622239a2f6ff949fed78ee37b48d499ba4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1df548d2da31574a48a7e2adbd228fe2171a7d5f1b4a2d7ca367cc57436c706a"
|
||||
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/sdkmath",
|
||||
"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 = "6bb638a9cf6177e3cc9748a95a2410bb26f1f426"
|
||||
version = "v1.25.8"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
pruneopts = "UT"
|
||||
revision = "37c8de3658fcb183f997c4e13e8337516ab753e6"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad"
|
||||
name = "github.com/certifi/gocertifi"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "deb3ae2ef2610fde3330947281941c562861188b"
|
||||
version = "2018.01.18"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2"
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
packages = ["."]
|
||||
pruneopts = "T"
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6dbb2bbc7e6333e691c4d82fd86485f0695a35902fbb9b2df5f72e22ab0040f3"
|
||||
name = "github.com/cloudflare/golibs"
|
||||
packages = ["lrucache"]
|
||||
pruneopts = "UT"
|
||||
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3f9506ee991cdee1f05bf0cd3e34b5cd922dc00d6a950fb4beb4e07ab1c4d3d1"
|
||||
name = "github.com/coredns/coredns"
|
||||
packages = [
|
||||
"core/dnsserver",
|
||||
@@ -52,30 +120,45 @@
|
||||
"plugin/pkg/uniq",
|
||||
"plugin/pkg/watch",
|
||||
"plugin/test",
|
||||
"request"
|
||||
"request",
|
||||
]
|
||||
revision = "992e7928c7c258628d2b13b769acc86781b9faea"
|
||||
pruneopts = "UT"
|
||||
revision = "2e322f6e8a54f18c6aef9c25a7c432c291a3d9f7"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6f70106e7bc1c803e8a0a4519e09c12d154771acfa2559206e97b033bbd1dd38"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
packages = ["jose"]
|
||||
pruneopts = "UT"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1da3a221f0bc090792d3a2a080ff09008427c0e0f0533a4ed6abd8994421da73"
|
||||
name = "github.com/coreos/go-systemd"
|
||||
packages = ["daemon"]
|
||||
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
|
||||
version = "v17"
|
||||
pruneopts = "UT"
|
||||
revision = "e64a0ec8b42a61e2a9801dc1d0abe539dea79197"
|
||||
version = "v20"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
pruneopts = "UT"
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c013ffc6e15f9f898078f9d38441c68b228aa7b899659452170250ccb27f5f1e"
|
||||
name = "github.com/elgs/gosqljson"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d4268b2a09b1f736633577c4ac93f2a5356c73742fff5344e2451aeec60a7ad0"
|
||||
name = "github.com/equinox-io/equinox"
|
||||
packages = [
|
||||
".",
|
||||
@@ -83,213 +166,294 @@
|
||||
"internal/go-update/internal/binarydist",
|
||||
"internal/go-update/internal/osext",
|
||||
"internal/osext",
|
||||
"proto"
|
||||
"proto",
|
||||
]
|
||||
revision = "f24972fa72facf59d05c91c848b65eac38815915"
|
||||
pruneopts = "UT"
|
||||
revision = "5205c98a6c11dc72747ce12fff6cd620a99fde05"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:433763f10d88dba9b533a7ea2fe9f5ee11e57e00306eb97a1f6090fd978e8fa1"
|
||||
name = "github.com/facebookgo/grace"
|
||||
packages = ["gracenet"]
|
||||
pruneopts = "UT"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:50a46ab1d5edbbdd55125b4d37f1bf503d0807c26461f9ad7b358d6006641d09"
|
||||
name = "github.com/flynn/go-shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d4623fc7bf7e281d9107367cc4a9e76ed3e86b1eec1a4e30630c870bef1fedd0"
|
||||
name = "github.com/getsentry/raven-go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f9a357116172beefdf218cada11a5123d84c6975c24a4380c75354c703ae0cf"
|
||||
name = "github.com/gliderlabs/ssh"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "63518b5243e0fe306cc35cb382f459de123e550e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3e6afc3ed8a72949aa735c00fddc23427dc9384ccfd51cf0d91a412e668da632"
|
||||
name = "github.com/golang-collections/collections"
|
||||
packages = ["queue"]
|
||||
pruneopts = "UT"
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f5ce1529abc1204444ec73779f44f94e2fa8fcdb7aca3c355b0c95947e4005c6"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
pruneopts = "UT"
|
||||
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
|
||||
version = "0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
|
||||
pruneopts = "UT"
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
|
||||
version = "v1.6.2"
|
||||
pruneopts = "UT"
|
||||
revision = "00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15"
|
||||
version = "v1.7.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1a1206efd03a54d336dce7bb8719e74f2f8932f661cb9f57d5813a1d99c083d8"
|
||||
name = "github.com/grpc-ecosystem/grpc-opentracing"
|
||||
packages = ["go/otgrpc"]
|
||||
pruneopts = "UT"
|
||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca"
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c2b33e84"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
|
||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:12cb143f2148bf54bcd9fe622abac17325e85eeb1d84b8ec6caf1c80232108fd"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
"oid",
|
||||
"scram",
|
||||
]
|
||||
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
|
||||
pruneopts = "UT"
|
||||
revision = "3427c32cb71afc948325f299f040e53c1dd78979"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4a29eeb25603debe8f2098a9902c4d3851034cf70d33be428826e86e8c30a1b0"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
pruneopts = "UT"
|
||||
revision = "98ec13f34aabf44cc914c65a1cfb7b9bc815aef1"
|
||||
version = "v0.1.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d62282425ffb75047679d7e2c3b980eea7f82c05ef5fb9142ee617ebac6e7432"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
pruneopts = "UT"
|
||||
revision = "88ba11cfdc67c7588b30042edf244b2875f892b6"
|
||||
version = "v0.0.10"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
pruneopts = "UT"
|
||||
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71"
|
||||
name = "github.com/mholt/caddy"
|
||||
packages = [
|
||||
".",
|
||||
"caddyfile",
|
||||
"telemetry"
|
||||
"telemetry",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2b4b4b2e5544c2a11a486c1b631357aa2ddf766e50c1b2483cf809da2c511234"
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1"
|
||||
version = "v1.0.8"
|
||||
pruneopts = "UT"
|
||||
revision = "73601d4aed9d844322611759d7f3619110b7c88e"
|
||||
version = "v1.1.8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
|
||||
pruneopts = "UT"
|
||||
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11e62d6050198055e6cd87ed57e5d8c669e84f839c16e16f192374d913d1a70d"
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"ext",
|
||||
"log"
|
||||
"log",
|
||||
]
|
||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
||||
version = "v1.0.2"
|
||||
pruneopts = "UT"
|
||||
revision = "659c90643e714681897ec2521c60567dd21da733"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "UT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c968b29db5d68ec97de404b6d058d5937fa015a141b3b4f7a0d87d5f8226f04c"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
"prometheus/promhttp",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||
version = "v0.9.0-pre1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
pruneopts = "UT"
|
||||
revision = "14fe0d1b01d4d5fc031dd4bec1823bd3ebbe8016"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f119e3205d3a1f0f19dbd7038eb37528e2c6f0933269dc344e305951fb87d632"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
"model",
|
||||
]
|
||||
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
|
||||
pruneopts = "UT"
|
||||
revision = "287d3e634a1e550c9e463dd7e5a75a422c614505"
|
||||
version = "v0.7.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a210815b437763623ecca8eb91e6a0bf4f2d6773c5a6c9aec0e28f19e5fd6deb"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/fs",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
|
||||
pruneopts = "UT"
|
||||
revision = "499c85531f756d1129edd26485a5f73871eeb308"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1a23fdd843129ef761ffe7651bc5fe7c5b09fbe933e92783ab06cc11c37b7b37"
|
||||
name = "github.com/rifflock/lfshook"
|
||||
packages = ["."]
|
||||
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
|
||||
version = "v2.3"
|
||||
pruneopts = "UT"
|
||||
revision = "b9218ef580f59a2e72dad1aa33d660150445d05a"
|
||||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
pruneopts = "UT"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
packages = [
|
||||
"assert",
|
||||
"require",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d9e4eb3f377dca21b3be66fc471106c3150cdbe43f1fba67fe79f6321b9731a8"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"ssh/terminal"
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"nacl/box",
|
||||
"nacl/secretbox",
|
||||
"poly1305",
|
||||
"salsa20/salsa",
|
||||
"ssh",
|
||||
"ssh/terminal",
|
||||
]
|
||||
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
|
||||
pruneopts = "UT"
|
||||
revision = "34f69633bfdcf9db92f698f8487115767eebef81"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ded5b1b804f079c993f887e0da394aa54cdf8d2a6a31e62c66d94ebe6f75bc2a"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
@@ -304,36 +468,44 @@
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"trace",
|
||||
"websocket"
|
||||
"websocket",
|
||||
]
|
||||
revision = "32a936f46389aa10549d60bd7833e54b01685d09"
|
||||
pruneopts = "UT"
|
||||
revision = "72f939374954d0a1b2a1e27dcda950e2724dd5c1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = "UT"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:122914ab6dc8ee219eeb0d11276704e7ac3854feb55dad10f71081b9639ed28e"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"cpu",
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
"windows/svc",
|
||||
"windows/svc/eventlog",
|
||||
"windows/svc/mgr"
|
||||
"windows/svc/mgr",
|
||||
]
|
||||
revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa"
|
||||
pruneopts = "UT"
|
||||
revision = "543471e840be449c53d44b32c7adf1261ad67e37"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8d8faad6b12a3a4c819a3f9618cb6ee1fa1cfc33253abeeea8b55336721e3405"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/language",
|
||||
"internal/language/compact",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
@@ -343,34 +515,46 @@
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
"unicode/rangetable",
|
||||
]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
pruneopts = "UT"
|
||||
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:583a0c80f5e3a9343d33aea4aead1e1afcc0043db66fdf961ddd1fe8cd3a4faf"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0"
|
||||
pruneopts = "UT"
|
||||
revision = "a023cd5227bd25fd1f5c5633743ff3eacc93d169"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6cd77d0b616d2dcebd363dfecba593f27b0151fc82cdb5fbfb96c5a7cfbc95b5"
|
||||
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",
|
||||
@@ -378,31 +562,37 @@
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"serviceconfig",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
|
||||
version = "v1.13.0"
|
||||
pruneopts = "UT"
|
||||
revision = "f6d0f9ee430895e87ef1ceb5ac8f39725bafceef"
|
||||
version = "v1.24.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "altsrc-parse-durations"
|
||||
digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787"
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
packages = [
|
||||
".",
|
||||
"altsrc"
|
||||
"altsrc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d604b6ffeee878fbf084fd2761466b6649989cee"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:59f10c1537d2199d9115d946927fe31165959a95190849c82ff11e05803528b0"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
pruneopts = "UT"
|
||||
revision = "f221b8435cfb71e54062f6c6e99e9ade30b124d5"
|
||||
version = "v2.2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642"
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
packages = [
|
||||
".",
|
||||
@@ -418,8 +608,9 @@
|
||||
"rpc/internal/refcount",
|
||||
"schemas",
|
||||
"server",
|
||||
"std/capnp/rpc"
|
||||
"std/capnp/rpc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "v2.17.1"
|
||||
@@ -427,6 +618,65 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9aac6a0260101988582bfa55d39559e53cf8ccd7e37caad3350aeea768f5bdb2"
|
||||
input-imports = [
|
||||
"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",
|
||||
"github.com/coredns/coredns/plugin",
|
||||
"github.com/coredns/coredns/plugin/cache",
|
||||
"github.com/coredns/coredns/plugin/metrics/vars",
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest",
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode",
|
||||
"github.com/coredns/coredns/request",
|
||||
"github.com/coreos/go-oidc/jose",
|
||||
"github.com/coreos/go-systemd/daemon",
|
||||
"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",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/lib/pq",
|
||||
"github.com/mattn/go-colorable",
|
||||
"github.com/miekg/dns",
|
||||
"github.com/mitchellh/go-homedir",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rifflock/lfshook",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"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",
|
||||
"golang.org/x/net/http2/hpack",
|
||||
"golang.org/x/net/idna",
|
||||
"golang.org/x/net/trace",
|
||||
"golang.org/x/net/websocket",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"golang.org/x/sys/windows",
|
||||
"golang.org/x/sys/windows/svc",
|
||||
"golang.org/x/sys/windows/svc/eventlog",
|
||||
"golang.org/x/sys/windows/svc/mgr",
|
||||
"gopkg.in/urfave/cli.v2",
|
||||
"gopkg.in/urfave/cli.v2/altsrc",
|
||||
"zombiezen.com/go/capnproto2",
|
||||
"zombiezen.com/go/capnproto2/encoding/text",
|
||||
"zombiezen.com/go/capnproto2/pogs",
|
||||
"zombiezen.com/go/capnproto2/rpc",
|
||||
"zombiezen.com/go/capnproto2/schemas",
|
||||
"zombiezen.com/go/capnproto2/server",
|
||||
"zombiezen.com/go/capnproto2/std/capnp/rpc",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+41
-12
@@ -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,19 +48,48 @@
|
||||
[[constraint]]
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "=2.17.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coredns/coredns"
|
||||
branch = "master"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/dns"
|
||||
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]]
|
||||
name = "github.com/coreos/go-oidc"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master" # master required by github.com/miekg/dns
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/golibs"
|
||||
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "=1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gliderlabs/ssh"
|
||||
revision = "63518b5243e0fe306cc35cb382f459de123e550e"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
version = "1.23.9"
|
||||
|
||||
@@ -16,23 +16,39 @@ ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
endif
|
||||
|
||||
ifeq ($(GOARCH),)
|
||||
GOARCH := amd64
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
.PHONY: clean
|
||||
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: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
@@ -43,8 +59,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
|
||||
|
||||
@@ -55,3 +71,17 @@ release: bin/equinox
|
||||
bin/equinox:
|
||||
mkdir -p bin
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
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 ./...)
|
||||
|
||||
+248
@@ -1,3 +1,251 @@
|
||||
2019.10.4
|
||||
- 2019-10-21 TUN-2450: Remove Brew publishing formula
|
||||
|
||||
2019.10.3
|
||||
- 2019-10-18 Fix #129: Excessive memory usage streaming large files (#142)
|
||||
|
||||
2019.10.2
|
||||
- 2019-10-17 AUTH-2167: Adds CLI option for host key directory
|
||||
|
||||
2019.10.1
|
||||
- 2019-10-17 Adds variable to fix windows build
|
||||
|
||||
2019.10.0
|
||||
- 2019-10-11 AUTH-2105: Dont require --destination arg
|
||||
- 2019-10-14 TUN-2344: log more details: http2.Framer.ErrorDetail() if available, connectionID
|
||||
- 2019-10-16 AUTH-2159: Moves shutdownC close into error handling AUTH-2161: Lowers size of preamble length AUTH-2160: Fixes url parsing logic
|
||||
- 2019-10-16 AUTH-2135: Adds support for IPv6 and tests
|
||||
- 2019-10-02 AUTH-2105: Adds support for local forwarding. Refactor auditlogger creation. AUTH-2088: Adds dynamic destination routing
|
||||
- 2019-10-09 AUTH-2114: Uses short lived cert auth for outgoing client connection
|
||||
- 2019-09-30 AUTH-2089: Revise ssh server to function as a proxy
|
||||
|
||||
2019.9.2
|
||||
- 2019-09-26 TUN-2355: Roll back TUN-2276
|
||||
|
||||
2019.9.1
|
||||
- 2019-09-23 TUN-2334: remove tlsConfig.ServerName special case
|
||||
- 2019-09-23 AUTH-2077: Quotes open browser command in windows
|
||||
- 2019-09-11 AUTH-2050: Adds time.sleep to temporarily avoid hitting tunnel muxer dealock issue
|
||||
- 2019-09-10 AUTH-2056: Writes stderr to its own stream for non-pty connections
|
||||
- 2019-09-16 TUN-2307: Capnp is the only serialization format used in tunnelpogs
|
||||
- 2019-09-18 TUN-2315: Replace Scope with IntentLabel
|
||||
- 2019-09-17 TUN-2309: Split ConnectResult into ConnectError and ConnectSuccess, each implementing its own capnp serialization logic
|
||||
- 2019-09-18 AUTH-2052: Adds tests for SSH server
|
||||
- 2019-09-18 AUTH-2067: Log commands correctly
|
||||
- 2019-09-19 AUTH-2055: Verifies token at edge on access login
|
||||
- 2019-09-04 TUN-2201: change SRV records used by cloudflared
|
||||
- 2019-09-06 TUN-2280: Revert "TUN-2260: add name/group to CapnpConnectParameters, remove Scope"
|
||||
- 2019-09-03 AUTH-1943 hooked up uploader to logger, added timestamp to session logs, add tests
|
||||
- 2019-09-04 AUTH-2036: Refactor user retrieval, shutdown after ssh server stops, add custom version string
|
||||
- 2019-09-06 AUTH-1942 added event log to ssh server
|
||||
- 2019-09-04 AUTH-2037: Adds support for ssh port forwarding
|
||||
- 2019-09-05 TUN-2276: Path encoding broken
|
||||
|
||||
2019.9.0
|
||||
- 2019-09-05 TUN-2279: Revert path encoding fix
|
||||
- 2019-08-30 AUTH-2021 - check error for failing tests
|
||||
- 2019-08-29 AUTH-2030: Support both authorized_key and short lived cert authentication simultaniously without specifiying at start time
|
||||
- 2019-08-29 AUTH-2026: Adds support for non-pty sessions and inline command exec
|
||||
- 2019-08-26 AUTH-1943: Adds session logging
|
||||
- 2019-08-26 TUN-2162: Decomplect OpenStream to allow finer-grained timeouts
|
||||
- 2019-08-29 TUN-2260: add name/group to CapnpConnectParameters, remove Scope
|
||||
|
||||
2019.8.4
|
||||
- 2019-08-30 Fix #111: Add support for specifying a specific HTTP Host: header on the origin. (#114)
|
||||
- 2019-08-22 TUN-2165: Add ClientConfig to tunnelrpc.ConnectResult
|
||||
- 2019-08-20 AUTH-2014: Checks users login shell
|
||||
- 2019-08-26 TUN-2243: Revert "STOR-519: Add db-connect, a SQL over HTTPS server"
|
||||
- 2019-08-27 TUN-2244: Add NO_AUTOUPDATE env var
|
||||
- 2019-08-22 AUTH-2018: Adds support for authorized keys and short lived certs
|
||||
- 2019-08-28 AUTH-2022: Adds ssh timeout configuration
|
||||
- 2019-08-28 TUN-1968: Gracefully diff StreamHandler.UpdateConfig
|
||||
- 2019-08-26 AUTH-2021 - s3 bucket uploading for SSH logs
|
||||
- 2019-08-19 AUTH-2004: Adds static host key support
|
||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
||||
|
||||
2019.8.3
|
||||
- 2019-08-20 STOR-519: Add db-connect, a SQL over HTTPS server
|
||||
- 2019-08-20 Release 2019.8.2
|
||||
- 2019-08-20 Revert "AUTH-1941: Adds initial SSH server implementation"
|
||||
- 2019-08-11 TUN-2163: Add GrapQLType method to Scope interface
|
||||
- 2019-08-06 TUN-2152: Requests with a query in the URL are erroneously escaped
|
||||
- 2019-07-18 AUTH-1941: Adds initial SSH server implementation
|
||||
|
||||
2019.8.1
|
||||
- 2019-08-05 TUN-2111: Implement custom serialization logic for FallibleConfig and OriginConfig
|
||||
- 2019-08-06 Revert "TUN-1736: Missing headers when passing an invalid path"
|
||||
|
||||
2019.8.0
|
||||
- 2019-07-11 TUN-1956: Go 1.12 update
|
||||
- 2019-07-24 TUN-1736: Missing headers when passing an invalid path
|
||||
- 2019-07-30 TUN-2117: read group/system-name from CLI, send it to edge
|
||||
- 2019-08-02 TUN-2125: Add PostgresType() to Scope
|
||||
- 2019-08-05 TUN-2147: Implemented ScopeUnmarshaler
|
||||
- 2019-07-31 TUN-2110: Implement custom deserialization logic for OriginConfig
|
||||
- 2019-07-31 AUTH-1972: Deletes token lock file if backoff retry attempts exceeded and intercepts signals until lock is released
|
||||
|
||||
2019.7.0
|
||||
- 2019-05-28 TUN-1913: Define OriginService for each type of origin
|
||||
- 2019-04-29 Build a docker container
|
||||
- 2019-06-12 TUN-1952: Group ClientConfig fields by the component that uses the config, and return the part of the config that failed to be applied
|
||||
- 2019-06-05 TUN-1893: Proxy requests to the origin based on tunnel hostname
|
||||
- 2019-06-17 TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge
|
||||
- 2019-06-18 TUN-1885: Reconfigure cloudflared on receiving new ClientConfig
|
||||
- 2019-06-19 TUN-1976: Pass tunnel hostname through header
|
||||
- 2019-06-20 TUN-1982: Load custom origin CA when OriginCAPool is specified
|
||||
- 2019-06-26 TUN-2005: Upgrade logrus
|
||||
- 2019-06-20 TUN-1981: Write response header & body on proxy error to notify eyeballs of failure category
|
||||
- 2019-06-20 TUN-1977: Validate OriginConfig has valid URL, and use scheme to determine if a HTTPOriginService is expecting HTTP or Unix
|
||||
- 2019-06-13 DoH: change the media type to application/dns-message
|
||||
- 2019-06-26 AUTH-1736: Better handling of token revocation
|
||||
|
||||
2019.6.0
|
||||
- 2019-05-17 TUN-1828: Update declarative tunnel config struct
|
||||
- 2019-05-29 Handle exit code on err
|
||||
- 2019-05-29 AUTH-1802: Fixed ssh-config templating
|
||||
- 2019-05-30 TUN-1914: Conflate HTTP and Unix OriginConfig, and add TLS config to WebSocketOriginConfig
|
||||
- 2019-06-03 AUTH-1811: ssh-gen config fixes
|
||||
|
||||
2019.5.0
|
||||
- 2019-04-25 TUN-1781: ServeStream should return early on error
|
||||
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
|
||||
- 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC
|
||||
- 2019-01-23 AUTH-1557: Short Lived Certs
|
||||
- 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers
|
||||
- 2019-05-13 AUTH-1706: fixes and testing
|
||||
- 2019-05-22 TUN-1880: Save debug and warn level log to logfile
|
||||
- 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config
|
||||
|
||||
2019.4.1
|
||||
- 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld
|
||||
- 2019-04-04 TUN-1619: Add flag to test declarative tunnels.
|
||||
- 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy
|
||||
- 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server
|
||||
- 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely.
|
||||
- 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames
|
||||
- 2019-04-17 TUN-1734: Pin packages at exact versions
|
||||
- 2019-04-18 TUN-1669: Update license message in help text. Also fix test
|
||||
|
||||
2019.4.0
|
||||
- 2019-03-28 TUN-1648: ConnectionID is now a UUID
|
||||
- 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs
|
||||
|
||||
2019.3.2
|
||||
- 2019-03-22 TUN-1637: Free tunnels shouldn't require cert.pem
|
||||
- 2019-03-18 TUN-1604: Define Connect RPC call
|
||||
|
||||
2019.3.1
|
||||
- 2019-03-09 Add rdp as a supported protocol in URL validation (#76)
|
||||
- 2019-03-15 TUN-1613: improved cloudflared RegisterTunnel fail metrics
|
||||
- 2019-03-17 TUN-1615: revert miekg/dns to last known working revision
|
||||
|
||||
2019.3.0
|
||||
- 2018-12-28 make http transport aware of proxy from envvar
|
||||
- 2019-02-28 TUN-1559: fix nil dereference in TunnelConfig.CloseConnOnce
|
||||
- 2019-03-04 TUN-1451: Make non-interactive, non-service execution possible on Windows
|
||||
- 2019-03-04 TUN-1562: Refactor connectedSignal to be safe to close multiple times
|
||||
- 2019-02-27 TUN-1550: Add validation timeout for non-responsive origins
|
||||
- 2019-03-06 AUTH-1531: Named flags for ssh service tokens
|
||||
- 2019-02-14 Support unix sockets.
|
||||
- 2019-03-08 TUN-1389: Non-scalar flags in a cloudflared config.yml don't get logged
|
||||
- 2019-03-07 TUN-1522: If we can't get SRV from default resolver, get them from 1.1.1.1 DoT
|
||||
|
||||
2019.2.1
|
||||
- 2019-02-14 TUN-1381: should tell you if you're on the latest version rather than just exiting silently
|
||||
- 2019-02-15 TUN-1467: build with Go 1.11
|
||||
- 2019-02-15 AUTH-1519: Added logging
|
||||
- 2019-02-19 TUN-1525: cloudflared metrics for registration success/fail
|
||||
- 2019-02-19 TUN-1510: Wrap the close() in sync.Once.Do
|
||||
|
||||
2019.2.0
|
||||
- 2019-01-24 AUTH-1462: better curl arg parsing
|
||||
- 2019-02-01 TUN-1456: Only make one UUID
|
||||
- 2019-01-30 cloudflared/linux_service: Add missing /etc/init.d shebang
|
||||
- 2019-02-07 AUTH-1511: Add custom headers for ssh command
|
||||
- 2019-02-01 AUTH-1503: Added RDP support
|
||||
- 2019-02-01 AUTH-1403: Print the paths in the ssh-config instructions
|
||||
|
||||
2019.1.0
|
||||
- 2018-12-10 TUN-1231: Horizontal overflow wrapping on the Hello page
|
||||
- 2018-12-17 TUN-1140: Show usage if invoked with no args or config
|
||||
- 2018-11-06 TUN-632 Filter out common network exceptions from going to Sentry on StartServer
|
||||
- 2019-01-07 TUN-1138: Install cloudflared service directory with 755 permissions
|
||||
- 2019-01-07 TUN-1265: Silent exit when failing to parse config
|
||||
- 2019-01-10 TUN-1350: Enhance error messages with cloudflarestatus.com link, if relevant
|
||||
- 2019-01-16 TUN-1358: Close readyList after Muxer.Serve() has stopped running
|
||||
- 2019-01-24 AUTH-1423: move from stdout to stderr
|
||||
- 2019-01-24 AUTH-1404: reauth if the token is about to expire within 15 minutes
|
||||
- 2019-01-24 AUTH-1459: improved ssh streaming error message
|
||||
- 2019-01-24 AUTH-1211: print all the versions
|
||||
- 2019-01-24 AUTH-1337: fix url path
|
||||
- 2019-01-28 TUN-1418: Rename ProtocolLogger to TransportLogger, and use TransportLogger to log RPC events.
|
||||
- 2019-01-28 TUN-1419: Identify request/response headers/content length with ray ID
|
||||
|
||||
2018.12.1
|
||||
- 2018-12-11 TUN-1270: cloudflared panic (HA metrics missing label)
|
||||
|
||||
2018.12.0
|
||||
- 2018-11-15 TUN-1196: Allow TLS config client CA and root CA to be constructed from multiple certificates
|
||||
- 2018-11-20 TUN-1209: TLS Config Certificates and GetCertificate can both be set
|
||||
- 2018-11-26 TUN-1212: Expose tunnel_id in metrics
|
||||
- 2018-11-30 TUN-1204: remove 'cloudflared hello' command
|
||||
- 2018-12-04 Fix license URL typo
|
||||
- 2018-12-07 TUN-1250: ValidateHTTPService shouldn't follow 302s
|
||||
|
||||
2018.11.0
|
||||
- 2018-10-31 AUTH-1282: Fixed an issue where we were receiving as opposed sending on the channel.
|
||||
- 2018-11-06 TUN-1179: Fix log message in cmd/cloudflared/transfer.Run
|
||||
- 2018-11-13 AUTH-1308: get jwt even when you are already logged in
|
||||
- 2018-11-12 TUN-1190: check URL parse error when starting SSH proxy server
|
||||
- 2018-11-15 AUTH-1320: Fixed request issue and unhide the ssh command
|
||||
|
||||
2018.10.5
|
||||
- 2018-10-18 TUN-968: Flow control for large requests/responses
|
||||
- 2018-10-26 TUN-1158: Windows: use process arguments rather than trivial service arguments
|
||||
- 2018-10-20 #30: Fix the Content-Length header for HTTP2->HTTP1
|
||||
- 2018-10-29 TUN-1160: pass Host header during origin url validation
|
||||
|
||||
2018.10.4
|
||||
- 2018-09-21 AUTH-1070: added SSH/protocol forwarding
|
||||
- 2018-10-19 AUTH-1235: fixed packaging of deb dev file
|
||||
- 2018-10-19 TUN-1097: Host missing from WebSocket request
|
||||
- 2018-10-19 AUTH-1188: UX Review and Changes for CLI SSH Access
|
||||
|
||||
2018.10.3
|
||||
- 2018-10-08 TUN-1099: Bring back changes in 2018.10.1
|
||||
- 2018-10-08 TUN-1098: removed deprecation error
|
||||
- 2018-10-08 TUN-1101: False negatives in Cloudflared error reporting
|
||||
|
||||
2018.10.2
|
||||
- 2018-10-06 TUN-1093: Revert cloudflared to 2018.8.0
|
||||
|
||||
2018.10.1
|
||||
- 2018-10-03 TUN-1012: Normalize config filename for Linux services
|
||||
- 2018-10-05 TUN-1081: cloudflared now generates UUIDs
|
||||
- 2018-10-05 TUN-1083: fixed incorrect help menu
|
||||
- 2018-10-05 TUN-1086: fixed config option
|
||||
|
||||
2018.10.0
|
||||
- 2018-08-15 AUTH-910, AUTH-1049, AUTH-1068, AUTH-1056: Generate and store Access tokens with E2EE option, curl/cmd wrapper
|
||||
- 2018-09-11 TUN-890: To support free tunnels, hostname can now be ""
|
||||
- 2018-09-12 TUN-810: Cloudflared should open dash/argotunnel not dash/warp
|
||||
- 2018-09-12 TUN-985: Don't display tunnel ID if it's empty string
|
||||
- 2018-09-11 TUN-881: Display trial zone URL upon successful registration
|
||||
- 2018-09-11 TUN-868: HTTP/HTTPS mismatch should have a better error message
|
||||
- 2018-09-19 TUN-1028: Unhide cloudflared compression flag
|
||||
- 2018-09-20 AUTH-1139: refactored cloudflared help menu
|
||||
- 2018-09-20 TUN-1035: New text for cloudflared tunnel --help
|
||||
- 2018-09-18 AUTH-1136: addressing beta feedback
|
||||
- 2018-09-26 AUTH-1165: hide access command
|
||||
- 2018-09-26 TUN-1046: Document that delta compression is a beta feature
|
||||
- 2018-09-28 TUN-1056: Lint error broke build
|
||||
- 2018-09-27 TUN-1052: Origintunneld can send back an Origincert to Cloudflared
|
||||
- 2018-09-28 TUN-1052: Changing type of OriginCert to :Data
|
||||
- 2018-10-01 TUN-1062: Makefile target for regenerating Capn Proto definitions
|
||||
- 2018-10-02 TUN-1064: Revert OriginCert capnp changes in Cloudflared. Reverts commits a1ee2342e97 and 8c756c45785.
|
||||
- 2018-10-03 TUN-1076: Pin capnproto2 to version 2.17.1
|
||||
- 2018-10-03 AUTH-1199: unhide access command, added beta label
|
||||
|
||||
2018.8.0
|
||||
- 2018-05-01 Initial commit
|
||||
- 2018-05-03 TUN-595: Add License/Readme files to cloudflared
|
||||
|
||||
@@ -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().Before(checkTime) {
|
||||
os.Remove(path)
|
||||
return nil
|
||||
}
|
||||
//add the upload to the work queue
|
||||
go func() {
|
||||
m.workQueue <- path
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// worker handles upload requests
|
||||
func (m *DirectoryUploadManager) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-m.shutdownC:
|
||||
return
|
||||
case filepath := <-m.workQueue:
|
||||
if err := m.Upload(filepath); err != nil {
|
||||
m.logger.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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package awsuploader
|
||||
|
||||
// UploadManager is used to manage file uploads on an interval
|
||||
type UploadManager interface {
|
||||
Upload(string) error
|
||||
Start()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
//Package carrier provides a WebSocket proxy to carry or proxy a connection
|
||||
//from the local client to the edge. See it as a wrapper around any protocol
|
||||
//that it packages up in a WebSocket connection to the edge.
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
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 {
|
||||
}
|
||||
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
return os.Stdin.Read(p)
|
||||
|
||||
}
|
||||
|
||||
// Write will write to Stdout
|
||||
func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
return os.Stdout.Write(p)
|
||||
}
|
||||
|
||||
// Helper to allow defering the response close with a check that the resp is not nil
|
||||
func closeRespBody(resp *http.Response) {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
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 {
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
default:
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go serveConnection(logger, conn, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(logger *logrus.Logger, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
serveStream(logger, c, options)
|
||||
}
|
||||
|
||||
// serveStream will serve the data over the WebSocket stream
|
||||
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", options.OriginURL)
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
cloudflaredWebsocket.Stream(wsConn, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions) (*cloudflaredWebsocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
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
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 != http.StatusFound {
|
||||
return false
|
||||
}
|
||||
|
||||
location, err := resp.Location()
|
||||
if err != nil || location == nil {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(location.Path, "/cdn-cgi/access/login") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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(options *StartOptions) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchToken(req.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to create a new request as FetchToken will modify req (boo mutable)
|
||||
// as it has to follow redirect on the API and such, so here we init a new one
|
||||
originRequest, err := http.NewRequest(http.MethodGet, 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
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
// example in Sec-Websocket-Key in rfc6455
|
||||
testSecWebsocketKey = "dGhlIHNhbXBsZSBub25jZQ=="
|
||||
)
|
||||
|
||||
type testStreamer struct {
|
||||
buf *bytes.Buffer
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
func newTestStream() *testStreamer {
|
||||
return &testStreamer{buf: new(bytes.Buffer)}
|
||||
}
|
||||
|
||||
func (s *testStreamer) Read(p []byte) (int, error) {
|
||||
s.l.RLock()
|
||||
defer s.l.RUnlock()
|
||||
return s.buf.Read(p)
|
||||
|
||||
}
|
||||
|
||||
func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
return s.buf.Write(p)
|
||||
}
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
buf := newTestStream()
|
||||
options := &StartOptions{
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
err := StartClient(logger, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
func TestStartServer(t *testing.T) {
|
||||
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 := Serve(logger, listener, shutdownC, options)
|
||||
if err != nil {
|
||||
t.Fatalf("Error running server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
func TestIsAccessResponse(t *testing.T) {
|
||||
validLocationHeader := http.Header{}
|
||||
validLocationHeader.Add("location", "https://test.cloudflareaccess.com/cdn-cgi/access/login/blahblah")
|
||||
invalidLocationHeader := http.Header{}
|
||||
invalidLocationHeader.Add("location", "https://google.com")
|
||||
testCases := []struct {
|
||||
Description string
|
||||
In *http.Response
|
||||
ExpectedOut bool
|
||||
}{
|
||||
{"nil response", nil, false},
|
||||
{"redirect with no location", &http.Response{StatusCode: http.StatusFound}, false},
|
||||
{"200 ok", &http.Response{StatusCode: http.StatusOK}, false},
|
||||
{"redirect with location", &http.Response{StatusCode: http.StatusFound, Header: validLocationHeader}, true},
|
||||
{"redirect with invalid location", &http.Response{StatusCode: http.StatusFound, Header: invalidLocationHeader}, false},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
if IsAccessResponse(tc.In) != tc.ExpectedOut {
|
||||
t.Fatalf("Failed case %d -- %s", i, tc.Description)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func newTestWebSocketServer() *httptest.Server {
|
||||
upgrader := ws.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, _ := upgrader.Upgrade(w, r, nil)
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(mt, []byte(message)); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
|
||||
req, err := http.NewRequest("GET", url, stream)
|
||||
if err != nil {
|
||||
t.Fatalf("testRequestHeader error")
|
||||
}
|
||||
|
||||
req.Header.Add("Connection", "Upgrade")
|
||||
req.Header.Add("Upgrade", "WebSocket")
|
||||
req.Header.Add("Sec-Websocket-Key", testSecWebsocketKey)
|
||||
req.Header.Add("Sec-Websocket-Protocol", "tunnel-protocol")
|
||||
req.Header.Add("Sec-Websocket-Version", "13")
|
||||
req.Header.Add("User-Agent", "curl/7.59.0")
|
||||
|
||||
return req
|
||||
}
|
||||
+17
-11
@@ -1,6 +1,7 @@
|
||||
pinned_go: &pinned_go go=1.9.3-1
|
||||
pinned_go: &pinned_go go=1.12.7-1
|
||||
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
|
||||
stretch:
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -46,15 +47,15 @@ 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,4 +87,9 @@ 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
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
// ssh will start a WS proxy server for server mode
|
||||
// or copy from stdin/stdout for client mode
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh")
|
||||
}
|
||||
originURL := "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(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
destination := c.String(sshDestinationFlag)
|
||||
if destination != "" {
|
||||
headers.Add("CF-Access-SSH-Destination", destination)
|
||||
}
|
||||
|
||||
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")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
forwarder, err := url.Parse(localForwarder)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
return carrier.StartServer(logger, forwarder.Host, shutdownC, options)
|
||||
}
|
||||
|
||||
return carrier.StartClient(logger, &carrier.StdinoutStream{}, options)
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
split := strings.Split(valuePair, ":")
|
||||
if len(split) > 1 {
|
||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildRequestHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("client", "value")
|
||||
headers.Add("secret", "safe-value")
|
||||
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
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 (
|
||||
logger = log.CreateLogger()
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
func Flags() []cli.Flag {
|
||||
return []cli.Flag{} // no flags yet.
|
||||
}
|
||||
|
||||
// Commands returns all the Access related subcommands
|
||||
func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
{
|
||||
Name: "access",
|
||||
Category: "Access (BETA)",
|
||||
Usage: "access <subcommand>",
|
||||
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
|
||||
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
|
||||
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
|
||||
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
|
||||
https://cfl.re/CLIAuthBeta`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Usage: "login <url of access application>",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
administrator. cloudflared stores the token in local storage.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
Action: curl,
|
||||
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.",
|
||||
SkipFlagParsing: true,
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: generateToken,
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "app",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: ssh,
|
||||
Aliases: []string{"rdp"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
Aliases: []string{"H"},
|
||||
Usage: "specify additional headers you wish to send.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: sshConfig,
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: sshGenCertFlag,
|
||||
Usage: "specify if you wish to generate short lived certs.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: 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.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// login pops up the browser window to do the actual login and JWT generation
|
||||
func login(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
args := c.Args()
|
||||
appURL, err := url.Parse(args.First())
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if err := verifyTokenAtEdge(appURL, c); err != nil {
|
||||
logger.WithError(err).Error("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
logger.Error("Please provide the access app and command you wish to run.")
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
tok, err = token.FetchToken(appURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func generateToken(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(os.Stdout, tok); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshConfig prints an example SSH config to stdout
|
||||
func sshConfig(c *cli.Context) error {
|
||||
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
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
// parseAllowRequest will parse cmdArgs and return a copy of the args and result
|
||||
// of the allow request was present
|
||||
func parseAllowRequest(cmdArgs []string) ([]string, bool) {
|
||||
if len(cmdArgs) > 1 {
|
||||
if cmdArgs[0] == "--allow-request" || cmdArgs[0] == "-ar" {
|
||||
return cmdArgs[1:], true
|
||||
}
|
||||
}
|
||||
|
||||
return cmdArgs, false
|
||||
}
|
||||
|
||||
// processURL will preprocess the string (parse to a url, convert to punycode, etc).
|
||||
func processURL(s string) (*url.URL, error) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return nil, errors.New("not a valid host")
|
||||
}
|
||||
|
||||
host, err := idna.ToASCII(u.Hostname())
|
||||
if err != nil { // we fail to convert to punycode, just return the url we parsed.
|
||||
return u, nil
|
||||
}
|
||||
if u.Port() != "" {
|
||||
u.Host = fmt.Sprintf("%s:%s", host, u.Port())
|
||||
} else {
|
||||
u.Host = host
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// cloudflaredPath pulls the full path of cloudflared on disk
|
||||
func cloudflaredPath() string {
|
||||
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
||||
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
||||
if isFileThere(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return "cloudflared"
|
||||
}
|
||||
|
||||
// isFileThere will check for the presence of candidate path
|
||||
func isFileThere(candidate string) bool {
|
||||
fi, err := os.Stat(candidate)
|
||||
if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
}
|
||||
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
return err
|
||||
} else if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := token.RemoveTokenIfExists(appUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if valid, err := isTokenValid(options); err != nil {
|
||||
return err
|
||||
} else if !valid {
|
||||
return errors.New("failed to verify token")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
|
||||
func isTokenValid(options *carrier.StartOptions) (bool, error) {
|
||||
req, err := carrier.BuildAccessRequest(options)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: time.Second * 5,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// A redirect to login means the token was invalid.
|
||||
return !carrier.IsAccessResponse(resp), nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
var (
|
||||
// File names from which we attempt to read configuration.
|
||||
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
)
|
||||
|
||||
const DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func FileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindInputSourceContext pulls the input source from the config flag.
|
||||
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// FindDefaultConfigPath returns the first path that contains a config file.
|
||||
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
|
||||
// contains a config file, return empty string.
|
||||
func FindDefaultConfigPath() string {
|
||||
for _, configDir := range DefaultConfigDirs {
|
||||
for _, configFile := range DefaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := FileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
||||
// i.e. it fails if a user specifies both --url and --unix-socket
|
||||
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
|
||||
return "", errors.New("--unix-socket must be used exclusivly.")
|
||||
}
|
||||
return c.String("unix-socket"), nil
|
||||
}
|
||||
|
||||
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
||||
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
||||
func ValidateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultConfigFiles = []string{"config.yml", "config.yaml"}
|
||||
|
||||
// Launchd doesn't set root env variables, so there is default
|
||||
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
|
||||
defaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
|
||||
)
|
||||
|
||||
const defaultCredentialFile = "cert.pem"
|
||||
|
||||
func fileExists(path string) (bool, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore missing files
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the defaultConfigDirs
|
||||
// (differs by OS for legacy reasons) contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range defaultConfigDirs {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, defaultCredentialFile))
|
||||
if ok, _ := fileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// returns the first path that contains a config file. If none of the combination of
|
||||
// defaultConfigDirs (differs by OS for legacy reasons) and defaultConfigFiles
|
||||
// contains a config file, return empty string
|
||||
func findDefaultConfigPath() string {
|
||||
for _, configDir := range defaultConfigDirs {
|
||||
for _, configFile := range defaultConfigFiles {
|
||||
dirPath, err := homedir.Expand(configDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dirPath, configFile)
|
||||
if ok, _ := fileExists(path); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
|
||||
if context.String("config") != "" {
|
||||
return altsrc.NewYamlSourceFromFile(context.String("config"))
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func generateRandomClientID() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
id := make([]byte, 32)
|
||||
r.Read(id)
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
|
||||
func enoughOptionsSet(c *cli.Context) bool {
|
||||
// For cloudflared to work, the user needs to at least provide a hostname,
|
||||
// or runs as stand alone DNS proxy .
|
||||
// When using sudo, use -E flag to preserve env vars
|
||||
if c.NumFlags() == 0 && c.NArg() == 0 && os.Getenv("TUNNEL_HOSTNAME") == "" && os.Getenv("TUNNEL_DNS") == "" {
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("No arguments were provided. You need to at least specify the hostname for this tunnel. See %s", quickStartUrl)
|
||||
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, run with --proxy-dns option or set TUNNEL_DNS environment variable.")
|
||||
} else {
|
||||
logger.Errorf("You need to specify all the options in a configuration file, or use environment variables. See %s and %s", serviceUrl, argumentsUrl)
|
||||
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, specify proxy-dns option in the configuration file, or set TUNNEL_DNS environment variable.")
|
||||
}
|
||||
cli.ShowAppHelp(c)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleDeprecatedOptions(c *cli.Context) error {
|
||||
// Fail if the user provided an old authentication method
|
||||
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
|
||||
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||||
return fmt.Errorf("Client provided deprecated options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if !c.IsSet("url") {
|
||||
return "", errors.New("Please specify an origin URL.")
|
||||
}
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Flags %v", flags)
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
// Find env variables for Argo Tunnel
|
||||
for _, env := range os.Environ() {
|
||||
// All Argo Tunnel env variables start with TUNNEL_
|
||||
if strings.Contains(env, "TUNNEL_") {
|
||||
vars := strings.Split(env, "=")
|
||||
if len(vars) == 2 {
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context) ([]byte, error) {
|
||||
if c.String("origincert") == "" {
|
||||
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", defaultCredentialFile, defaultConfigDirs)
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
}
|
||||
ok, err := fileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID = generateRandomClientID()
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Tag parse failure")
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
url, err := validateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating url")
|
||||
return nil, errors.Wrap(err, "Error validating url")
|
||||
}
|
||||
logger.Infof("Proxying tunnel requests to %s", url)
|
||||
|
||||
originCert, err := getOriginCert(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
originCertPool, err := loadCertPool(c, logger)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error loading cert pool")
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
OriginUrl: url,
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
|
||||
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"),
|
||||
ProtocolLogger: protoLogger,
|
||||
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"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
|
||||
const originCAPoolFlag = "origin-ca-pool"
|
||||
originCAPoolFilename := c.String(originCAPoolFlag)
|
||||
var originCustomCAPool []byte
|
||||
|
||||
if originCAPoolFilename != "" {
|
||||
var err error
|
||||
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, originCAPoolFlag))
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.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
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Package encrypter is suitable for encrypting messages you would like to securely share between two points.
|
||||
// Useful for providing end to end encryption (E2EE). It uses Box (NaCl) for encrypting the messages.
|
||||
// tldr is it uses Elliptic Curves (Curve25519) for the keys, XSalsa20 and Poly1305 for encryption.
|
||||
// You can read more here https://godoc.org/golang.org/x/crypto/nacl/box.
|
||||
//
|
||||
// msg := []byte("super safe message.")
|
||||
// alice, err := New("alice_priv_key.pem", "alice_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// bob, err := New("bob_priv_key.pem", "bob_pub_key.pem")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// encrypted, err := alice.Encrypt(msg, bob.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// data, err := bob.Decrypt(encrypted, alice.PublicKey())
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(string(data))
|
||||
package encrypter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// Encrypter represents a keypair value with auxiliary functions to make
|
||||
// doing encryption and decryption easier
|
||||
type Encrypter struct {
|
||||
privateKey *[32]byte
|
||||
publicKey *[32]byte
|
||||
}
|
||||
|
||||
// New returns a new encrypter with initialized keypair
|
||||
func New(privateKey, publicKey string) (*Encrypter, error) {
|
||||
e := &Encrypter{}
|
||||
pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.privateKey, e.publicKey = key, pubKey
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests)
|
||||
func (e *Encrypter) PublicKey() string {
|
||||
return base64.URLEncoding.EncodeToString(e.publicKey[:])
|
||||
}
|
||||
|
||||
// Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt
|
||||
// data is an encrypted buffer of data, mostly like from the Encrypt function. Messages contain the nonce data on the front
|
||||
// of the message.
|
||||
// senderPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the decrypted buffer or an error.
|
||||
func (e *Encrypter) Decrypt(data []byte, senderPublicKey string) ([]byte, error) {
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], data[:24]) // we pull the nonce from the front of the actual message.
|
||||
pubKey, err := e.decodePublicKey(senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, ok := box.Open(nil, data[24:], &decryptNonce, pubKey, e.privateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to decrypt message")
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// Encrypt data using our privateKey and the recipient publicKey
|
||||
// data is a buffer of data that we would like to encrypt. Messages will have the nonce added to front
|
||||
// as they have to unique for each message shared.
|
||||
// recipientPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
|
||||
// The return value is the encrypted buffer or an error.
|
||||
func (e *Encrypter) Encrypt(data []byte, recipientPublicKey string) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKey, err := e.decodePublicKey(recipientPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This encrypts msg and adds the nonce to the front of the message, since the nonce has to be
|
||||
// the same for encrypting and decrypting
|
||||
return box.Seal(nonce[:], data, &nonce, pubKey, e.privateKey), nil
|
||||
}
|
||||
|
||||
// WriteKeys keys will take the currently initialized keypair and write them to provided filenames
|
||||
func (e *Encrypter) WriteKeys(privateKey, publicKey string) error {
|
||||
if err := e.writeKey(e.privateKey[:], "BOX PRIVATE KEY", privateKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.writeKey(e.publicKey[:], "PUBLIC KEY", publicKey)
|
||||
}
|
||||
|
||||
// fetchOrGenerateKeys will either load or create a keypair if it doesn't exist
|
||||
func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte, *[32]byte, error) {
|
||||
key, err := e.fetchKey(privateKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pub, err := e.fetchKey(publicKey)
|
||||
if os.IsNotExist(err) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pub, key, nil
|
||||
}
|
||||
|
||||
// writeKey will write a key to disk in DER format (it's a standard pem key)
|
||||
func (e *Encrypter) writeKey(key []byte, pemType, filename string) error {
|
||||
data := pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemType,
|
||||
Bytes: key,
|
||||
})
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchKey will load a a DER formatted key from disk
|
||||
func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
io.Copy(buf, f)
|
||||
|
||||
p, _ := pem.Decode(buf.Bytes())
|
||||
if p == nil {
|
||||
return nil, errors.New("Failed to decode key")
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], p.Bytes)
|
||||
|
||||
return &newKey, nil
|
||||
}
|
||||
|
||||
// decodePublicKey will base64 decode the provided key to the box representation
|
||||
func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) {
|
||||
pub, err := base64.URLEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var newKey [32]byte
|
||||
copy(newKey[:], pub)
|
||||
return &newKey, nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
)
|
||||
|
||||
|
||||
func helloWorld(c *cli.Context) error {
|
||||
address := fmt.Sprintf(":%d", c.Int("port"))
|
||||
listener, err := hello.CreateTLSListener(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
err = hello.StartHelloWorldServer(logger, listener, nil)
|
||||
return err
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,13 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
const serviceConfigDir = "/etc/cloudflared"
|
||||
// The directory and files that are used by the service.
|
||||
// These are hard-coded in the templates below.
|
||||
const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
@@ -77,7 +84,8 @@ WantedBy=timers.target
|
||||
var sysvTemplate = ServiceTemplate{
|
||||
Path: "/etc/init.d/cloudflared",
|
||||
FileMode: 0755,
|
||||
Content: `# For RedHat and cousins:
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# processname: {{.Path}}
|
||||
@@ -174,6 +182,23 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
}
|
||||
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
|
||||
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -181,11 +206,12 @@ func installLinuxService(c *cli.Context) error {
|
||||
}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
|
||||
defaultConfigDir := filepath.Dir(c.String("config"))
|
||||
defaultConfigFile := filepath.Base(c.String("config"))
|
||||
if err = copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile); err != nil {
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
|
||||
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, defaultCredentialFile, defaultConfigFiles[0])
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const baseLoginURL = "https://dash.cloudflare.com/warp"
|
||||
const baseCertStoreURL = "https://login.cloudflarewarp.com"
|
||||
const clientTimeout = time.Minute * 20
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
configPath, err := homedir.Expand(defaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := fileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(configPath, defaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
fmt.Fprintf(os.Stderr, `You have an existing certificate at %s which login would overwrite.
|
||||
If this is intentional, please move or delete that file then run this command again.
|
||||
`, path)
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
|
||||
return err
|
||||
}
|
||||
|
||||
// for local debugging
|
||||
baseURL := baseCertStoreURL
|
||||
if c.IsSet("url") {
|
||||
baseURL = c.String("url")
|
||||
}
|
||||
// Generate a random post URL
|
||||
certURL := baseURL + generateRandomPath()
|
||||
loginURL, err := url.Parse(baseLoginURL)
|
||||
if err != nil {
|
||||
// shouldn't happen, URL is hardcoded
|
||||
return err
|
||||
}
|
||||
loginURL.RawQuery = "callback=" + url.QueryEscape(certURL)
|
||||
|
||||
err = open(loginURL.String())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, `Please open the following URL and log in with your Cloudflare account:
|
||||
|
||||
%s
|
||||
|
||||
Leave cloudflared running to install the certificate automatically.
|
||||
`, loginURL.String())
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL:
|
||||
|
||||
%s
|
||||
|
||||
If the browser failed to open, open it yourself and visit the URL above.
|
||||
|
||||
`, loginURL.String())
|
||||
}
|
||||
|
||||
if download(certURL, path) {
|
||||
fmt.Fprintf(os.Stderr, `You have successfully logged in.
|
||||
If you wish to copy your credentials to a server, they have been saved to:
|
||||
%s
|
||||
`, path)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, `Failed to write the certificate due to the following error:
|
||||
%v
|
||||
|
||||
Your browser will download the certificate instead. You will have to manually
|
||||
copy it to the following path:
|
||||
|
||||
%s
|
||||
|
||||
`, err, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRandomPath generates a random URL to associate with the certificate.
|
||||
func generateRandomPath() string {
|
||||
randomBytes := make([]byte, 40)
|
||||
_, err := rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return "/" + base32.StdEncoding.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// open opens the specified URL in the default browser of the user.
|
||||
func open(url string) error {
|
||||
var cmd string
|
||||
var args []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = "cmd"
|
||||
args = []string{"/c", "start"}
|
||||
case "darwin":
|
||||
cmd = "open"
|
||||
default: // "linux", "freebsd", "openbsd", "netbsd"
|
||||
cmd = "xdg-open"
|
||||
}
|
||||
args = append(args, url)
|
||||
return exec.Command(cmd, args...).Start()
|
||||
}
|
||||
|
||||
func download(certURL, filePath string) bool {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
// attempt a (long-running) certificate get
|
||||
for i := 0; i < 20; i++ {
|
||||
ok, err := tryDownload(client, certURL, filePath)
|
||||
if ok {
|
||||
putSuccess(client, certURL)
|
||||
return true
|
||||
}
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error fetching certificate")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) {
|
||||
resp, err := client.Get(certURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == 404 {
|
||||
return false, nil
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return false, fmt.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
if resp.Header.Get("Content-Type") != "application/x-pem-file" {
|
||||
return false, fmt.Errorf("Unexpected content type %s", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
// write response
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
written, err := io.Copy(file, resp.Body)
|
||||
switch {
|
||||
case err != nil:
|
||||
return false, err
|
||||
case resp.ContentLength != written && resp.ContentLength != -1:
|
||||
return false, fmt.Errorf("Short read (%d bytes) from server while writing certificate", written)
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func putSuccess(client *http.Client, certURL string) {
|
||||
// indicate success to the relay server
|
||||
req, err := http.NewRequest("PUT", certURL+"/ok", nil)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP request error")
|
||||
return
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("HTTP error")
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
+102
-592
@@ -2,48 +2,45 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"sync"
|
||||
"syscall"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
licenseUrl = developerPortal + "/licence/"
|
||||
versionText = "Print the version"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
logger = log.CreateLogger()
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
"An existing connection was forcibly closed by the remote host.",
|
||||
"use of closed connection",
|
||||
"You need to enable Argo Smart Routing",
|
||||
"3001 connection closed",
|
||||
"3002 connection dropped",
|
||||
"rpc exception: dial tcp",
|
||||
"rpc exception: EOF",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
raven.SetDSN(sentryDSN)
|
||||
raven.SetRelease(Version)
|
||||
|
||||
// Force shutdown channel used by the app. When closed, app must terminate.
|
||||
@@ -53,590 +50,92 @@ func main() {
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
cli.VersionFlag = &cli.BoolFlag{
|
||||
Name: "version",
|
||||
Aliases: []string{"v", "V"},
|
||||
Usage: versionText,
|
||||
}
|
||||
|
||||
app := &cli.App{}
|
||||
app.Name = "cloudflared"
|
||||
app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
|
||||
Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
|
||||
app.Usage = "Cloudflare reverse tunnelling proxy agent"
|
||||
app.Usage = "Cloudflare's command-line tool and agent"
|
||||
app.ArgsUsage = "origin-url"
|
||||
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 = `A reverse tunnel proxy agent that connects to Cloudflare's infrastructure.
|
||||
Upon connecting, you are assigned a unique subdomain on cftunnel.com.
|
||||
You need to specify a hostname on a zone you control.
|
||||
A DNS record will be created to CNAME your hostname to the unique subdomain on cftunnel.com.
|
||||
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()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
Requests made to Cloudflare's servers for your hostname will be proxied
|
||||
through the tunnel to your local webserver.`
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "Specifies a config file in YAML format.",
|
||||
Value: findDefaultConfigPath(),
|
||||
},
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "autoupdate-freq",
|
||||
Usage: "Autoupdate frequency. Default is 24h.",
|
||||
Value: time.Hour * 24,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-autoupdate",
|
||||
Usage: "Disable periodic check for updates, restarting the server with the new version.",
|
||||
Value: false,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "is-autoupdated",
|
||||
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
|
||||
Value: false,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "edge",
|
||||
Usage: "Address of the Cloudflare tunnel server.",
|
||||
EnvVars: []string{"TUNNEL_EDGE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "cacert",
|
||||
Usage: "Certificate Authority authenticating the Cloudflare tunnel connection.",
|
||||
EnvVars: []string{"TUNNEL_CACERT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-tls-verify",
|
||||
Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
|
||||
EnvVars: []string{"NO_TLS_VERIFY"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origincert",
|
||||
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
|
||||
Value: findDefaultOriginCertPath(),
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-ca-pool",
|
||||
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"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Value: "https://localhost:8080",
|
||||
Usage: "Connect to the local webserver at `URL`.",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origin-server-name",
|
||||
Usage: "Hostname on the origin server certificate.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "A unique identifier used to tie connections to this tunnel instance.",
|
||||
EnvVars: []string{"TUNNEL_ID"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "lb-pool",
|
||||
Usage: "The name of a (new/existing) load balancing pool to add this origin to.",
|
||||
EnvVars: []string{"TUNNEL_LB_POOL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-key",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_KEY"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-email",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_EMAIL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "api-ca-key",
|
||||
Usage: "This parameter has been deprecated since version 2017.10.1.",
|
||||
EnvVars: []string{"TUNNEL_API_CA_KEY"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "metrics-update-freq",
|
||||
Usage: "Frequency to update tunnel metrics",
|
||||
Value: time.Second * 5,
|
||||
EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified",
|
||||
EnvVars: []string{"TUNNEL_TAG"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "heartbeat-interval",
|
||||
Usage: "Minimum idle time before sending a heartbeat.",
|
||||
Value: time.Second * 5,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewUint64Flag(&cli.Uint64Flag{
|
||||
Name: "heartbeat-count",
|
||||
Usage: "Minimum number of unacked heartbeats to send before closing the connection.",
|
||||
Value: 5,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "info",
|
||||
Usage: "Application logging level {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proto-loglevel",
|
||||
Value: "warn",
|
||||
Usage: "Protocol logging level {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL"},
|
||||
}),
|
||||
altsrc.NewUintFlag(&cli.UintFlag{
|
||||
Name: "retries",
|
||||
Value: 5,
|
||||
Usage: "Maximum number of retries for connection/protocol errors.",
|
||||
EnvVars: []string{"TUNNEL_RETRIES"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "hello-world",
|
||||
Value: false,
|
||||
Usage: "Run Hello World Server",
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "pidfile",
|
||||
Usage: "Write the application's PID to this file after first successful connection.",
|
||||
EnvVars: []string{"TUNNEL_PIDFILE"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "logfile",
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGFILE"},
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "ha-connections",
|
||||
Value: 4,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-connect-timeout",
|
||||
Usage: "HTTP proxy timeout for establishing a new connection",
|
||||
Value: time.Second * 30,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-tls-timeout",
|
||||
Usage: "HTTP proxy timeout for completing a TLS handshake",
|
||||
Value: time.Second * 10,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-tcp-keepalive",
|
||||
Usage: "HTTP proxy TCP keepalive duration",
|
||||
Value: time.Second * 30,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-no-happy-eyeballs",
|
||||
Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-keepalive-connections",
|
||||
Usage: "HTTP proxy maximum keepalive connection pool size",
|
||||
Value: 100,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "proxy-keepalive-timeout",
|
||||
Usage: "HTTP proxy timeout for closing an idle connection",
|
||||
Value: time.Second * 90,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "proxy-dns",
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS"},
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "proxy-dns-port",
|
||||
Value: 53,
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proxy-dns-address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "proxy-dns-upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "grace-period",
|
||||
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
|
||||
Value: time.Second * 30,
|
||||
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewUintFlag(&cli.UintFlag{
|
||||
Name: "compression-quality",
|
||||
Value: 0,
|
||||
Usage: "Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high",
|
||||
EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "no-chunked-encoding",
|
||||
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
|
||||
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "trace-output",
|
||||
Usage: "Name of trace output file, generated when cloudflared stops.",
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
}),
|
||||
}
|
||||
app.Action = func(c *cli.Context) (err error) {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
app.Before = func(context *cli.Context) error {
|
||||
if context.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
|
||||
}
|
||||
inputSource, err := findInputSourceContext(context)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot load configuration from %s", context.String("config"))
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(context, inputSource, app.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot apply configuration from %s", context.String("config"))
|
||||
return err
|
||||
}
|
||||
logger.Infof("Applied configuration from %s", context.String("config"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Description: `Looks for a new version on the offical download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
{
|
||||
Name: "login",
|
||||
Action: login,
|
||||
Usage: "Generate a configuration file with your login details",
|
||||
ArgsUsage: " ",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "url",
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "hello",
|
||||
Action: helloWorld,
|
||||
Usage: "Run a simple \"Hello World\" server for testing Argo Tunnel.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on the selected port.",
|
||||
Value: 8080,
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
},
|
||||
{
|
||||
Name: "proxy-dns",
|
||||
Action: tunneldns.Run,
|
||||
Usage: "Run a DNS over HTTPS proxy server.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Usage: "Listen address for the DNS over HTTPS proxy server.",
|
||||
Value: "localhost",
|
||||
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
|
||||
Value: 53,
|
||||
EnvVars: []string{"TUNNEL_DNS_PORT"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "upstream",
|
||||
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
|
||||
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
|
||||
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Action: func(c *cli.Context) error {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
|
||||
fmt.Printf("\nSQL Database Password: ")
|
||||
pass, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
go sqlgateway.StartProxy(c, logger, string(pass))
|
||||
|
||||
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
if c.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
|
||||
}
|
||||
inputSource, err := findInputSourceContext(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot load configuration from %s", c.String("config"))
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(c, inputSource, app.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot apply configuration from %s", c.String("config"))
|
||||
return err
|
||||
}
|
||||
logger.Infof("Applied configuration from %s", c.String("config"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "db",
|
||||
Value: true,
|
||||
Usage: "Enable the SQL Gateway Proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: "",
|
||||
Usage: "Database connection string: db://user:pass",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
}
|
||||
|
||||
func startServer(c *cli.Context, shutdownC, graceShutdownC chan struct{}) error {
|
||||
var wg sync.WaitGroup
|
||||
listeners := gracenet.Net{}
|
||||
errC := make(chan error)
|
||||
connectedSignal := make(chan struct{})
|
||||
dnsReadySignal := make(chan struct{})
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: updater.Update,
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Description: `Looks for a new version on the official download server.
|
||||
If a new version exists, updates the agent binary and quits.
|
||||
Otherwise, does nothing.
|
||||
|
||||
// check whether client provides enough flags or env variables. If not, print help.
|
||||
if ok := enoughOptionsSet(c); !ok {
|
||||
return nil
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Action: func(c *cli.Context) (err error) {
|
||||
version(c)
|
||||
return nil
|
||||
},
|
||||
Usage: versionText,
|
||||
Description: versionText,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
return cmds
|
||||
}
|
||||
|
||||
if err := configMainLogger(c); err != nil {
|
||||
return errors.Wrap(err, "Error configuring logger")
|
||||
}
|
||||
func flags() []cli.Flag {
|
||||
flags := tunnel.Flags()
|
||||
return append(flags, access.Flags()...)
|
||||
}
|
||||
|
||||
protoLogger, err := configProtoLogger(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error configuring protocol logger")
|
||||
}
|
||||
func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
if c.IsSet("trace-output") {
|
||||
tmpTraceFile, err := ioutil.TempFile("", "trace")
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return func(c *cli.Context) (err error) {
|
||||
if isEmptyInvocation(c) {
|
||||
cli.ShowAppHelpAndExit(c, 1)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
|
||||
handleError(err)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := tmpTraceFile.Close(); err != nil {
|
||||
logger.WithError(err).Error("Failed to close trace output file %s", tmpTraceFile.Name())
|
||||
}
|
||||
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
|
||||
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
|
||||
} else {
|
||||
os.Remove(tmpTraceFile.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
if err := trace.Start(tmpTraceFile); err != nil {
|
||||
logger.WithError(err).Error("Failed to start trace")
|
||||
return errors.Wrap(err, "Error starting tracing")
|
||||
}
|
||||
defer trace.Stop()
|
||||
// we already handle error printing, so we pass an empty string so we
|
||||
// don't have to print again.
|
||||
return cli.Exit("", exitCode)
|
||||
}
|
||||
|
||||
if c.String("logfile") != "" {
|
||||
if err := initLogFile(c, logger, protoLogger); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := handleDeprecatedOptions(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buildInfo := origin.GetBuildInfo()
|
||||
logger.Infof("Build info: %+v", *buildInfo)
|
||||
logger.Infof("Version %s", Version)
|
||||
logClientOptions(c)
|
||||
|
||||
if c.IsSet("proxy-dns") {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
|
||||
}()
|
||||
} else {
|
||||
close(dnsReadySignal)
|
||||
}
|
||||
|
||||
// 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 isAutoupdateEnabled(c) {
|
||||
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- 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")
|
||||
return errors.Wrap(err, "Error opening metrics server listener")
|
||||
}
|
||||
defer metricsListener.Close()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger)
|
||||
}()
|
||||
|
||||
go notifySystemd(connectedSignal)
|
||||
if c.IsSet("pidfile") {
|
||||
go writePidFile(connectedSignal, c.String("pidfile"))
|
||||
}
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
if dnsProxyStandAlone(c) {
|
||||
close(connectedSignal)
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
|
||||
}
|
||||
|
||||
if c.IsSet("hello-world") {
|
||||
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Hello World Server")
|
||||
return errors.Wrap(err, "Cannot start Hello World Server")
|
||||
}
|
||||
defer helloListener.Close()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
|
||||
}()
|
||||
c.Set("url", "https://"+helloListener.Addr().String())
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, logger, protoLogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
|
||||
}()
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
errC chan error,
|
||||
shutdownC, graceShutdownC chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) error {
|
||||
var err error
|
||||
if gracePeriod > 0 {
|
||||
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
|
||||
} else {
|
||||
err = waitForSignal(errC, shutdownC)
|
||||
close(graceShutdownC)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Quitting due to error")
|
||||
} else {
|
||||
logger.Info("Quitting...")
|
||||
}
|
||||
// Wait for clean exit, discarding all errors
|
||||
go func() {
|
||||
for range errC {
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func notifySystemd(waitForSignal chan struct{}) {
|
||||
<-waitForSignal
|
||||
daemon.SdNotify(false, "READY=1")
|
||||
}
|
||||
|
||||
func writePidFile(waitForSignal chan struct{}, pidFile string) {
|
||||
<-waitForSignal
|
||||
file, err := os.Create(pidFile)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
|
||||
}
|
||||
defer file.Close()
|
||||
fmt.Fprintf(file, "%d", os.Getpid())
|
||||
}
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
@@ -651,3 +150,14 @@ func userHomeDir() (string, error) {
|
||||
}
|
||||
return homeDir, nil
|
||||
}
|
||||
|
||||
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
|
||||
func handleError(err error) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
return
|
||||
}
|
||||
}
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
@@ -94,9 +94,9 @@ func runCommand(command string, args ...string) error {
|
||||
}
|
||||
|
||||
func ensureConfigDirExists(configDir string) error {
|
||||
ok, err := fileExists(configDir)
|
||||
ok, err := config.FileExists(configDir)
|
||||
if !ok && err == nil {
|
||||
err = os.Mkdir(configDir, 0700)
|
||||
err = os.Mkdir(configDir, 0755)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -119,8 +119,7 @@ func openFile(path string, create bool) (file *os.File, exists bool, err error)
|
||||
return file, false, err
|
||||
}
|
||||
|
||||
func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
destCredentialPath := filepath.Join(destConfigDir, credentialFile)
|
||||
func copyCredential(srcCredentialPath, destCredentialPath string) error {
|
||||
destFile, exists, err := openFile(destCredentialPath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,7 +129,6 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
srcCredentialPath := filepath.Join(srcConfigDir, credentialFile)
|
||||
srcFile, _, err := openFile(srcCredentialPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -146,17 +144,8 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile string) error {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyCertificate(defaultConfigDir, serviceConfigDir, defaultCredentialFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyConfig(srcConfigPath, destConfigPath string) error {
|
||||
// Copy or create config
|
||||
destConfigPath := filepath.Join(serviceConfigDir, defaultConfigFile)
|
||||
destFile, exists, err := openFile(destConfigPath, true)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("cannot open %s", destConfigPath)
|
||||
@@ -167,7 +156,6 @@ func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defa
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
srcConfigPath := filepath.Join(defaultConfigDir, defaultConfigFile)
|
||||
srcFile, _, err := openFile(srcConfigPath, false)
|
||||
if err != nil {
|
||||
fmt.Println("Your service needs a config file that at least specifies the hostname option.")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build darwin
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("open", url)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
return exec.Command("xdg-open", url)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//+build windows
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getBrowserCmd(url string) *exec.Cmd {
|
||||
cmd := exec.Command("cmd")
|
||||
// CmdLine is only defined when compiling for windows.
|
||||
// Empty string is the cmd proc "Title". Needs to be included because the start command will interpret the first
|
||||
// quoted string as that field and we want to quote the URL.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: fmt.Sprintf(`/c start "" "%s"`, url)}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// OpenBrowser opens the specified URL in the default browser of the user
|
||||
func OpenBrowser(url string) error {
|
||||
return getBrowserCmd(url).Start()
|
||||
}
|
||||
|
||||
// Run will kick off a shell task and pipe the results to the respective std pipes
|
||||
func Run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
stderr, err := c.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stderr, stderr)
|
||||
}()
|
||||
|
||||
stdout, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
io.Copy(os.Stdout, stdout)
|
||||
}()
|
||||
return c.Run()
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
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 := path.GenerateFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileLock := newLock(path)
|
||||
|
||||
err = fileLock.Acquire()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fileLock.Release()
|
||||
|
||||
// check to see if another process has gotten a token while we waited for the lock
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists
|
||||
func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.Encode(), nil
|
||||
}
|
||||
|
||||
// RemoveTokenIfExists removes the a token from local storage if it exists
|
||||
func RemoveTokenIfExists(url *url.URL) error {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
)
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.argotunnel.com/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// Run does the transfer "dance" with the end result downloading the supported resource.
|
||||
// The expanded description is run is encapsulation of shared business logic needed
|
||||
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
|
||||
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
||||
err = shell.OpenBrowser(requestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, open it yourself and visit the URL above.\n", requestURL)
|
||||
}
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceData = buf
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceData, nil
|
||||
}
|
||||
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
func transferRequest(requestURL string) ([]byte, string, error) {
|
||||
client := &http.Client{Timeout: clientTimeout}
|
||||
const pollAttempts = 10
|
||||
// we do "long polling" on the endpoint to get the resource.
|
||||
for i := 0; i < pollAttempts; i++ {
|
||||
buf, key, err := poll(client, requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
logger.WithError(err).Error("Failed to update resource success")
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
}
|
||||
return nil, "", errors.New("Failed to fetch resource")
|
||||
}
|
||||
|
||||
// poll the endpoint for the request resource, waiting for the user interaction
|
||||
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ignore everything other than server errors as the resource
|
||||
// may not exist until the user does the interaction
|
||||
if resp.StatusCode >= 500 {
|
||||
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
logger.Info("Waiting for login...")
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, resp.Body); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
|
||||
}
|
||||
|
||||
// putSuccess tells the server we successfully downloaded the resource
|
||||
func putSuccess(client *http.Client, requestURL string) error {
|
||||
req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
quickStartUrl = developerPortal + "/quickstart/quickstart/"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
)
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
|
||||
// contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigDirs {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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 {
|
||||
// Fail if the user provided an old authentication method
|
||||
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
|
||||
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||||
return fmt.Errorf("Client provided deprecated options")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.WithFields(flags).Info("Flags")
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
// Find env variables for Argo Tunnel
|
||||
for _, env := range os.Environ() {
|
||||
// All Argo Tunnel env variables start with TUNNEL_
|
||||
if strings.Contains(env, "TUNNEL_") {
|
||||
vars := strings.Split(env, "=")
|
||||
if len(vars) == 2 {
|
||||
envs[vars[0]] = vars[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(envs) > 0 {
|
||||
logger.Infof("Environmental variables %v", envs)
|
||||
}
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||||
}
|
||||
|
||||
func getOriginCert(c *cli.Context) ([]byte, error) {
|
||||
if c.String("origincert") == "" {
|
||||
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
|
||||
if isRunningFromTerminal() {
|
||||
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
|
||||
}
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
|
||||
}
|
||||
if !ok {
|
||||
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string, logger,
|
||||
transportLogger *logrus.Logger,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Tag parse failure")
|
||||
return nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCert, err = getOriginCert(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialContext := (&net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating --unix-socket")
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
|
||||
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
|
||||
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
// if --unix-socket specified, enforce network type "unix"
|
||||
return dialContext(ctx, "unix", unixSocket)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
httpTransport.DialContext = dialContext
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
|
||||
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to connect to the origin")
|
||||
return nil, errors.Wrap(err, "unable to connect to the origin")
|
||||
}
|
||||
|
||||
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{
|
||||
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 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"))
|
||||
}
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return connection.NewEdgeAddrResolver(logger)
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// +build ignore
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650`
|
||||
var samplePEM = []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQCb/H0EUrdXEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBPbmUwHhcNMTgwNDI2MTYxMDUxWhcNMjgwNDIzMTYxMDUxWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBPbmUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAwVQD
|
||||
K0SJ25UFLznm2pU3zhzMEvpDEofHVNnCjk4mlDrtVop7PkKZ8pDEmuQANltUrxC8
|
||||
yHBE2wXMv+GlH+bDtwIDAQABMA0GCSqGSIb3DQEBCwUAA0EAjVYQzozIFPkt/HRY
|
||||
uUoZ8zEHIDICb0syFf5VAjm9AgTwIPzUmD+c5vl6LWDnxq7L45nLCzhhQ6YmiwDz
|
||||
X7Wcyg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQDZfCdAJ+mwzDANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBUd28wHhcNMTgwNDI2MTYxMTIwWhcNMjgwNDIzMTYxMTIwWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBUd28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoHKp
|
||||
ROVK3zCSsH7ocYeyRAML4V7SFAbZcb4WIwDnE08oMBVRkQVcW5tqEkvG3RiClfzV
|
||||
wZIJ3CfqKIeSNSDU9wIDAQABMA0GCSqGSIb3DQEBCwUAA0EAJw2gUbnPiq4C2p5b
|
||||
iWzlA9Q7aKo+VQ4H7IZS7tTccr59nVjvH/TG3eWujpnocr4TOqW9M3CK1DF9mUGP
|
||||
3pQ3Jg==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
|
||||
var systemCertPoolSubjects []*pkix.Name
|
||||
|
||||
type certificateFixture struct {
|
||||
ou string
|
||||
cn string
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
systemCertPool, err := x509.SystemCertPool()
|
||||
if isUnrecoverableError(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if systemCertPool == nil {
|
||||
// On Windows, let's just assume the system cert pool was empty
|
||||
systemCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
systemCertPoolSubjects, err = getCertPoolSubjects(systemCertPool)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolJustSystemPool(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
// Remove extra subjects from the cert pool
|
||||
var filteredSystemCertPoolSubjects []*pkix.Name
|
||||
|
||||
t.Log(extraSubjects)
|
||||
|
||||
OUTER:
|
||||
for _, subject := range certPoolSubjects {
|
||||
for _, extraSubject := range extraSubjects {
|
||||
if subject == extraSubject {
|
||||
t.Log(extraSubject)
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
filteredSystemCertPoolSubjects = append(filteredSystemCertPoolSubjects, subject)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(filteredSystemCertPoolSubjects), len(systemCertPoolSubjects))
|
||||
|
||||
difference := subjectSubtract(systemCertPoolSubjects, filteredSystemCertPoolSubjects)
|
||||
assert.Equal(t, 0, len(difference))
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolCFCertificates(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{ou: "CloudFlare Origin SSL ECC Certificate Authority"},
|
||||
{ou: "CloudFlare Origin SSL Certificate Authority"},
|
||||
{cn: "origin-pull.cloudflare.net"},
|
||||
{cn: "Argo Tunnel Sample Hello Server Certificate"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, extraSubjects)
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolWithExtraPEMs(t *testing.T) {
|
||||
certPoolWithoutPEMSubjects := loadCertPoolSubjects(t, nil)
|
||||
certPoolWithPEMSubjects := loadCertPoolSubjects(t, samplePEM)
|
||||
|
||||
difference := subjectSubtract(certPoolWithoutPEMSubjects, certPoolWithPEMSubjects)
|
||||
|
||||
assert.Equal(t, 2, len(difference))
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{cn: "Test One"},
|
||||
{cn: "Test Two"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, difference)
|
||||
}
|
||||
|
||||
func loadCertPoolSubjects(t *testing.T, originCAPoolPEM []byte) []*pkix.Name {
|
||||
certPool, err := loadOriginCertPool(originCAPoolPEM)
|
||||
if isUnrecoverableError(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEmpty(t, certPool.Subjects())
|
||||
certPoolSubjects, err := getCertPoolSubjects(certPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return certPoolSubjects
|
||||
}
|
||||
|
||||
func assertFixturesMatchSubjects(t *testing.T, fixtures []*certificateFixture, subjects []*pkix.Name) {
|
||||
assert.Equal(t, len(fixtures), len(subjects))
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
found := false
|
||||
for _, subject := range subjects {
|
||||
found = found || fixtureMatchesSubjectPredicate(fixture, subject)
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureMatchesSubjectPredicate(fixture *certificateFixture, subject *pkix.Name) bool {
|
||||
cnMatch := true
|
||||
if fixture.cn != "" {
|
||||
cnMatch = fixture.cn == subject.CommonName
|
||||
}
|
||||
|
||||
ouMatch := true
|
||||
if fixture.ou != "" {
|
||||
ouMatch = len(subject.OrganizationalUnit) > 0 && fixture.ou == subject.OrganizationalUnit[0]
|
||||
}
|
||||
|
||||
return cnMatch && ouMatch
|
||||
}
|
||||
|
||||
func subjectSubtract(left []*pkix.Name, right []*pkix.Name) []*pkix.Name {
|
||||
var difference []*pkix.Name
|
||||
|
||||
var found bool
|
||||
for _, r := range right {
|
||||
found = false
|
||||
for _, l := range left {
|
||||
if (*l).String() == (*r).String() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
difference = append(difference, r)
|
||||
}
|
||||
}
|
||||
|
||||
return difference
|
||||
}
|
||||
|
||||
func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
|
||||
var subjects []*pkix.Name
|
||||
|
||||
for _, subject := range certPool.Subjects() {
|
||||
var sequence pkix.RDNSequence
|
||||
_, err := asn1.Unmarshal(subject, &sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := pkix.Name{}
|
||||
name.FillFromRDNSequence(&sequence)
|
||||
|
||||
subjects = append(subjects, &name)
|
||||
}
|
||||
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func isUnrecoverableError(err error) bool {
|
||||
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
@@ -14,6 +13,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
|
||||
"Response status, content length and header will also be logged in debug level."
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
func configMainLogger(c *cli.Context) error {
|
||||
@@ -23,18 +25,21 @@ func configMainLogger(c *cli.Context) error {
|
||||
return errors.Wrap(err, "Unknown logging level specified")
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
if logLevel == logrus.DebugLevel {
|
||||
logger.Warn(debugLevelWarning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configProtoLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
protoLogLevel, err := logrus.ParseLevel(c.String("proto-loglevel"))
|
||||
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Unknown protocol logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown protocol logging level specified")
|
||||
logger.WithError(err).Fatal("Unknown transport logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown transport logging level specified")
|
||||
}
|
||||
protoLogger := logrus.New()
|
||||
protoLogger.Level = protoLogLevel
|
||||
return protoLogger, nil
|
||||
transportLogger := logrus.New()
|
||||
transportLogger.Level = transportLogLevel
|
||||
return transportLogger, nil
|
||||
}
|
||||
|
||||
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
@@ -54,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,
|
||||
@@ -0,0 +1,69 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
)
|
||||
|
||||
func login(c *cli.Context) error {
|
||||
path, ok, err := checkForExistingCert()
|
||||
if ok {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginURL, err := url.Parse(baseLoginURL)
|
||||
if err != nil {
|
||||
// shouldn't happen, URL is hardcoded
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkForExistingCert() (string, bool, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return "", false, 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 "", false, err
|
||||
}
|
||||
path := filepath.Join(configPath, config.DefaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
return path, true, nil
|
||||
}
|
||||
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
|
||||
return path, false, err
|
||||
}
|
||||
|
||||
return path, false, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,115 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
)
|
||||
|
||||
const (
|
||||
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."
|
||||
)
|
||||
|
||||
var publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
}
|
||||
|
||||
func checkForUpdates() ReleaseInfo {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return ReleaseInfo{}
|
||||
case err != nil:
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
}
|
||||
|
||||
return ReleaseInfo{Updated: true, Version: resp.ReleaseVersion}
|
||||
}
|
||||
|
||||
func update(_ *cli.Context) error {
|
||||
if updateApplied() {
|
||||
os.Exit(64)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
|
||||
tickC := time.Tick(freq)
|
||||
for {
|
||||
if updateApplied() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateApplied() bool {
|
||||
releaseInfo := checkForUpdates()
|
||||
if releaseInfo.Updated {
|
||||
logger.Infof("Updated to version %s", releaseInfo.Version)
|
||||
return true
|
||||
}
|
||||
if releaseInfo.Error != nil {
|
||||
logger.WithError(releaseInfo.Error).Error("Update check failed")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAutoupdateEnabled(c *cli.Context) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Info(noUpdateOnWindowsMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
var (
|
||||
publicKey = []byte(`
|
||||
-----BEGIN ECDSA PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
|
||||
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
|
||||
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
-----END ECDSA PUBLIC KEY-----
|
||||
`)
|
||||
logger = log.CreateLogger()
|
||||
)
|
||||
|
||||
type UpdateOutcome struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error != nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func checkForUpdateAndApply() UpdateOutcome {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return UpdateOutcome{}
|
||||
case err != nil:
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
updateOutcome := loggedUpdate()
|
||||
if updateOutcome.Error != nil {
|
||||
os.Exit(10)
|
||||
}
|
||||
|
||||
if updateOutcome.noUpdate() {
|
||||
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
|
||||
}
|
||||
|
||||
return updateOutcome.Error
|
||||
}
|
||||
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate() UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
}
|
||||
if updateOutcome.Error != nil {
|
||||
logger.WithError(updateOutcome.Error).Error("update check failed")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
const (
|
||||
windowsServiceName = "Cloudflared"
|
||||
windowsServiceDescription = "Argo Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
|
||||
recoverActionDelay = time.Second * 20
|
||||
failureCountResetPeriod = time.Hour * 24
|
||||
@@ -29,6 +31,10 @@ const (
|
||||
// not defined in golang.org/x/sys/windows package
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx
|
||||
serviceConfigFailureActionsFlag = 4
|
||||
|
||||
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
|
||||
serviceControllerConnectionFailure = 1063
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
@@ -49,16 +55,48 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
},
|
||||
})
|
||||
|
||||
// `IsAnInteractiveSession()` isn't exactly equivalent to "should the
|
||||
// process run as a normal EXE?" There are legitimate non-service cases,
|
||||
// like running cloudflared in a GCP startup script, for which
|
||||
// `IsAnInteractiveSession()` returns false. For more context, see:
|
||||
// https://github.com/judwhite/go-svc/issues/6
|
||||
// It seems that the "correct way" to check "is this a normal EXE?" is:
|
||||
// 1. attempt to connect to the Service Control Manager
|
||||
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// This involves actually trying to start the service.
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
}
|
||||
|
||||
if isIntSess {
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
|
||||
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
|
||||
// Run the app in "interactive" mode anyway.
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
|
||||
@@ -67,26 +105,23 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
defer elog.Close()
|
||||
|
||||
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, elog: elog, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err))
|
||||
return
|
||||
defer func() {
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}()
|
||||
|
||||
// the arguments passed here are only meaningful if they were manually
|
||||
// specified by the user, e.g. using the Services console or `sc start`.
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point
|
||||
// https://stackoverflow.com/a/6235139
|
||||
var args []string
|
||||
if len(serviceArgs) > 1 {
|
||||
args = serviceArgs
|
||||
} else {
|
||||
// fall back to the arguments from ImagePath (or, as sc calls it, binPath)
|
||||
args = os.Args
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
elog *eventlog.Log
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
statusChan <- svc.Status{State: svc.StartPending}
|
||||
errC := make(chan error)
|
||||
@@ -100,26 +135,23 @@ func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, stat
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
|
||||
statusChan <- c.CurrentStatus
|
||||
case svc.Stop:
|
||||
s.elog.Info(1, "received stop control request")
|
||||
close(s.graceShutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
case svc.Shutdown:
|
||||
s.elog.Info(1, "received shutdown control request")
|
||||
close(s.shutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
default:
|
||||
s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case err := <-errC:
|
||||
ssec = true
|
||||
if err != nil {
|
||||
s.elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
errno = 1
|
||||
} else {
|
||||
s.elog.Info(1, "cloudflared terminated without error")
|
||||
elog.Info(1, "cloudflared terminated without error")
|
||||
errno = 0
|
||||
}
|
||||
return
|
||||
@@ -163,7 +195,7 @@ func installWindowsService(c *cli.Context) error {
|
||||
err = configRecoveryOption(s.Handle)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot set service recovery actions")
|
||||
logger.Infof("See %s to manually configure service recovery actions", serviceUrl)
|
||||
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA origintunneld servers
|
||||
srvService = "origintunneld"
|
||||
srvProto = "tcp"
|
||||
srvName = "argotunnel.com"
|
||||
|
||||
// Used to fallback to DoT when we can't use the default resolver to
|
||||
// discover HA origintunneld servers (GitHub issue #75).
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
|
||||
// SRV record resolution TTL
|
||||
resolveEdgeAddrTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
`Please try the following things to diagnose this issue:`,
|
||||
` 1. ensure that argotunnel.com is returning "origintunneld" service records.`,
|
||||
` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`,
|
||||
` 2. ensure that your DNS resolver is not returning compressed SRV records.`,
|
||||
` See GitHub issue https://github.com/golang/go/issues/27546`,
|
||||
` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`,
|
||||
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
|
||||
}
|
||||
|
||||
// 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"),
|
||||
}
|
||||
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.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
// But both of these cases miss out on a key feature from the stdlib:
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
|
||||
// Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it.
|
||||
// See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552
|
||||
r := fallbackResolver(dotServerName, dotServerAddr)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dotTimeout)
|
||||
defer cancel()
|
||||
_, fallbackAddrs, fallbackErr := r.LookupSRV(ctx, srvService, srvProto, srvName)
|
||||
if fallbackErr != nil || len(fallbackAddrs) == 0 {
|
||||
// use the original DNS error `err` in messages, not `fallbackErr`
|
||||
logger.Errorln("Error looking up Cloudflare edge IPs: the DNS query failed:", err)
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Errorln(s)
|
||||
}
|
||||
return nil, errors.Wrap(err, "Could not lookup srv records on _origintunneld._tcp.argotunnel.com")
|
||||
}
|
||||
// Accept the fallback results and keep going
|
||||
addrs = fallbackAddrs
|
||||
}
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var lookupErr error
|
||||
for _, addr := range addrs {
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
if err != nil || len(ips) == 0 {
|
||||
// don't return early, we might be able to resolve other addresses
|
||||
lookupErr = err
|
||||
continue
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
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) {
|
||||
ips, err := net.LookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var result []*net.TCPAddr
|
||||
for len(ipsByService) > 0 {
|
||||
filtered := ipsByService[:0]
|
||||
for _, ips := range ipsByService {
|
||||
if len(ips) == 0 {
|
||||
// sanity check
|
||||
continue
|
||||
}
|
||||
result = append(result, ips[0])
|
||||
if len(ips) > 1 {
|
||||
filtered = append(filtered, ips[1:])
|
||||
}
|
||||
}
|
||||
ipsByService = filtered
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
|
||||
func fallbackResolver(serverName, serverAddress string) *net.Resolver {
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "tcp", serverAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := &tls.Config{ServerName: serverName}
|
||||
return tls.Client(conn, tlsConfig), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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},
|
||||
@@ -0,0 +1,294 @@
|
||||
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/"
|
||||
defaultRetryAfter = time.Second * 5
|
||||
)
|
||||
|
||||
// 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
|
||||
IntentLabel 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 connErr := em.newConnection(ctx); connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
em.logger.WithError(connErr).Error(em.noRetryMessage())
|
||||
return connErr
|
||||
}
|
||||
em.logger.WithError(connErr).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) *pogs.ConnectError {
|
||||
edgeIP := em.serviceDiscoverer.Addr()
|
||||
edgeConn, err := em.dialEdge(ctx, edgeIP)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("dial edge error: %v", err))
|
||||
}
|
||||
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 {
|
||||
retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err))
|
||||
}
|
||||
|
||||
h2muxConn, err := newConnection(muxer, edgeIP)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("couldn't create h2mux connection: %v", err))
|
||||
}
|
||||
|
||||
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(),
|
||||
IntentLabel: em.cloudflaredConfig.IntentLabel,
|
||||
Tags: em.cloudflaredConfig.Tags,
|
||||
}, em.logger)
|
||||
if err != nil {
|
||||
h2muxConn.Shutdown()
|
||||
return retryConnection(fmt.Sprintf("couldn't connect to edge: %v", err))
|
||||
}
|
||||
|
||||
if connErr := connResult.ConnectError(); connErr != nil {
|
||||
return connErr
|
||||
}
|
||||
|
||||
em.state.newConnection(h2muxConn)
|
||||
em.logger.Infof("connected to %s", connResult.ConnectedTo())
|
||||
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
|
||||
}
|
||||
|
||||
func retryConnection(cause string) *pogs.ConnectError {
|
||||
return &pogs.ConnectError{
|
||||
Cause: cause,
|
||||
RetryAfter: defaultRetryAfter,
|
||||
ShouldRetry: true,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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}
|
||||
)
|
||||
|
||||
+106
-67
@@ -1,7 +1,6 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -15,11 +14,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFrameSize uint32 = 1 << 14 // Minimum frame size in http2 spec
|
||||
defaultWindowSize uint32 = 65535
|
||||
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size specified in http2 spec
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultFrameSize uint32 = 1 << 14 // Minimum frame size in http2 spec
|
||||
defaultWindowSize uint32 = (1 << 16) - 1 // Minimum window size in http2 spec
|
||||
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size in http2 spec
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
|
||||
|
||||
SettingMuxerMagic http2.SettingID = 0x42db
|
||||
MuxerMagicOrigin uint32 = 0xa2e43c8b
|
||||
@@ -49,6 +49,12 @@ type MuxerConfig struct {
|
||||
// Logger to use
|
||||
Logger *log.Entry
|
||||
CompressionQuality CompressionSetting
|
||||
// Initial size for HTTP2 flow control windows
|
||||
DefaultWindowSize uint32
|
||||
// Largest allowable size for HTTP2 flow control windows
|
||||
MaxWindowSize uint32
|
||||
// Largest allowable capacity for the buffer of data to be sent
|
||||
StreamWriteBufferMaxLen int
|
||||
}
|
||||
|
||||
type Muxer struct {
|
||||
@@ -64,7 +70,7 @@ type Muxer struct {
|
||||
// muxWriter is the write process.
|
||||
muxWriter *MuxWriter
|
||||
// muxMetricsUpdater is the process to update metrics
|
||||
muxMetricsUpdater *muxMetricsUpdater
|
||||
muxMetricsUpdater muxMetricsUpdater
|
||||
// newStreamChan is used to create new streams on the writer thread.
|
||||
// The writer will assign the next available stream ID.
|
||||
newStreamChan chan MuxedStreamRequest
|
||||
@@ -87,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(
|
||||
@@ -98,6 +112,15 @@ func Handshake(
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = defaultTimeout
|
||||
}
|
||||
if config.DefaultWindowSize == 0 {
|
||||
config.DefaultWindowSize = defaultWindowSize
|
||||
}
|
||||
if config.MaxWindowSize == 0 {
|
||||
config.MaxWindowSize = maxWindowSize
|
||||
}
|
||||
if config.StreamWriteBufferMaxLen == 0 {
|
||||
config.StreamWriteBufferMaxLen = defaultWriteBufferMaxLen
|
||||
}
|
||||
// Initialise connection state fields
|
||||
m := &Muxer{
|
||||
f: http2.NewFramer(w, r), // A framer that writes to w and reads from r
|
||||
@@ -147,11 +170,6 @@ func Handshake(
|
||||
// set up reader/writer pair ready for serve
|
||||
streamErrors := NewStreamErrorMap()
|
||||
goAwayChan := make(chan http2.ErrCode, 1)
|
||||
updateRTTChan := make(chan *roundTripMeasurement, 1)
|
||||
updateReceiveWindowChan := make(chan uint32, 1)
|
||||
updateSendWindowChan := make(chan uint32, 1)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
inBoundCounter := NewAtomicCounter(0)
|
||||
outBoundCounter := NewAtomicCounter(0)
|
||||
pingTimestamp := NewPingTimestamp()
|
||||
@@ -168,6 +186,14 @@ func Handshake(
|
||||
config.Logger.Warn("Minimum number of unacked heartbeats to send before closing the connection has been adjusted to ", maxRetries)
|
||||
}
|
||||
|
||||
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
|
||||
m.muxMetricsUpdater = newMuxMetricsUpdater(
|
||||
m.abortChan,
|
||||
compBytesBefore,
|
||||
compBytesAfter,
|
||||
)
|
||||
|
||||
m.explicitShutdown = NewBooleanFuse()
|
||||
m.muxReader = &MuxReader{
|
||||
f: m.f,
|
||||
@@ -179,47 +205,30 @@ func Handshake(
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
connActive: connActive,
|
||||
initialStreamWindow: defaultWindowSize,
|
||||
streamWindowMax: maxWindowSize,
|
||||
initialStreamWindow: m.config.DefaultWindowSize,
|
||||
streamWindowMax: m.config.MaxWindowSize,
|
||||
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
r: m.r,
|
||||
updateRTTChan: updateRTTChan,
|
||||
updateReceiveWindowChan: updateReceiveWindowChan,
|
||||
updateSendWindowChan: updateSendWindowChan,
|
||||
metricsUpdater: m.muxMetricsUpdater,
|
||||
bytesRead: inBoundCounter,
|
||||
updateInBoundBytesChan: updateInBoundBytesChan,
|
||||
}
|
||||
m.muxWriter = &MuxWriter{
|
||||
f: m.f,
|
||||
streams: m.streams,
|
||||
streamErrors: streamErrors,
|
||||
readyStreamChan: m.readyList.ReadyChannel(),
|
||||
newStreamChan: m.newStreamChan,
|
||||
goAwayChan: goAwayChan,
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
idleTimer: NewIdleTimer(idleDuration, maxRetries),
|
||||
connActiveChan: connActive.WaitChannel(),
|
||||
maxFrameSize: defaultFrameSize,
|
||||
updateReceiveWindowChan: updateReceiveWindowChan,
|
||||
updateSendWindowChan: updateSendWindowChan,
|
||||
bytesWrote: outBoundCounter,
|
||||
updateOutBoundBytesChan: updateOutBoundBytesChan,
|
||||
f: m.f,
|
||||
streams: m.streams,
|
||||
streamErrors: streamErrors,
|
||||
readyStreamChan: m.readyList.ReadyChannel(),
|
||||
newStreamChan: m.newStreamChan,
|
||||
goAwayChan: goAwayChan,
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
idleTimer: NewIdleTimer(idleDuration, maxRetries),
|
||||
connActiveChan: connActive.WaitChannel(),
|
||||
maxFrameSize: defaultFrameSize,
|
||||
metricsUpdater: m.muxMetricsUpdater,
|
||||
bytesWrote: outBoundCounter,
|
||||
}
|
||||
m.muxWriter.headerEncoder = hpack.NewEncoder(&m.muxWriter.headerBuffer)
|
||||
|
||||
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
|
||||
m.muxMetricsUpdater = newMuxMetricsUpdater(
|
||||
updateRTTChan,
|
||||
updateReceiveWindowChan,
|
||||
updateSendWindowChan,
|
||||
updateInBoundBytesChan,
|
||||
updateOutBoundBytesChan,
|
||||
m.abortChan,
|
||||
compBytesBefore,
|
||||
compBytesAfter,
|
||||
)
|
||||
|
||||
if m.compressionQuality.dictSize > 0 && m.compressionQuality.nDicts > 0 {
|
||||
nd, sz := m.compressionQuality.nDicts, m.compressionQuality.dictSize
|
||||
writeDicts, dictChan := newH2WriteDictionaries(
|
||||
@@ -305,6 +314,12 @@ func joinErrorsWithTimeout(errChan <-chan error, receiveCount int, timeout time.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve runs the event loops that comprise h2mux:
|
||||
// - MuxReader.run()
|
||||
// - MuxWriter.run()
|
||||
// - muxMetricsUpdater.run()
|
||||
// In the normal case, Shutdown() is called concurrently with Serve() to stop
|
||||
// these loops.
|
||||
func (m *Muxer) Serve(ctx context.Context) error {
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
@@ -335,6 +350,7 @@ func (m *Muxer) Serve(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown is called to initiate the "happy path" of muxer termination.
|
||||
func (m *Muxer) Shutdown() {
|
||||
m.explicitShutdown.Fuse(true)
|
||||
m.muxReader.Shutdown()
|
||||
@@ -370,41 +386,64 @@ 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{},
|
||||
receiveWindow: defaultWindowSize,
|
||||
receiveWindowCurrentMax: defaultWindowSize, // Initial window size limit. exponentially increase it when receiveWindow is exhausted
|
||||
receiveWindowMax: maxWindowSize,
|
||||
sendWindow: 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
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) Metrics() *MuxerMetrics {
|
||||
return m.muxMetricsUpdater.Metrics()
|
||||
return m.muxMetricsUpdater.metrics()
|
||||
}
|
||||
|
||||
func (m *Muxer) abort() {
|
||||
m.abortOnce.Do(func() {
|
||||
close(m.abortChan)
|
||||
m.readyList.Close()
|
||||
m.streams.Abort()
|
||||
})
|
||||
}
|
||||
|
||||
+300
-240
@@ -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,86 +43,80 @@ 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,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Timeout: testHandshakeTimeout,
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
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,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Timeout: time.Second,
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: log.NewEntry(log.New()),
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: log.NewEntry(log.New()),
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
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() {
|
||||
@@ -149,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))
|
||||
}
|
||||
@@ -175,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)
|
||||
@@ -186,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,
|
||||
)
|
||||
@@ -216,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)
|
||||
@@ -224,14 +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
|
||||
streamReady := make(chan struct{})
|
||||
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))
|
||||
}
|
||||
@@ -257,14 +259,13 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
if n != len(payload) {
|
||||
t.Fatalf("origin short write: %d/%d bytes", n, len(payload))
|
||||
}
|
||||
t.Log("Payload written; signaling that the stream is ready")
|
||||
streamReady <- struct{}{}
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -282,9 +283,6 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
}
|
||||
responseBody := make([]byte, bodySize)
|
||||
|
||||
<-streamReady
|
||||
t.Log("Received stream ready signal; resuming the test")
|
||||
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -367,20 +365,18 @@ func TestMultipleStreams(t *testing.T) {
|
||||
log.Error(err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreamsFlowControl failed")
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
maxStreams := 32
|
||||
errorsC := make(chan error, maxStreams)
|
||||
streamReady := make(chan struct{})
|
||||
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))
|
||||
}
|
||||
@@ -398,7 +394,6 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
payload[i] = byte(i % 256)
|
||||
}
|
||||
n, err := stream.Write(payload)
|
||||
streamReady <- struct{}{}
|
||||
if err != nil {
|
||||
t.Fatalf("origin write error: %s", err)
|
||||
}
|
||||
@@ -407,64 +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)
|
||||
}
|
||||
|
||||
<-streamReady
|
||||
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"},
|
||||
})
|
||||
@@ -482,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)
|
||||
@@ -514,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"},
|
||||
@@ -536,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,
|
||||
)
|
||||
@@ -583,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:
|
||||
@@ -600,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"},
|
||||
@@ -629,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"},
|
||||
@@ -691,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">` +
|
||||
@@ -715,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
|
||||
|
||||
@@ -747,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,88 +759,105 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Add(len(paths))
|
||||
errorsC := make(chan error, len(paths))
|
||||
|
||||
for i, s := range paths {
|
||||
go func(i int, path string) {
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
go func(index int, path string) {
|
||||
defer wg.Done()
|
||||
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 {
|
||||
log.Printf("error from (*MuxedStream).Read: %s", err)
|
||||
t.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
errorsC <- fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
|
||||
return
|
||||
}
|
||||
if n != len(expectBody) {
|
||||
log.Printf("expected response body to have %d bytes, got %d", len(expectBody), n)
|
||||
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
|
||||
errorsC <- fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(expectBody), n)
|
||||
return
|
||||
}
|
||||
if string(responseBody[:n]) != expectBody {
|
||||
log.Printf("expected response body %s, got %s", expectBody, responseBody[:n])
|
||||
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
|
||||
errorsC <- fmt.Errorf("stream %d expected response body %s, got %s", stream.streamID, expectBody, responseBody[:n])
|
||||
return
|
||||
}
|
||||
wg.Done()
|
||||
}(i, s)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
wg.Wait()
|
||||
close(errorsC)
|
||||
testFail := false
|
||||
for err := range errorsC {
|
||||
testFail = true
|
||||
log.Error(err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
}
|
||||
|
||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
@@ -875,86 +869,152 @@ 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()
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
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() {
|
||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 100*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
||||
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())
|
||||
}
|
||||
|
||||
+151
-36
@@ -17,73 +17,143 @@ type ReadWriteClosedCloser interface {
|
||||
Closed() bool
|
||||
}
|
||||
|
||||
// MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data.
|
||||
type MuxedStream struct {
|
||||
Headers []Header
|
||||
|
||||
streamID uint32
|
||||
|
||||
// The "Receive" end of the stream
|
||||
readBufferLock sync.RWMutex
|
||||
readBuffer ReadWriteClosedCloser
|
||||
// This is the amount of bytes that are in our receive window
|
||||
// (how much data we can receive into this stream).
|
||||
receiveWindow uint32
|
||||
// current receive window size limit. Exponentially increase it when it's exhausted
|
||||
receiveWindowCurrentMax uint32
|
||||
// hard limit set in http2 spec. 2^31-1
|
||||
receiveWindowMax uint32
|
||||
// The desired size increment for receiveWindow.
|
||||
// If this is nonzero, a WINDOW_UPDATE frame needs to be sent.
|
||||
windowUpdate uint32
|
||||
// The headers that were most recently received.
|
||||
// Particularly:
|
||||
// * for an eyeball-initiated stream (as passed to TunnelHandler::ServeStream),
|
||||
// these are the request headers
|
||||
// * for a cloudflared-initiated stream (as created by Register/UnregisterTunnel),
|
||||
// these are the response headers.
|
||||
// They are useful in both of these contexts; hence `Headers` is public.
|
||||
Headers []Header
|
||||
// For use in the context of a cloudflared-initiated stream.
|
||||
responseHeadersReceived chan struct{}
|
||||
|
||||
readBuffer ReadWriteClosedCloser
|
||||
receiveWindow uint32
|
||||
// current window size limit. Exponentially increase it when it's exhausted
|
||||
receiveWindowCurrentMax uint32
|
||||
// limit set in http2 spec. 2^31-1
|
||||
receiveWindowMax uint32
|
||||
|
||||
// nonzero if a WINDOW_UPDATE frame for a stream needs to be sent
|
||||
windowUpdate uint32
|
||||
|
||||
writeLock sync.Mutex
|
||||
// The zero value for Buffer is an empty buffer ready to use.
|
||||
// The "Send" end of the stream
|
||||
writeLock sync.Mutex
|
||||
writeBuffer ReadWriteLengther
|
||||
|
||||
// The maximum capacity that the send buffer should grow to.
|
||||
writeBufferMaxLen int
|
||||
// A channel to be notified when the send buffer is not full.
|
||||
writeBufferHasSpace chan struct{}
|
||||
// This is the amount of bytes that are in the peer's receive window
|
||||
// (how much data we can send from this stream).
|
||||
sendWindow uint32
|
||||
|
||||
readyList *ReadyList
|
||||
// Reference to the muxer's readyList; signal this for stream data to be sent.
|
||||
readyList *ReadyList
|
||||
// The headers that should be sent, and a flag so we only send them once.
|
||||
headersSent bool
|
||||
writeHeaders []Header
|
||||
|
||||
// EOF-related fields
|
||||
// true if the write end of this stream has been closed
|
||||
writeEOF bool
|
||||
// true if we have sent EOF to the peer
|
||||
sentEOF bool
|
||||
// true if the peer sent us an EOF
|
||||
receivedEOF bool
|
||||
|
||||
// dictionary that was used to compress the stream
|
||||
// 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
|
||||
contentType string
|
||||
path string
|
||||
dictionaries h2Dictionaries
|
||||
readBufferLock sync.RWMutex
|
||||
}
|
||||
|
||||
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 {
|
||||
s.readBufferLock.RLock()
|
||||
b := s.readBuffer
|
||||
readBuffer = s.readBuffer
|
||||
s.readBufferLock.RUnlock()
|
||||
return b.Read(p)
|
||||
} else {
|
||||
readBuffer = s.readBuffer
|
||||
}
|
||||
return s.readBuffer.Read(p)
|
||||
n, err = readBuffer.Read(p)
|
||||
s.replenishReceiveWindow(uint32(n))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Write(p []byte) (n int, err error) {
|
||||
// Blocks until len(p) bytes have been written to the buffer
|
||||
func (s *MuxedStream) Write(p []byte) (int, error) {
|
||||
// If assignDictToStream returns success, then it will have acquired the
|
||||
// writeLock. Otherwise we must acquire it ourselves.
|
||||
ok := assignDictToStream(s, p)
|
||||
if !ok {
|
||||
s.writeLock.Lock()
|
||||
}
|
||||
defer s.writeLock.Unlock()
|
||||
|
||||
if s.writeEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err = s.writeBuffer.Write(p)
|
||||
if n != len(p) || err != nil {
|
||||
return n, err
|
||||
totalWritten := 0
|
||||
for totalWritten < len(p) {
|
||||
// If the buffer is full, block till there is more room.
|
||||
// Use a loop to recheck the buffer size after the lock is reacquired.
|
||||
for s.writeBufferMaxLen <= s.writeBuffer.Len() {
|
||||
s.writeLock.Unlock()
|
||||
<-s.writeBufferHasSpace
|
||||
s.writeLock.Lock()
|
||||
}
|
||||
amountToWrite := len(p) - totalWritten
|
||||
spaceAvailable := s.writeBufferMaxLen - s.writeBuffer.Len()
|
||||
if spaceAvailable < amountToWrite {
|
||||
amountToWrite = spaceAvailable
|
||||
}
|
||||
amountWritten, err := s.writeBuffer.Write(p[totalWritten : totalWritten+amountToWrite])
|
||||
totalWritten += amountWritten
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
s.writeNotify()
|
||||
}
|
||||
s.writeNotify()
|
||||
return n, nil
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Close() error {
|
||||
@@ -143,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()
|
||||
@@ -164,9 +253,9 @@ func (s *MuxedStream) writeNotify() {
|
||||
// receive window (how much data we can send).
|
||||
func (s *MuxedStream) replenishSendWindow(bytes uint32) {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
s.sendWindow += bytes
|
||||
s.writeNotify()
|
||||
s.writeLock.Unlock()
|
||||
}
|
||||
|
||||
// Call by muxreader when it receives a data frame
|
||||
@@ -178,17 +267,30 @@ func (s *MuxedStream) consumeReceiveWindow(bytes uint32) bool {
|
||||
return false
|
||||
}
|
||||
s.receiveWindow -= bytes
|
||||
if s.receiveWindow < s.receiveWindowCurrentMax/2 {
|
||||
if s.receiveWindow < s.receiveWindowCurrentMax/2 && s.receiveWindowCurrentMax < s.receiveWindowMax {
|
||||
// exhausting client send window (how much data client can send)
|
||||
if s.receiveWindowCurrentMax < s.receiveWindowMax {
|
||||
s.receiveWindowCurrentMax <<= 1
|
||||
// and there is room to grow the receive window
|
||||
newMax := s.receiveWindowCurrentMax << 1
|
||||
if newMax > s.receiveWindowMax {
|
||||
newMax = s.receiveWindowMax
|
||||
}
|
||||
s.windowUpdate += s.receiveWindowCurrentMax - s.receiveWindow
|
||||
s.windowUpdate += newMax - s.receiveWindowCurrentMax
|
||||
s.receiveWindowCurrentMax = newMax
|
||||
// notify MuxWriter to write WINDOW_UPDATE frame
|
||||
s.writeNotify()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Arranges for the MuxWriter to send a WINDOW_UPDATE
|
||||
// Called by MuxedStream::Read when data has left the read buffer.
|
||||
func (s *MuxedStream) replenishReceiveWindow(bytes uint32) {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
s.windowUpdate += bytes
|
||||
s.writeNotify()
|
||||
}
|
||||
|
||||
// receiveEOF should be called when the peer indicates no more data will be sent.
|
||||
// Returns true if the socket is now closed (i.e. the write side is already closed).
|
||||
func (s *MuxedStream) receiveEOF() (closed bool) {
|
||||
@@ -226,7 +328,8 @@ type streamChunk struct {
|
||||
// true if a HEADERS frame should be sent
|
||||
sendHeaders bool
|
||||
headers []Header
|
||||
// nonzero if a WINDOW_UPDATE frame should be sent
|
||||
// nonzero if a WINDOW_UPDATE frame should be sent;
|
||||
// in that case, it is the increment value to use
|
||||
windowUpdate uint32
|
||||
// true if data frames should be sent
|
||||
sendData bool
|
||||
@@ -249,11 +352,23 @@ func (s *MuxedStream) getChunk() *streamChunk {
|
||||
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
|
||||
}
|
||||
|
||||
// Copies at most s.sendWindow bytes
|
||||
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
|
||||
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
|
||||
s.sendWindow -= uint32(writeLen)
|
||||
|
||||
// Non-blocking channel send. This will allow MuxedStream::Write() to continue, if needed
|
||||
if s.writeBuffer.Len() < s.writeBufferMaxLen {
|
||||
select {
|
||||
case s.writeBufferHasSpace <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// When we write the chunk, we'll write the WINDOW_UPDATE frame if needed
|
||||
s.receiveWindow += s.windowUpdate
|
||||
s.windowUpdate = 0
|
||||
|
||||
// When we write the chunk, we'll write the headers if needed
|
||||
s.headersSent = true
|
||||
|
||||
// if this chunk contains the end of the stream, close the stream now
|
||||
|
||||
+54
-19
@@ -23,47 +23,55 @@ func TestFlowControlSingleStream(t *testing.T) {
|
||||
sendWindow: testWindowSize,
|
||||
readyList: NewReadyList(),
|
||||
}
|
||||
var tempWindowUpdate uint32
|
||||
var tempStreamChunk *streamChunk
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(testWindowSize/2))
|
||||
dataSent := testWindowSize / 2
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempWindowUpdate := stream.windowUpdate
|
||||
|
||||
streamChunk := stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, uint32(0), tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(2))
|
||||
dataSent += 2
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, (testWindowSize<<1)-stream.receiveWindow, stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, testWindowSize, stream.windowUpdate)
|
||||
tempWindowUpdate = stream.windowUpdate
|
||||
|
||||
streamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, (testWindowSize<<1)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(testWindowSize+10))
|
||||
dataSent = testWindowSize + 10
|
||||
dataSent += testWindowSize + 10
|
||||
assert.Equal(t, (testWindowSize<<1)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, (testWindowSize<<2)-stream.receiveWindow, stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.windowUpdate)
|
||||
tempWindowUpdate = stream.windowUpdate
|
||||
|
||||
streamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, (testWindowSize<<2)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.False(t, stream.consumeReceiveWindow(testMaxWindowSize+1))
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindow)
|
||||
assert.Equal(t, (testWindowSize<<2)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testMaxWindowSize, stream.receiveWindowCurrentMax)
|
||||
}
|
||||
|
||||
@@ -90,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())
|
||||
}
|
||||
}
|
||||
|
||||
+75
-20
@@ -17,7 +17,24 @@ const (
|
||||
updateFreq = time.Second
|
||||
)
|
||||
|
||||
type muxMetricsUpdater struct {
|
||||
type muxMetricsUpdater interface {
|
||||
// metrics returns the latest metrics
|
||||
metrics() *MuxerMetrics
|
||||
// run is a blocking call to start the event loop
|
||||
run(logger *log.Entry) error
|
||||
// updateRTTChan is called by muxReader to report new RTT measurements
|
||||
updateRTT(rtt *roundTripMeasurement)
|
||||
//updateReceiveWindowChan is called by muxReader and muxWriter when receiveWindow size is updated
|
||||
updateReceiveWindow(receiveWindow uint32)
|
||||
//updateSendWindowChan is called by muxReader and muxWriter when sendWindow size is updated
|
||||
updateSendWindow(sendWindow uint32)
|
||||
// updateInBoundBytesChan is called periodicallyby muxReader to report bytesRead
|
||||
updateInBoundBytes(inBoundBytes uint64)
|
||||
// updateOutBoundBytesChan is called periodically by muxWriter to report bytesWrote
|
||||
updateOutBoundBytes(outBoundBytes uint64)
|
||||
}
|
||||
|
||||
type muxMetricsUpdaterImpl struct {
|
||||
// rttData keeps record of rtt, rttMin, rttMax and last measured time
|
||||
rttData *rttData
|
||||
// receiveWindowData keeps record of receive window measurement
|
||||
@@ -28,16 +45,16 @@ type muxMetricsUpdater struct {
|
||||
inBoundRate *rate
|
||||
// outBoundRate is outgoing bytes/sec
|
||||
outBoundRate *rate
|
||||
// updateRTTChan is the channel to receive new RTT measurement from muxReader
|
||||
updateRTTChan <-chan *roundTripMeasurement
|
||||
//updateReceiveWindowChan is the channel to receive updated receiveWindow size from muxReader and muxWriter
|
||||
updateReceiveWindowChan <-chan uint32
|
||||
//updateSendWindowChan is the channel to receive updated sendWindow size from muxReader and muxWriter
|
||||
updateSendWindowChan <-chan uint32
|
||||
// updateInBoundBytesChan us the channel to receive bytesRead from muxReader
|
||||
updateInBoundBytesChan <-chan uint64
|
||||
// updateOutBoundBytesChan us the channel to receive bytesWrote from muxWriter
|
||||
updateOutBoundBytesChan <-chan uint64
|
||||
// updateRTTChan is the channel to receive new RTT measurement
|
||||
updateRTTChan chan *roundTripMeasurement
|
||||
//updateReceiveWindowChan is the channel to receive updated receiveWindow size
|
||||
updateReceiveWindowChan chan uint32
|
||||
//updateSendWindowChan is the channel to receive updated sendWindow size
|
||||
updateSendWindowChan chan uint32
|
||||
// updateInBoundBytesChan us the channel to receive bytesRead
|
||||
updateInBoundBytesChan chan uint64
|
||||
// updateOutBoundBytesChan us the channel to receive bytesWrote
|
||||
updateOutBoundBytesChan chan uint64
|
||||
// shutdownC is to signal the muxerMetricsUpdater to shutdown
|
||||
abortChan <-chan struct{}
|
||||
|
||||
@@ -84,15 +101,16 @@ type rate struct {
|
||||
}
|
||||
|
||||
func newMuxMetricsUpdater(
|
||||
updateRTTChan <-chan *roundTripMeasurement,
|
||||
updateReceiveWindowChan <-chan uint32,
|
||||
updateSendWindowChan <-chan uint32,
|
||||
updateInBoundBytesChan <-chan uint64,
|
||||
updateOutBoundBytesChan <-chan uint64,
|
||||
abortChan <-chan struct{},
|
||||
compBytesBefore, compBytesAfter *AtomicCounter,
|
||||
) *muxMetricsUpdater {
|
||||
return &muxMetricsUpdater{
|
||||
) muxMetricsUpdater {
|
||||
updateRTTChan := make(chan *roundTripMeasurement, 1)
|
||||
updateReceiveWindowChan := make(chan uint32, 1)
|
||||
updateSendWindowChan := make(chan uint32, 1)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
|
||||
return &muxMetricsUpdaterImpl{
|
||||
rttData: newRTTData(),
|
||||
receiveWindowData: newFlowControlData(),
|
||||
sendWindowData: newFlowControlData(),
|
||||
@@ -109,7 +127,7 @@ func newMuxMetricsUpdater(
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdater) Metrics() *MuxerMetrics {
|
||||
func (updater *muxMetricsUpdaterImpl) metrics() *MuxerMetrics {
|
||||
m := &MuxerMetrics{}
|
||||
m.RTT, m.RTTMin, m.RTTMax = updater.rttData.metrics()
|
||||
m.ReceiveWindowAve, m.ReceiveWindowMin, m.ReceiveWindowMax = updater.receiveWindowData.metrics()
|
||||
@@ -120,7 +138,7 @@ func (updater *muxMetricsUpdater) Metrics() *MuxerMetrics {
|
||||
return m
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdater) run(parentLogger *log.Entry) error {
|
||||
func (updater *muxMetricsUpdaterImpl) run(parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithFields(log.Fields{
|
||||
"subsystem": "mux",
|
||||
"dir": "metrics",
|
||||
@@ -152,6 +170,43 @@ func (updater *muxMetricsUpdater) run(parentLogger *log.Entry) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateRTT(rtt *roundTripMeasurement) {
|
||||
select {
|
||||
case updater.updateRTTChan <- rtt:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateReceiveWindow(receiveWindow uint32) {
|
||||
select {
|
||||
case updater.updateReceiveWindowChan <- receiveWindow:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateSendWindow(sendWindow uint32) {
|
||||
select {
|
||||
case updater.updateSendWindowChan <- sendWindow:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateInBoundBytes(inBoundBytes uint64) {
|
||||
select {
|
||||
case updater.updateInBoundBytesChan <- inBoundBytes:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateOutBoundBytes(outBoundBytes uint64) {
|
||||
select {
|
||||
case updater.updateOutBoundBytesChan <- outBoundBytes:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func newRTTData() *rttData {
|
||||
return &rttData{}
|
||||
}
|
||||
|
||||
+18
-29
@@ -86,24 +86,11 @@ func TestFlowControlDataUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMuxMetricsUpdater(t *testing.T) {
|
||||
t.Skip("Race condition")
|
||||
updateRTTChan := make(chan *roundTripMeasurement)
|
||||
updateReceiveWindowChan := make(chan uint32)
|
||||
updateSendWindowChan := make(chan uint32)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
abortChan := make(chan struct{})
|
||||
t.Skip("Inherently racy test due to muxMetricsUpdaterImpl.run()")
|
||||
errChan := make(chan error)
|
||||
abortChan := make(chan struct{})
|
||||
compBefore, compAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
m := newMuxMetricsUpdater(updateRTTChan,
|
||||
updateReceiveWindowChan,
|
||||
updateSendWindowChan,
|
||||
updateInBoundBytesChan,
|
||||
updateOutBoundBytesChan,
|
||||
abortChan,
|
||||
compBefore,
|
||||
compAfter,
|
||||
)
|
||||
m := newMuxMetricsUpdater(abortChan, compBefore, compAfter)
|
||||
logger := log.NewEntry(log.New())
|
||||
|
||||
go func() {
|
||||
@@ -116,42 +103,44 @@ func TestMuxMetricsUpdater(t *testing.T) {
|
||||
// mock muxReader
|
||||
readerStart := time.Now()
|
||||
rm := &roundTripMeasurement{receiveTime: readerStart, sendTime: readerStart}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// Becareful if dataPoints is not divisibile by 4
|
||||
assert.Equal(t, 0, dataPoints%4,
|
||||
"dataPoints is not divisible by 4; this test should be adjusted accordingly")
|
||||
readerSend := readerStart.Add(time.Millisecond)
|
||||
for i := 1; i <= dataPoints/4; i++ {
|
||||
readerReceive := readerSend.Add(time.Duration(i) * time.Millisecond)
|
||||
rm := &roundTripMeasurement{receiveTime: readerReceive, sendTime: readerSend}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
readerSend = readerReceive.Add(time.Millisecond)
|
||||
m.updateReceiveWindow(uint32(i))
|
||||
m.updateSendWindow(uint32(i))
|
||||
|
||||
updateReceiveWindowChan <- uint32(i)
|
||||
updateSendWindowChan <- uint32(i)
|
||||
|
||||
updateInBoundBytesChan <- uint64(i)
|
||||
m.updateInBoundBytes(uint64(i))
|
||||
}
|
||||
}()
|
||||
|
||||
// mock muxWriter
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
assert.Equal(t, 0, dataPoints%4,
|
||||
"dataPoints is not divisible by 4; this test should be adjusted accordingly")
|
||||
for j := dataPoints/4 + 1; j <= dataPoints/2; j++ {
|
||||
updateReceiveWindowChan <- uint32(j)
|
||||
updateSendWindowChan <- uint32(j)
|
||||
m.updateReceiveWindow(uint32(j))
|
||||
m.updateSendWindow(uint32(j))
|
||||
|
||||
// should always be disgard since the send time is before readerSend
|
||||
// should always be disgarded since the send time is before readerSend
|
||||
rm := &roundTripMeasurement{receiveTime: readerStart, sendTime: readerStart.Add(-time.Duration(j*dataPoints) * time.Millisecond)}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
|
||||
updateOutBoundBytesChan <- uint64(j)
|
||||
m.updateOutBoundBytes(uint64(j))
|
||||
}
|
||||
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
metrics := m.Metrics()
|
||||
metrics := m.metrics()
|
||||
points := dataPoints / 2
|
||||
assert.Equal(t, time.Millisecond, metrics.RTTMin)
|
||||
assert.Equal(t, time.Duration(dataPoints/4)*time.Millisecond, metrics.RTTMax)
|
||||
|
||||
+30
-21
@@ -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
|
||||
@@ -35,18 +39,14 @@ type MuxReader struct {
|
||||
initialStreamWindow uint32
|
||||
// The max value for the send window of a stream.
|
||||
streamWindowMax uint32
|
||||
// The max size for the write buffer of a stream
|
||||
streamWriteBufferMaxLen int
|
||||
// r is a reference to the underlying connection used when shutting down.
|
||||
r io.Closer
|
||||
// updateRTTChan is the channel to send new RTT measurement to muxerMetricsUpdater
|
||||
updateRTTChan chan<- *roundTripMeasurement
|
||||
// updateReceiveWindowChan is the channel to update receiveWindow size to muxerMetricsUpdater
|
||||
updateReceiveWindowChan chan<- uint32
|
||||
// updateSendWindowChan is the channel to update sendWindow size to muxerMetricsUpdater
|
||||
updateSendWindowChan chan<- uint32
|
||||
// bytesRead is the amount of bytes read from data frame since the last time we send bytes read to metrics
|
||||
// metricsUpdater is used to report metrics
|
||||
metricsUpdater muxMetricsUpdater
|
||||
// bytesRead is the amount of bytes read from data frames since the last time we called metricsUpdater.updateInBoundBytes()
|
||||
bytesRead *AtomicCounter
|
||||
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
|
||||
updateInBoundBytesChan chan<- uint64
|
||||
// dictionaries holds the h2 cross-stream compression dictionaries
|
||||
dictionaries h2Dictionaries
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
case <-r.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
r.updateInBoundBytesChan <- r.bytesRead.Count()
|
||||
r.metricsUpdater.updateInBoundBytes(r.bytesRead.Count())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -87,23 +87,28 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
for {
|
||||
frame, err := r.f.ReadFrame()
|
||||
if err != nil {
|
||||
errLogger := logger.WithError(err)
|
||||
if errorDetail := r.f.ErrorDetail(); errorDetail != nil {
|
||||
errLogger = errLogger.WithField("errorDetail", errorDetail)
|
||||
}
|
||||
switch e := err.(type) {
|
||||
case http2.StreamError:
|
||||
logger.WithError(err).Warn("stream error")
|
||||
errLogger.Warn("stream error")
|
||||
r.streamError(e.StreamID, e.Code)
|
||||
case http2.ConnectionError:
|
||||
logger.WithError(err).Warn("connection error")
|
||||
errLogger.Warn("connection error")
|
||||
return r.connectionError(err)
|
||||
default:
|
||||
if isConnectionClosedError(err) {
|
||||
if r.streams.Len() == 0 {
|
||||
// don't log the error here -- that would just be extra noise
|
||||
logger.Debug("shutting down")
|
||||
return nil
|
||||
}
|
||||
logger.Warn("connection closed unexpectedly")
|
||||
errLogger.Warn("connection closed unexpectedly")
|
||||
return err
|
||||
} else {
|
||||
logger.WithError(err).Warn("frame read error")
|
||||
errLogger.Warn("frame read error")
|
||||
return r.connectionError(err)
|
||||
}
|
||||
}
|
||||
@@ -153,6 +158,8 @@ func (r *MuxReader) newMuxedStream(streamID uint32) *MuxedStream {
|
||||
streamID: streamID,
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: r.streamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: r.initialStreamWindow,
|
||||
receiveWindowCurrentMax: r.initialStreamWindow,
|
||||
receiveWindowMax: r.streamWindowMax,
|
||||
@@ -237,6 +244,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})
|
||||
}
|
||||
@@ -285,7 +294,7 @@ func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.E
|
||||
if !stream.consumeReceiveWindow(uint32(len(data))) {
|
||||
return r.streamError(stream.streamID, http2.ErrCodeFlowControl)
|
||||
}
|
||||
r.updateReceiveWindowChan <- stream.getReceiveWindow()
|
||||
r.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -297,13 +306,13 @@ func (r *MuxReader) receivePingData(frame *http2.PingFrame) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update updates the computed values with a new measurement.
|
||||
// outgoingTime is the time that the probe was sent.
|
||||
// We assume that time.Now() is the time we received that probe.
|
||||
r.updateRTTChan <- &roundTripMeasurement{
|
||||
// Update the computed RTT aggregations with a new measurement.
|
||||
// `ts` is the time that the probe was sent.
|
||||
// We assume that `time.Now()` is the time we received that probe.
|
||||
r.metricsUpdater.updateRTT(&roundTripMeasurement{
|
||||
receiveTime: time.Now(),
|
||||
sendTime: time.Unix(0, ts),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Receive a GOAWAY from the peer. Gracefully shut down our connection.
|
||||
@@ -464,7 +473,7 @@ func (r *MuxReader) updateStreamWindow(frame *http2.WindowUpdateFrame) error {
|
||||
return nil
|
||||
}
|
||||
stream.replenishSendWindow(frame.Increment)
|
||||
r.updateSendWindowChan <- stream.getSendWindow()
|
||||
r.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
+7
-10
@@ -40,14 +40,11 @@ type MuxWriter struct {
|
||||
headerEncoder *hpack.Encoder
|
||||
// headerBuffer is the temporary buffer used by headerEncoder.
|
||||
headerBuffer bytes.Buffer
|
||||
// updateReceiveWindowChan is the channel to update receiveWindow size to muxerMetricsUpdater
|
||||
updateReceiveWindowChan chan<- uint32
|
||||
// updateSendWindowChan is the channel to update sendWindow size to muxerMetricsUpdater
|
||||
updateSendWindowChan chan<- uint32
|
||||
// bytesWrote is the amount of bytes wrote to data frame since the last time we send bytes wrote to metrics
|
||||
|
||||
// metricsUpdater is used to report metrics
|
||||
metricsUpdater muxMetricsUpdater
|
||||
// bytesWrote is the amount of bytes written to data frames since the last time we called metricsUpdater.updateOutBoundBytes()
|
||||
bytesWrote *AtomicCounter
|
||||
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
|
||||
updateOutBoundBytesChan chan<- uint64
|
||||
|
||||
useDictChan <-chan useDictRequest
|
||||
}
|
||||
@@ -83,7 +80,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
case <-w.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
w.updateOutBoundBytesChan <- w.bytesWrote.Count()
|
||||
w.metricsUpdater.updateOutBoundBytes(w.bytesWrote.Count())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -172,8 +169,8 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) error {
|
||||
logger.Debug("writable")
|
||||
chunk := stream.getChunk()
|
||||
w.updateReceiveWindowChan <- stream.getReceiveWindow()
|
||||
w.updateSendWindowChan <- stream.getSendWindow()
|
||||
w.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
||||
w.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
||||
if chunk.sendHeadersFrame() {
|
||||
err := w.writeHeaders(chunk.streamID, chunk.headers)
|
||||
if err != nil {
|
||||
|
||||
+24
-13
@@ -1,15 +1,23 @@
|
||||
package h2mux
|
||||
|
||||
import "sync"
|
||||
|
||||
// ReadyList multiplexes several event signals onto a single channel.
|
||||
type ReadyList struct {
|
||||
// signalC is used to signal that a stream can be enqueued
|
||||
signalC chan uint32
|
||||
waitC chan uint32
|
||||
// waitC is used to signal the ID of the first ready descriptor
|
||||
waitC chan uint32
|
||||
// doneC is used to signal that run should terminate
|
||||
doneC chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewReadyList() *ReadyList {
|
||||
rl := &ReadyList{
|
||||
signalC: make(chan uint32),
|
||||
waitC: make(chan uint32),
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
go rl.run()
|
||||
return rl
|
||||
@@ -17,7 +25,11 @@ func NewReadyList() *ReadyList {
|
||||
|
||||
// ID is the stream ID
|
||||
func (r *ReadyList) Signal(ID uint32) {
|
||||
r.signalC <- ID
|
||||
select {
|
||||
case r.signalC <- ID:
|
||||
// ReadyList already closed
|
||||
case <-r.doneC:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReadyList) ReadyChannel() <-chan uint32 {
|
||||
@@ -25,7 +37,9 @@ func (r *ReadyList) ReadyChannel() <-chan uint32 {
|
||||
}
|
||||
|
||||
func (r *ReadyList) Close() {
|
||||
close(r.signalC)
|
||||
r.closeOnce.Do(func() {
|
||||
close(r.doneC)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *ReadyList) run() {
|
||||
@@ -35,28 +49,25 @@ func (r *ReadyList) run() {
|
||||
activeDescriptors := newReadyDescriptorMap()
|
||||
for {
|
||||
if firstReady == nil {
|
||||
// Wait for first ready descriptor
|
||||
i, ok := <-r.signalC
|
||||
if !ok {
|
||||
// closed
|
||||
select {
|
||||
case i := <-r.signalC:
|
||||
firstReady = activeDescriptors.SetIfMissing(i)
|
||||
case <-r.doneC:
|
||||
return
|
||||
}
|
||||
firstReady = activeDescriptors.SetIfMissing(i)
|
||||
}
|
||||
select {
|
||||
case r.waitC <- firstReady.ID:
|
||||
activeDescriptors.Delete(firstReady.ID)
|
||||
firstReady = queue.Dequeue()
|
||||
case i, ok := <-r.signalC:
|
||||
if !ok {
|
||||
// closed
|
||||
return
|
||||
}
|
||||
case i := <-r.signalC:
|
||||
newReady := activeDescriptors.SetIfMissing(i)
|
||||
if newReady != nil {
|
||||
// key doesn't exist
|
||||
queue.Enqueue(newReady)
|
||||
}
|
||||
case <-r.doneC:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-24
@@ -3,36 +3,59 @@ package h2mux
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReadyList(t *testing.T) {
|
||||
func assertEmpty(t *testing.T, rl *ReadyList) {
|
||||
select {
|
||||
case <-rl.ReadyChannel():
|
||||
t.Fatal("Spurious wakeup")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func assertClosed(t *testing.T, rl *ReadyList) {
|
||||
select {
|
||||
case _, ok := <-rl.ReadyChannel():
|
||||
assert.False(t, ok, "ReadyChannel was not closed")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func receiveWithTimeout(t *testing.T, rl *ReadyList) uint32 {
|
||||
select {
|
||||
case i := <-rl.ReadyChannel():
|
||||
return i
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyListEmpty(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
c := rl.ReadyChannel()
|
||||
// helper functions
|
||||
assertEmpty := func() {
|
||||
select {
|
||||
case <-c:
|
||||
t.Fatalf("Spurious wakeup")
|
||||
default:
|
||||
}
|
||||
}
|
||||
receiveWithTimeout := func() uint32 {
|
||||
select {
|
||||
case i := <-c:
|
||||
return i
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// no signals, receive should fail
|
||||
assertEmpty()
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
func TestReadyListSignal(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
assertEmpty(t, rl)
|
||||
|
||||
rl.Signal(0)
|
||||
if receiveWithTimeout() != 0 {
|
||||
if receiveWithTimeout(t, rl) != 0 {
|
||||
t.Fatalf("Received wrong ID of signalled event")
|
||||
}
|
||||
// no new signals, receive should fail
|
||||
assertEmpty()
|
||||
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
|
||||
func TestReadyListMultipleSignals(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
assertEmpty(t, rl)
|
||||
|
||||
// Signals should not block;
|
||||
// Duplicate unhandled signals should not cause multiple wakeups
|
||||
signalled := [5]bool{}
|
||||
@@ -42,12 +65,45 @@ func TestReadyList(t *testing.T) {
|
||||
}
|
||||
// All signals should be received once (in any order)
|
||||
for range signalled {
|
||||
i := receiveWithTimeout()
|
||||
i := receiveWithTimeout(t, rl)
|
||||
if signalled[i] {
|
||||
t.Fatalf("Received signal %d more than once", i)
|
||||
}
|
||||
signalled[i] = true
|
||||
}
|
||||
for i := range signalled {
|
||||
if !signalled[i] {
|
||||
t.Fatalf("Never received signal %d", i)
|
||||
}
|
||||
}
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
|
||||
func TestReadyListClose(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
rl.Close()
|
||||
|
||||
// readyList.run() occurs in a separate goroutine,
|
||||
// so there's no way to directly check that run() has terminated.
|
||||
// Perform an indirect check: is the ready channel closed?
|
||||
assertClosed(t, rl)
|
||||
|
||||
// a second rl.Close() shouldn't cause a panic
|
||||
rl.Close()
|
||||
|
||||
// Signal shouldn't block after Close()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
rl.Signal(uint32(i))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyDescriptorQueue(t *testing.T) {
|
||||
|
||||
+1
-3
@@ -43,7 +43,7 @@ const indexTemplate = `
|
||||
<meta name="description" content="Argo Tunnel Connection">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}section{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent;-webkit-text-decoration-skip:objects}/* 1 */::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */a,body,dd,div,dl,dt,h1,h4,html,p,section{box-sizing:border-box}.bt{border-top-style:solid;border-top-width:1px}.bl{border-left-style:solid;border-left-width:1px}.b--orange{border-color:#f38020}.br1{border-radius:.125rem}.bw2{border-width:.25rem}.dib{display:inline-block}.sans-serif{font-family:open sans,-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.code{font-family:Consolas,monaco,monospace}.b{font-weight:700}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.mw-100{max-width:100%}.mw4{max-width:8rem}.mw7{max-width:48rem}.bg-light-gray{background-color:#f7f7f7}.link-hover:hover{background-color:#1f679e}.white{color:#fff}.bg-white{background-color:#fff}.bg-blue{background-color:#408bc9}.pb2{padding-bottom:.5rem}.pb6{padding-bottom:8rem}.pt3{padding-top:1rem}.pt5{padding-top:4rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ml0{margin-left:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mt5{margin-top:4rem}.ttu{text-transform:uppercase}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.center{margin-left:auto}.center{margin-right:auto}@media screen and (min-width:30em){.f2-ns{font-size:2.25rem}}@media screen and (min-width:30em) and (max-width:60em){.f5-m{font-size:1rem}}@media screen and (min-width:60em){.f4-l{font-size:1.25rem}}
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}section{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent;-webkit-text-decoration-skip:objects}/* 1 */::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */a,body,dd,div,dl,dt,h1,h4,html,p,section{box-sizing:border-box}.bt{border-top-style:solid;border-top-width:1px}.bl{border-left-style:solid;border-left-width:1px}.b--orange{border-color:#f38020}.br1{border-radius:.125rem}.bw2{border-width:.25rem}.dib{display:inline-block}.sans-serif{font-family:open sans,-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.overflow-x-auto{overflow-x:auto}.code{font-family:Consolas,monaco,monospace}.b{font-weight:700}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.mw-100{max-width:100%}.mw4{max-width:8rem}.mw7{max-width:48rem}.bg-light-gray{background-color:#f7f7f7}.link-hover:hover{background-color:#1f679e}.white{color:#fff}.bg-white{background-color:#fff}.bg-blue{background-color:#408bc9}.pb2{padding-bottom:.5rem}.pb6{padding-bottom:8rem}.pt3{padding-top:1rem}.pt5{padding-top:4rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ml0{margin-left:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mt5{margin-top:4rem}.ttu{text-transform:uppercase}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.center{margin-left:auto}.center{margin-right:auto}@media screen and (min-width:30em){.f2-ns{font-size:2.25rem}}@media screen and (min-width:30em) and (max-width:60em){.f5-m{font-size:1rem}}@media screen and (min-width:60em){.f4-l{font-size:1.25rem}}
|
||||
.st0{fill:#FFF}.st1{fill:#f48120}.st2{fill:#faad3f}.st3{fill:#404041}
|
||||
</style>
|
||||
</head>
|
||||
@@ -91,7 +91,6 @@ const indexTemplate = `
|
||||
</html>
|
||||
`
|
||||
|
||||
|
||||
func StartHelloWorldServer(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}) error {
|
||||
logger.Infof("Starting Hello World server at %s", listener.Addr())
|
||||
serverName := defaultServerName
|
||||
@@ -132,7 +131,6 @@ func CreateTLSListener(address string) (net.Listener, error) {
|
||||
return listener, err
|
||||
}
|
||||
|
||||
|
||||
func uptimeHandler(startTime time.Time) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Note that if autoupdate is enabled, the uptime is reset when a new client
|
||||
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/trace"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Redeclare time functions so they can be overridden in tests.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func immediateTimeAfter(time.Duration) <-chan time.Time {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/golibs/lrucache"
|
||||
)
|
||||
|
||||
// StatusPage.io API docs:
|
||||
// https://www.cloudflarestatus.com/api/v2/#incidents-unresolved
|
||||
const (
|
||||
activeIncidentsURL = "https://yh6f0r4529hb.statuspage.io/api/v2/incidents/unresolved.json"
|
||||
argoTunnelKeyword = "argo tunnel"
|
||||
incidentDetailsPrefix = "https://www.cloudflarestatus.com/incidents/"
|
||||
)
|
||||
|
||||
// IncidentLookup is an object that checks for active incidents in
|
||||
// the Cloudflare infrastructure.
|
||||
type IncidentLookup interface {
|
||||
ActiveIncidents() []Incident
|
||||
}
|
||||
|
||||
// NewIncidentLookup returns a new IncidentLookup instance that caches its
|
||||
// results with a 1-minute TTL.
|
||||
func NewIncidentLookup() IncidentLookup {
|
||||
return newCachedIncidentLookup(fetchActiveIncidents)
|
||||
}
|
||||
|
||||
type IncidentUpdate struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
type Incident struct {
|
||||
Name string
|
||||
ID string `json:"id"`
|
||||
Updates []IncidentUpdate `json:"incident_updates"`
|
||||
}
|
||||
|
||||
type StatusPage struct {
|
||||
Incidents []Incident
|
||||
}
|
||||
|
||||
func (i Incident) URL() string {
|
||||
return incidentDetailsPrefix + i.ID
|
||||
}
|
||||
|
||||
func parseStatusPage(data []byte) (*StatusPage, error) {
|
||||
var result StatusPage
|
||||
err := json.Unmarshal(data, &result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func isArgoTunnelIncident(i Incident) bool {
|
||||
if strings.Contains(strings.ToLower(i.Name), argoTunnelKeyword) {
|
||||
return true
|
||||
}
|
||||
for _, u := range i.Updates {
|
||||
if strings.Contains(strings.ToLower(u.Body), argoTunnelKeyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchActiveIncidents() (incidents []Incident) {
|
||||
resp, err := http.Get(activeIncidentsURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
statusPage, err := parseStatusPage(body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, i := range statusPage.Incidents {
|
||||
if isArgoTunnelIncident(i) {
|
||||
incidents = append(incidents, i)
|
||||
}
|
||||
}
|
||||
return incidents
|
||||
}
|
||||
|
||||
type cachedIncidentLookup struct {
|
||||
cache *lrucache.LRUCache
|
||||
ttl time.Duration
|
||||
uncachedLookup func() []Incident
|
||||
}
|
||||
|
||||
func newCachedIncidentLookup(uncachedLookup func() []Incident) *cachedIncidentLookup {
|
||||
return &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: time.Minute,
|
||||
uncachedLookup: uncachedLookup,
|
||||
}
|
||||
}
|
||||
|
||||
// We only need one cache entry. Always use the empty string as its key.
|
||||
const cacheKey = ""
|
||||
|
||||
func (c *cachedIncidentLookup) ActiveIncidents() []Incident {
|
||||
if cached, ok := c.cache.GetNotStale(cacheKey); ok {
|
||||
if incidents, ok := cached.([]Incident); ok {
|
||||
return incidents
|
||||
}
|
||||
}
|
||||
incidents := c.uncachedLookup()
|
||||
c.cache.Set(cacheKey, incidents, time.Now().Add(c.ttl))
|
||||
return incidents
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/golibs/lrucache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseStatusPage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input []byte
|
||||
output *StatusPage
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
input: []byte(`<html>
|
||||
<head><title>504 Gateway Time-out</title></head>
|
||||
<body><center><h1>504 Gateway Time-out</h1></center></body>
|
||||
</html>`),
|
||||
output: nil,
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
input: []byte(`{
|
||||
"page": {
|
||||
"id": "yh6f0r4529hb",
|
||||
"name": "Cloudflare",
|
||||
"url": "https://www.cloudflarestatus.com",
|
||||
"time_zone": "Etc/UTC",
|
||||
"updated_at": "2019-01-10T20:11:38.750Z"
|
||||
},
|
||||
"incidents": [
|
||||
{
|
||||
"name": "Cloudflare API service issues",
|
||||
"status": "resolved",
|
||||
"created_at": "2018-09-17T19:29:21.132Z",
|
||||
"updated_at": "2018-09-18T07:45:41.313Z",
|
||||
"monitoring_at": "2018-09-17T21:35:06.492Z",
|
||||
"resolved_at": "2018-09-18T07:45:41.290Z",
|
||||
"shortlink": "http://stspg.io/7f079791e",
|
||||
"id": "q746ybtyb6q0",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"incident_updates": [
|
||||
{
|
||||
"status": "resolved",
|
||||
"body": "Cloudflare has resolved the issue and the service have resumed normal operation.",
|
||||
"created_at": "2018-09-18T07:45:41.290Z",
|
||||
"updated_at": "2018-09-18T07:45:41.290Z",
|
||||
"display_at": "2018-09-18T07:45:41.290Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": true,
|
||||
"tweet_id": null,
|
||||
"id": "zl5g2pl5zhfs",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "monitoring",
|
||||
"body": "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.",
|
||||
"created_at": "2018-09-17T21:35:06.492Z",
|
||||
"updated_at": "2018-09-17T21:35:06.492Z",
|
||||
"display_at": "2018-09-17T21:35:06.492Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "degraded_performance",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "0001sv3chdnx",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "We are continuing to investigate this issue.",
|
||||
"created_at": "2018-09-17T19:30:08.049Z",
|
||||
"updated_at": "2018-09-17T19:30:08.049Z",
|
||||
"display_at": "2018-09-17T19:30:08.049Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "degraded_performance"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "qdr164tfpq7m",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.",
|
||||
"created_at": "2018-09-17T19:29:21.201Z",
|
||||
"updated_at": "2018-09-17T19:29:21.201Z",
|
||||
"display_at": "2018-09-17T19:29:21.201Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "qzl2n0q8tskg",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"status": "operational",
|
||||
"name": "Cloudflare APIs",
|
||||
"created_at": "2014-10-09T03:32:07.158Z",
|
||||
"updated_at": "2019-01-01T22:58:30.846Z",
|
||||
"position": 2,
|
||||
"description": null,
|
||||
"showcase": false,
|
||||
"id": "g4tb35rs9yw7",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"group_id": "1km35smx8p41",
|
||||
"group": false,
|
||||
"only_show_if_degraded": false,
|
||||
"automation_email": "component+g4tb35rs9yw7@notifications.statuspage.io"
|
||||
}
|
||||
],
|
||||
"impact": "minor"
|
||||
},
|
||||
{
|
||||
"name": "Web Analytics Delays",
|
||||
"status": "resolved",
|
||||
"created_at": "2018-09-17T18:05:39.907Z",
|
||||
"updated_at": "2018-09-17T22:53:05.078Z",
|
||||
"monitoring_at": null,
|
||||
"resolved_at": "2018-09-17T22:53:05.057Z",
|
||||
"shortlink": "http://stspg.io/cb208928c",
|
||||
"id": "wqfk9mzs5qt1",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"incident_updates": [
|
||||
{
|
||||
"status": "resolved",
|
||||
"body": "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.",
|
||||
"created_at": "2018-09-17T22:53:05.057Z",
|
||||
"updated_at": "2018-09-17T22:53:05.057Z",
|
||||
"display_at": "2018-09-17T22:53:05.057Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "4c231tkdlpcl",
|
||||
"name": "Cloudflare customer dashboard and APIs - Analytics",
|
||||
"old_status": "degraded_performance",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "93y1w00yqzk4",
|
||||
"incident_id": "wqfk9mzs5qt1",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.",
|
||||
"created_at": "2018-09-17T18:05:40.033Z",
|
||||
"updated_at": "2018-09-17T18:05:40.033Z",
|
||||
"display_at": "2018-09-17T18:05:40.033Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "4c231tkdlpcl",
|
||||
"name": "Cloudflare customer dashboard and APIs - Analytics",
|
||||
"old_status": "operational",
|
||||
"new_status": "degraded_performance"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "362t6lv0vrpk",
|
||||
"incident_id": "wqfk9mzs5qt1",
|
||||
"custom_tweet": null
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"status": "operational",
|
||||
"name": "Analytics",
|
||||
"created_at": "2014-11-13T11:54:10.191Z",
|
||||
"updated_at": "2018-12-31T08:20:52.349Z",
|
||||
"position": 3,
|
||||
"description": "Customer data",
|
||||
"showcase": false,
|
||||
"id": "4c231tkdlpcl",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"group_id": "1km35smx8p41",
|
||||
"group": false,
|
||||
"only_show_if_degraded": false,
|
||||
"automation_email": "component+4c231tkdlpcl@notifications.statuspage.io"
|
||||
}
|
||||
],
|
||||
"impact": "minor"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
output: &StatusPage{
|
||||
Incidents: []Incident{
|
||||
Incident{
|
||||
Name: "Cloudflare API service issues",
|
||||
ID: "q746ybtyb6q0",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has resolved the issue and the service have resumed normal operation.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "We are continuing to investigate this issue.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.",
|
||||
},
|
||||
},
|
||||
},
|
||||
Incident{
|
||||
Name: "Web Analytics Delays",
|
||||
ID: "wqfk9mzs5qt1",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
output, err := parseStatusPage(testCase.input)
|
||||
if testCase.fail {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, testCase.output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArgoTunnelIncident(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input Incident
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
input: Incident{},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "An Argo Tunnel incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an argo tunnel incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an aRgO TuNnEl incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an argotunnel incident"},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "irrelevant"},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{
|
||||
Name: "irrelevant",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "an Argo Tunnel incident"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
},
|
||||
},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{
|
||||
Name: "an Argo Tunnel incident",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
},
|
||||
},
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
actual := isArgoTunnelIncident(testCase.input)
|
||||
assert.Equal(t, testCase.output, actual, "Test case failed: %v", testCase.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentURL(t *testing.T) {
|
||||
incident := Incident{
|
||||
ID: "s6k0dnn5347b",
|
||||
}
|
||||
assert.Equal(t, "https://www.cloudflarestatus.com/incidents/s6k0dnn5347b", incident.URL())
|
||||
}
|
||||
|
||||
func TestNewCachedIncidentLookup(t *testing.T) {
|
||||
c := newCachedIncidentLookup(func() []Incident { return nil })
|
||||
assert.Equal(t, time.Minute, c.ttl)
|
||||
assert.Equal(t, 1, c.cache.Capacity())
|
||||
}
|
||||
|
||||
func TestCachedIncidentLookup(t *testing.T) {
|
||||
expected := []Incident{
|
||||
Incident{
|
||||
Name: "An incident",
|
||||
ID: "incidentID",
|
||||
},
|
||||
}
|
||||
|
||||
var shouldCallUncachedLookup bool
|
||||
c := &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: 50 * time.Millisecond,
|
||||
uncachedLookup: func() []Incident {
|
||||
if !shouldCallUncachedLookup {
|
||||
t.Fatal("uncachedLookup shouldn't have been called")
|
||||
}
|
||||
return expected
|
||||
},
|
||||
}
|
||||
|
||||
shouldCallUncachedLookup = true
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
|
||||
shouldCallUncachedLookup = false
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
shouldCallUncachedLookup = true
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
}
|
||||
|
||||
func TestCachedIncidentLookupDoesntPanic(t *testing.T) {
|
||||
expected := []Incident{
|
||||
Incident{
|
||||
Name: "An incident",
|
||||
ID: "incidentID",
|
||||
},
|
||||
}
|
||||
c := &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: 50 * time.Millisecond,
|
||||
uncachedLookup: func() []Incident { return expected },
|
||||
}
|
||||
c.cache.Set(cacheKey, 42, time.Now().Add(30*time.Minute))
|
||||
actual := c.ActiveIncidents()
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA Warp servers
|
||||
srvService = "warp"
|
||||
srvProto = "tcp"
|
||||
srvName = "cloudflarewarp.com"
|
||||
)
|
||||
|
||||
func ResolveEdgeIPs(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
|
||||
}
|
||||
// HA service discovery lookup
|
||||
_, addrs, err := net.LookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var lookupErr error
|
||||
for _, addr := range addrs {
|
||||
ips, err := ResolveSRVToTCP(addr)
|
||||
if err != nil || len(ips) == 0 {
|
||||
// don't return early, we might be able to resolve other addresses
|
||||
lookupErr = err
|
||||
continue
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
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) {
|
||||
ips, err := net.LookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var result []*net.TCPAddr
|
||||
for len(ipsByService) > 0 {
|
||||
filtered := ipsByService[:0]
|
||||
for _, ips := range ipsByService {
|
||||
if len(ips) == 0 {
|
||||
// sanity check
|
||||
continue
|
||||
}
|
||||
result = append(result, ips[0])
|
||||
if len(ips) > 1 {
|
||||
filtered = append(filtered, ips[1:])
|
||||
}
|
||||
}
|
||||
ipsByService = filtered
|
||||
}
|
||||
return result
|
||||
}
|
||||
+48
-3
@@ -51,7 +51,13 @@ type TunnelMetrics struct {
|
||||
// oldServerLocations stores the last server the tunnel was connected to
|
||||
oldServerLocations map[string]string
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
regSuccess prometheus.Counter
|
||||
regFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
tunnelsHA tunnelsForHA
|
||||
userHostnamesCounts *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newMuxerMetrics() *muxerMetrics {
|
||||
@@ -341,6 +347,40 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
)
|
||||
prometheus.MustRegister(serverLocations)
|
||||
|
||||
rpcFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_rpc_fail",
|
||||
Help: "Count of RPC connection errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
prometheus.MustRegister(rpcFail)
|
||||
|
||||
registerFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_register_fail",
|
||||
Help: "Count of tunnel registration errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
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",
|
||||
Help: "Count of successful tunnel registrations",
|
||||
})
|
||||
prometheus.MustRegister(registerSuccess)
|
||||
|
||||
return &TunnelMetrics{
|
||||
haConnections: haConnections,
|
||||
totalRequests: totalRequests,
|
||||
@@ -355,6 +395,11 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
serverLocations: serverLocations,
|
||||
oldServerLocations: make(map[string]string),
|
||||
muxerMetrics: newMuxerMetrics(),
|
||||
tunnelsHA: NewTunnelsForHA(),
|
||||
regSuccess: registerSuccess,
|
||||
regFail: registerFail,
|
||||
rpcFail: rpcFail,
|
||||
userHostnamesCounts: userHostnamesCounts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +420,7 @@ func (t *TunnelMetrics) incrementRequests(connectionID string) {
|
||||
var concurrentRequests uint64
|
||||
var ok bool
|
||||
if concurrentRequests, ok = t.concurrentRequests[connectionID]; ok {
|
||||
t.concurrentRequests[connectionID] += 1
|
||||
t.concurrentRequests[connectionID]++
|
||||
concurrentRequests++
|
||||
} else {
|
||||
t.concurrentRequests[connectionID] = 1
|
||||
@@ -395,7 +440,7 @@ func (t *TunnelMetrics) incrementRequests(connectionID string) {
|
||||
func (t *TunnelMetrics) decrementConcurrentRequests(connectionID string) {
|
||||
t.concurrentRequestsLock.Lock()
|
||||
if _, ok := t.concurrentRequests[connectionID]; ok {
|
||||
t.concurrentRequests[connectionID] -= 1
|
||||
t.concurrentRequests[connectionID]--
|
||||
}
|
||||
t.concurrentRequestsLock.Unlock()
|
||||
|
||||
|
||||
+42
-21
@@ -1,11 +1,17 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,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 {
|
||||
@@ -47,12 +55,13 @@ func NewSupervisor(config *TunnelConfig) *Supervisor {
|
||||
config: config,
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
logger: config.Logger.WithField("subsystem", "supervisor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) error {
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(ctx, connectedSignal); err != nil {
|
||||
if err := s.initialize(ctx, connectedSignal, u); err != nil {
|
||||
return err
|
||||
}
|
||||
var tunnelsWaiting []int
|
||||
@@ -95,7 +104,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) err
|
||||
case <-backoffTimer:
|
||||
backoffTimer = nil
|
||||
for _, index := range tunnelsWaiting {
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index))
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), u)
|
||||
}
|
||||
tunnelsActive += len(tunnelsWaiting)
|
||||
tunnelsWaiting = nil
|
||||
@@ -119,9 +128,11 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct{}) error {
|
||||
logger := s.config.Logger
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.EdgeAddrs)
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
logger := s.logger
|
||||
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
|
||||
if err != nil {
|
||||
logger.Infof("ResolveEdgeIPs err")
|
||||
return err
|
||||
@@ -134,7 +145,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
s.lastResolve = time.Now()
|
||||
// check entitlement and version too old error before attempting to register more tunnels
|
||||
s.nextUnusedEdgeIP = s.config.HAConnections
|
||||
go s.startFirstTunnel(ctx, connectedSignal)
|
||||
go s.startFirstTunnel(ctx, connectedSignal, u)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-s.tunnelErrors
|
||||
@@ -142,11 +153,12 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
return fmt.Errorf("context was canceled")
|
||||
case tunnelError := <-s.tunnelErrors:
|
||||
return tunnelError.err
|
||||
case <-connectedSignal:
|
||||
case <-connectedSignal.Wait():
|
||||
}
|
||||
// At least one successful connection, so start the rest
|
||||
for i := 1; i < s.config.HAConnections; i++ {
|
||||
go s.startTunnel(ctx, i, make(chan struct{}))
|
||||
ch := signal.New(make(chan struct{}))
|
||||
go s.startTunnel(ctx, i, ch, u)
|
||||
time.Sleep(registrationInterval)
|
||||
}
|
||||
return nil
|
||||
@@ -154,8 +166,8 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
|
||||
// startTunnel starts the first tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors. It will send a signal via connectedSignal if registration succeed
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal chan struct{}) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal)
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: 0, err: err}
|
||||
}()
|
||||
@@ -176,23 +188,23 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal chan
|
||||
default:
|
||||
return
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal)
|
||||
err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
}
|
||||
}
|
||||
|
||||
// startTunnel starts a new tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors.
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal chan struct{}) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal)
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, u uuid.UUID) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal, u)
|
||||
s.tunnelErrors <- tunnelError{index: index, err: err}
|
||||
}
|
||||
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) chan struct{} {
|
||||
signal := make(chan struct{})
|
||||
s.tunnelsConnecting[index] = signal
|
||||
s.nextConnectedSignal = signal
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
|
||||
sig := make(chan struct{})
|
||||
s.tunnelsConnecting[index] = sig
|
||||
s.nextConnectedSignal = sig
|
||||
s.nextConnectedIndex = index
|
||||
return signal
|
||||
return signal.New(sig)
|
||||
}
|
||||
|
||||
func (s *Supervisor) waitForNextTunnel(index int) bool {
|
||||
@@ -210,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
|
||||
@@ -219,7 +240,7 @@ func (s *Supervisor) refreshEdgeIPs() {
|
||||
}
|
||||
s.resolverC = make(chan resolveResult)
|
||||
go func() {
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.EdgeAddrs)
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
+300
-184
@@ -2,6 +2,7 @@ package origin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -10,58 +11,69 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"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"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
_ "github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
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 {
|
||||
EdgeAddrs []string
|
||||
OriginUrl 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
|
||||
ProtocolLogger *log.Logger
|
||||
Logger *log.Logger
|
||||
IsAutoupdated bool
|
||||
GracePeriod time.Duration
|
||||
RunFromTerminal bool
|
||||
NoChunkedEncoding bool
|
||||
WSGI bool
|
||||
CompressionQuality uint64
|
||||
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
|
||||
}
|
||||
|
||||
type dialError struct {
|
||||
@@ -99,11 +111,16 @@ type clientRegisterTunnelError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec) clientRegisterTunnelError {
|
||||
counter.WithLabelValues(cause.Error()).Inc()
|
||||
return clientRegisterTunnelError{cause: cause}
|
||||
}
|
||||
|
||||
func (e clientRegisterTunnelError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP string) *tunnelpogs.RegistrationOptions {
|
||||
func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions {
|
||||
policy := tunnelrpc.ExistingTunnelPolicy_balance
|
||||
if c.HAConnections <= 1 && c.LBPool == "" {
|
||||
policy = tunnelrpc.ExistingTunnelPolicy_disconnect
|
||||
@@ -120,51 +137,38 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
IsAutoupdated: c.IsAutoupdated,
|
||||
RunFromTerminal: c.RunFromTerminal,
|
||||
CompressionQuality: c.CompressionQuality,
|
||||
UUID: uuid.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal chan struct{}) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-shutdownC
|
||||
cancel()
|
||||
}()
|
||||
// If a user specified negative HAConnections, we will treat it as requesting 1 connection
|
||||
if config.HAConnections > 1 {
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal)
|
||||
} else {
|
||||
addrs, err := ResolveEdgeIPs(config.EdgeAddrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal)
|
||||
}
|
||||
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,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectedSignal chan struct{},
|
||||
connectedSignal *signal.Signal,
|
||||
u uuid.UUID,
|
||||
) error {
|
||||
logger := config.Logger
|
||||
connectionLogger := config.Logger.WithField("connectionID", connectionID)
|
||||
config.Metrics.incrementHaConnections()
|
||||
defer config.Metrics.decrementHaConnections()
|
||||
backoff := BackoffHandler{MaxRetries: config.Retries}
|
||||
// Used to close connectedSignal no more than once
|
||||
connectedFuse := h2mux.NewBooleanFuse()
|
||||
go func() {
|
||||
if connectedFuse.Await() {
|
||||
close(connectedSignal)
|
||||
connectedSignal.Notify()
|
||||
}
|
||||
}()
|
||||
// Ensure the above goroutine will terminate if we return without connecting
|
||||
defer connectedFuse.Fuse(false)
|
||||
for {
|
||||
err, recoverable := ServeTunnel(ctx, config, addr, connectionID, connectedFuse, &backoff)
|
||||
err, recoverable := ServeTunnel(ctx, config, connectionLogger, addr, connectionID, connectedFuse, &backoff, u)
|
||||
if recoverable {
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
logger.Infof("Retrying in %s seconds", duration)
|
||||
connectionLogger.Infof("Retrying in %s seconds", duration)
|
||||
backoff.Backoff(ctx)
|
||||
continue
|
||||
}
|
||||
@@ -176,10 +180,12 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
func ServeTunnel(
|
||||
ctx context.Context,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectedFuse *h2mux.BooleanFuse,
|
||||
backoff *BackoffHandler,
|
||||
u uuid.UUID,
|
||||
) (err error, recoverable bool) {
|
||||
// Treat panics as recoverable errors
|
||||
defer func() {
|
||||
@@ -194,7 +200,6 @@ func ServeTunnel(
|
||||
}()
|
||||
|
||||
connectionTag := uint8ToString(connectionID)
|
||||
logger := config.Logger.WithField("connectionID", connectionTag)
|
||||
|
||||
// additional tags to send other than hostname which is set in cloudflared main package
|
||||
tags := make(map[string]string)
|
||||
@@ -203,7 +208,7 @@ func ServeTunnel(
|
||||
// Returns error from parsing the origin URL or handshake errors
|
||||
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr.String(), connectionID)
|
||||
if err != nil {
|
||||
errLog := config.Logger.WithError(err)
|
||||
errLog := logger.WithError(err)
|
||||
switch err.(type) {
|
||||
case dialError:
|
||||
errLog.Error("Unable to dial edge")
|
||||
@@ -219,7 +224,7 @@ func ServeTunnel(
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := RegisterTunnel(serveCtx, handler.muxer, config, connectionID, originLocalIP)
|
||||
err := RegisterTunnel(serveCtx, handler.muxer, config, logger, connectionID, originLocalIP, u)
|
||||
if err == nil {
|
||||
connectedFuse.Fuse(true)
|
||||
backoff.SetGracePeriod()
|
||||
@@ -233,7 +238,7 @@ func ServeTunnel(
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
// UnregisterTunnel blocks until the RPC call returns
|
||||
err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.Logger)
|
||||
err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
|
||||
handler.muxer.Shutdown()
|
||||
return err
|
||||
case <-updateMetricsTickC:
|
||||
@@ -263,6 +268,9 @@ func ServeTunnel(
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error from server side")
|
||||
// Don't send registration error return from server to Sentry. They are
|
||||
// logged on server side
|
||||
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
|
||||
logger.Error(activeIncidentsMsg(incidents))
|
||||
}
|
||||
return castedErr.cause, !castedErr.permanent
|
||||
case clientRegisterTunnelError:
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
|
||||
@@ -290,24 +298,28 @@ func IsRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfig, connectionID uint8, originLocalIP string) error {
|
||||
config.Logger.Debug("initiating RPC stream to register")
|
||||
stream, err := muxer.OpenStream([]h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
func RegisterTunnel(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
uuid uuid.UUID,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to register")
|
||||
stream, err := openStream(ctx, muxer)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
}
|
||||
if !IsRPCStreamResponse(stream.Headers) {
|
||||
// stream response error
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
}
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(config.Logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(config.Logger.WithField("subsystem", "rpc-transport")),
|
||||
tunnelrpc.NewTransportLogger(config.TransportLogger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(config.TransportLogger.WithField("subsystem", "rpc-transport")),
|
||||
)
|
||||
defer conn.Close()
|
||||
ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
|
||||
@@ -320,37 +332,64 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi
|
||||
ctx,
|
||||
config.OriginCert,
|
||||
config.Hostname,
|
||||
config.RegistrationOptions(connectionID, originLocalIP),
|
||||
config.RegistrationOptions(connectionID, originLocalIP, uuid),
|
||||
)
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, config.Logger)
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
|
||||
if err != nil {
|
||||
// RegisterTunnel RPC failure
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.regFail)
|
||||
}
|
||||
for _, logLine := range registration.LogLines {
|
||||
config.Logger.Info(logLine)
|
||||
logger.Info(logLine)
|
||||
}
|
||||
if registration.Err == DuplicateConnectionError {
|
||||
return dupConnRegisterTunnelError{}
|
||||
} else if registration.Err != "" {
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", registration.Err),
|
||||
permanent: registration.PermanentFailure,
|
||||
|
||||
if regErr := processRegisterTunnelError(registration.Err, registration.PermanentFailure, config.Metrics); regErr != nil {
|
||||
return regErr
|
||||
}
|
||||
|
||||
if registration.TunnelID != "" {
|
||||
config.Metrics.tunnelsHA.AddTunnelID(connectionID, registration.TunnelID)
|
||||
logger.Infof("Each HA connection's tunnel IDs: %v", config.Metrics.tunnelsHA.String())
|
||||
}
|
||||
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one)
|
||||
if isTrialTunnel := config.Hostname == ""; isTrialTunnel {
|
||||
if url, err := url.Parse(registration.Url); err == nil {
|
||||
for _, line := range asciiBox(trialZoneMsg(url.String()), 2) {
|
||||
logger.Infoln(line)
|
||||
}
|
||||
} else {
|
||||
logger.Errorln("Failed to connect tunnel, please try again.")
|
||||
return fmt.Errorf("empty URL in response from Cloudflare edge")
|
||||
}
|
||||
}
|
||||
|
||||
config.Logger.Info("Tunnel ID: " + registration.TunnelID)
|
||||
config.Logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
return nil
|
||||
}
|
||||
|
||||
func processRegisterTunnelError(err string, permanentFailure bool, metrics *TunnelMetrics) error {
|
||||
if err == "" {
|
||||
metrics.regSuccess.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
metrics.regFail.WithLabelValues(err).Inc()
|
||||
if err == DuplicateConnectionError {
|
||||
return dupConnRegisterTunnelError{}
|
||||
}
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", err),
|
||||
permanent: permanentFailure,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -359,7 +398,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")),
|
||||
@@ -370,11 +408,21 @@ 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,
|
||||
metrics *TunnelMetrics,
|
||||
logger *log.Logger,
|
||||
logger *log.Entry,
|
||||
) {
|
||||
serverInfoMessage, err := promise.Struct()
|
||||
if err != nil {
|
||||
@@ -390,33 +438,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
|
||||
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 {
|
||||
@@ -427,17 +448,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
|
||||
@@ -454,10 +472,11 @@ func NewTunnelHandler(ctx context.Context,
|
||||
) (*TunnelHandler, string, error) {
|
||||
originURL, err := validation.ValidateUrl(config.OriginUrl)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("Unable to parse origin url %#v", originURL)
|
||||
return nil, "", fmt.Errorf("unable to parse origin URL %#v", originURL)
|
||||
}
|
||||
h := &TunnelHandler{
|
||||
originUrl: originURL,
|
||||
httpHostHeader: config.HTTPHostHeader,
|
||||
httpClient: config.HTTPTransport,
|
||||
tlsConfig: config.ClientTlsConfig,
|
||||
tags: config.Tags,
|
||||
@@ -493,7 +512,7 @@ func NewTunnelHandler(ctx context.Context,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.ProtocolLogger.WithFields(log.Fields{}),
|
||||
Logger: config.TransportLogger.WithFields(log.Fields{}),
|
||||
CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -510,65 +529,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) {
|
||||
@@ -598,25 +655,42 @@ func (h *TunnelHandler) logError(stream *h2mux.MuxedStream, err error) {
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool) {
|
||||
logger := log.NewEntry(h.logger)
|
||||
if cfRay != "" {
|
||||
h.logger.WithField("CF-RAY", cfRay).Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else if lbProbe {
|
||||
h.logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else {
|
||||
h.logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
|
||||
logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
|
||||
}
|
||||
logger.Debugf("Request Headers %+v", req.Header)
|
||||
|
||||
if contentLen := req.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Request Content length unknown")
|
||||
} else {
|
||||
logger.Debugf("Request content length %d", contentLen)
|
||||
}
|
||||
h.logger.Debugf("Request Headers %+v", req.Header)
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
h.logger.WithField("CF-RAY", cfRay).Debugf("%s", r.Status)
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s", r.Status)
|
||||
} else if lbProbe {
|
||||
h.logger.Debugf("Response to Load Balancer health check %s", r.Status)
|
||||
logger.Debugf("Response to Load Balancer health check %s", r.Status)
|
||||
} else {
|
||||
h.logger.Infof("%s", r.Status)
|
||||
logger.Infof("%s", r.Status)
|
||||
}
|
||||
logger.Debugf("Response Headers %+v", r.Header)
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Response content length unknown")
|
||||
} else {
|
||||
logger.Debugf("Response content length %d", contentLen)
|
||||
}
|
||||
h.logger.Debugf("Response Headers %+v", r.Header)
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) UpdateMetrics(connectionID string) {
|
||||
@@ -627,6 +701,48 @@ 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)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func trialZoneMsg(url string) []string {
|
||||
return []string{
|
||||
"Your free tunnel has started! Visit it:",
|
||||
" " + url,
|
||||
}
|
||||
}
|
||||
|
||||
func activeIncidentsMsg(incidents []Incident) string {
|
||||
preamble := "There is an active Cloudflare incident that may be related:"
|
||||
if len(incidents) > 1 {
|
||||
preamble = "There are active Cloudflare incidents that may be related:"
|
||||
}
|
||||
incidentStrings := []string{}
|
||||
for _, incident := range incidents {
|
||||
incidentString := fmt.Sprintf("%s (%s)", incident.Name, incident.URL())
|
||||
incidentStrings = append(incidentStrings, incidentString)
|
||||
}
|
||||
return preamble + " " + strings.Join(incidentStrings, "; ")
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve.
|
||||
type tunnelsForHA struct {
|
||||
sync.Mutex
|
||||
metrics *prometheus.GaugeVec
|
||||
entries map[uint8]string
|
||||
}
|
||||
|
||||
// NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA.
|
||||
func NewTunnelsForHA() tunnelsForHA {
|
||||
metrics := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "tunnel_ids",
|
||||
Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.",
|
||||
},
|
||||
[]string{"tunnel_id", "ha_conn_id"},
|
||||
)
|
||||
prometheus.MustRegister(metrics)
|
||||
|
||||
return tunnelsForHA{
|
||||
metrics: metrics,
|
||||
entries: make(map[uint8]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics.
|
||||
func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
haStr := fmt.Sprintf("%v", haConn)
|
||||
if oldTunnelID, ok := t.entries[haConn]; ok {
|
||||
t.metrics.WithLabelValues(oldTunnelID, haStr).Dec()
|
||||
}
|
||||
t.entries[haConn] = tunnelID
|
||||
t.metrics.WithLabelValues(tunnelID, haStr).Inc()
|
||||
}
|
||||
|
||||
func (t *tunnelsForHA) String() string {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
return fmt.Sprintf("%v", t.entries)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILENAME=$1
|
||||
VERSION=$2
|
||||
TAP_ROOT=$3
|
||||
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-${VERSION}-darwin-amd64.tgz"
|
||||
SHA256=$(sha256sum -b "${FILENAME}" | cut -b1-64)
|
||||
|
||||
cd "${TAP_ROOT}" || exit 1
|
||||
git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/argo-tunnel/'
|
||||
url '${URL}'
|
||||
sha256 '${SHA256}'
|
||||
version '${VERSION}'
|
||||
def install
|
||||
bin.install 'cloudflared'
|
||||
end
|
||||
end
|
||||
EOF
|
||||
git add cloudflared.rb
|
||||
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Argo Tunnel ${VERSION}"
|
||||
git version
|
||||
GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=../github_known_hosts" git push -v origin master
|
||||
@@ -0,0 +1,33 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Signal lets goroutines signal that some event has occurred. Other goroutines can wait for the signal.
|
||||
type Signal struct {
|
||||
ch chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// New wraps a channel and turns it into a signal for a one-time event.
|
||||
func New(ch chan struct{}) *Signal {
|
||||
return &Signal{
|
||||
ch: ch,
|
||||
once: sync.Once{},
|
||||
}
|
||||
}
|
||||
|
||||
// Notify alerts any goroutines waiting on this signal that the event has occurred.
|
||||
// After the first call to Notify(), future calls are no-op.
|
||||
func (s *Signal) Notify() {
|
||||
s.once.Do(func() {
|
||||
close(s.ch)
|
||||
})
|
||||
}
|
||||
|
||||
// Wait returns a channel which will be written to when Notify() is called for the first time.
|
||||
// This channel will never be written to a second time.
|
||||
func (s *Signal) Wait() <-chan struct{} {
|
||||
return s.ch
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiNotifyDoesntCrash(t *testing.T) {
|
||||
sig := New(make(chan struct{}))
|
||||
sig.Notify()
|
||||
sig.Notify()
|
||||
// If code has reached here without crashing, the test has passed.
|
||||
}
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
sig := New(make(chan struct{}))
|
||||
sig.Notify()
|
||||
select {
|
||||
case <-sig.Wait():
|
||||
// Test succeeds
|
||||
return
|
||||
default:
|
||||
// sig.Wait() should have been read from, because sig.Notify() wrote to it.
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3-buster
|
||||
|
||||
RUN wget https://bin.equinox.io/c/VdrWdbjqyF/cloudflared-stable-linux-amd64.deb \
|
||||
&& dpkg -i cloudflared-stable-linux-amd64.deb
|
||||
|
||||
RUN pip install pexpect
|
||||
|
||||
COPY tests.py .
|
||||
COPY ssh /root/.ssh
|
||||
RUN chmod 600 /root/.ssh/id_rsa
|
||||
|
||||
ARG SSH_HOSTNAME
|
||||
RUN bash -c 'sed -i "s/{{hostname}}/${SSH_HOSTNAME}/g" /root/.ssh/authorized_keys_config'
|
||||
RUN bash -c 'sed -i "s/{{hostname}}/${SSH_HOSTNAME}/g" /root/.ssh/short_lived_cert_config'
|
||||
@@ -0,0 +1,23 @@
|
||||
# Cloudflared SSH server smoke tests
|
||||
|
||||
Runs several tests in a docker container against a server that is started out of band of these tests.
|
||||
Cloudflared token also needs to be retrieved out of band.
|
||||
SSH server hostname and user need to be configured in a docker environment file
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
* Build cloudflared:
|
||||
make cloudflared
|
||||
|
||||
* Start server:
|
||||
sudo ./cloudflared tunnel --hostname HOSTNAME --ssh-server
|
||||
|
||||
* Fetch token:
|
||||
./cloudflared access login HOSTNAME
|
||||
|
||||
* Create docker env file:
|
||||
echo "SSH_HOSTNAME=HOSTNAME\nSSH_USER=USERNAME\n" > ssh_server_tests/.env
|
||||
|
||||
* Run tests:
|
||||
make test-ssh-server
|
||||
@@ -0,0 +1,18 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
ssh_test:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- SSH_HOSTNAME=${SSH_HOSTNAME}
|
||||
volumes:
|
||||
- "~/.cloudflared/:/root/.cloudflared"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- AUTHORIZED_KEYS_SSH_CONFIG=/root/.ssh/authorized_keys_config
|
||||
- SHORT_LIVED_CERT_SSH_CONFIG=/root/.ssh/short_lived_cert_config
|
||||
- REMOTE_SCP_FILENAME=scp_test.txt
|
||||
- ROOT_ONLY_TEST_FILE_PATH=~/permission_test.txt
|
||||
entrypoint: "python tests.py"
|
||||
@@ -0,0 +1,5 @@
|
||||
Host *
|
||||
AddressFamily inet
|
||||
|
||||
Host {{hostname}}
|
||||
ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
|
||||
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEAvi26NDQ8cYTTztqPe9ZgF5HR/rIo5FoDgL5NbbZKW6h0txP9Fd8s
|
||||
id9Bgmo+aGkeM327tPVVMQ6UFmdRksOCIDWQDjNLF8b6S+Fu95tvMKSbGreRoR32OvgZKV
|
||||
I6KmOsF4z4GIv9naPplZswtKEUhSSI+/gPdAs9wfwalqZ77e82QJ727bYMeC3lzuoT+KBI
|
||||
dYufJ4OQhLtpHrqhB5sn7s6+oCv/u85GSln5SIC18Hi2t9lW4tgb5tH8P0kEDDWGfPS5ok
|
||||
qGi4kFTvwBXOCS2r4dhi5hRkpP7PqG4np0OCfvK5IRRJ27fCnj0loc+puZJAxnPMbuJr64
|
||||
vwxRx78PM/V0PDUsl0P6aR/vbe0XmF9FGqbWf2Tar1p4r6C9/bMzcDz8seYT8hzLIHP3+R
|
||||
l1hdlsTLm+1EzhaExKId+tjXegKGG4nU24h6qHEnRxLQDMwEsdkfj4E1pVypZJXVyNj99D
|
||||
o84vi0EUnu7R4HmQb/C+Pu7qMDtLT3Zk7O5Mg4LQ+cTz9V0noYEAyG46nAB4U/nJzBnV1J
|
||||
+aAdpioHmUAYhLYlQ9Kiy7LCJi92g9Wqa4wxMKxBbO5ZeH++p2p2lUi/oQNqx/2dLYFmy0
|
||||
wxvJHbZIhAaFbOeCvHg1ucIAQznli2jOr2qoB+yKRRPAp/3NXnZg1v7ce2CkwiAD52wjtC
|
||||
kAAAdILMJUeyzCVHsAAAAHc3NoLXJzYQAAAgEAvi26NDQ8cYTTztqPe9ZgF5HR/rIo5FoD
|
||||
gL5NbbZKW6h0txP9Fd8sid9Bgmo+aGkeM327tPVVMQ6UFmdRksOCIDWQDjNLF8b6S+Fu95
|
||||
tvMKSbGreRoR32OvgZKVI6KmOsF4z4GIv9naPplZswtKEUhSSI+/gPdAs9wfwalqZ77e82
|
||||
QJ727bYMeC3lzuoT+KBIdYufJ4OQhLtpHrqhB5sn7s6+oCv/u85GSln5SIC18Hi2t9lW4t
|
||||
gb5tH8P0kEDDWGfPS5okqGi4kFTvwBXOCS2r4dhi5hRkpP7PqG4np0OCfvK5IRRJ27fCnj
|
||||
0loc+puZJAxnPMbuJr64vwxRx78PM/V0PDUsl0P6aR/vbe0XmF9FGqbWf2Tar1p4r6C9/b
|
||||
MzcDz8seYT8hzLIHP3+Rl1hdlsTLm+1EzhaExKId+tjXegKGG4nU24h6qHEnRxLQDMwEsd
|
||||
kfj4E1pVypZJXVyNj99Do84vi0EUnu7R4HmQb/C+Pu7qMDtLT3Zk7O5Mg4LQ+cTz9V0noY
|
||||
EAyG46nAB4U/nJzBnV1J+aAdpioHmUAYhLYlQ9Kiy7LCJi92g9Wqa4wxMKxBbO5ZeH++p2
|
||||
p2lUi/oQNqx/2dLYFmy0wxvJHbZIhAaFbOeCvHg1ucIAQznli2jOr2qoB+yKRRPAp/3NXn
|
||||
Zg1v7ce2CkwiAD52wjtCkAAAADAQABAAACAQCbnVsyAFQ9J00Rg/HIiUATyTQlzq57O9SF
|
||||
8jH1RiZOHedzLx32WaleH5rBFiJ+2RTnWUjQ57aP77fpJR2wk93UcT+w/vPBPwXsNUjRvx
|
||||
Qan3ZzRCYbyiKDWiNslmYV7X0RwD36CAK8jTVDP7t48h2SXLTiSLaMY+5i3uD6yLu7k/O2
|
||||
qNyw4jgN1rCmwQ8acD0aQec3NAZ7NcbsaBX/3Uutsup0scwOZtlJWZoLY5Z8cKpCgcsAz4
|
||||
j1NHnNZvey7dFgSffj/ktdvf7kBH0w/GnuJ4aNF0Jte70u0kiw5TZYBQVFh74tgUu6a6SJ
|
||||
qUbxIYUL5EJNjxGsDn+phHEemw3aMv0CwZG6Tqaionlna7bLsl9Bg1HTGclczVWx8uqC+M
|
||||
6agLmkhYCHG0rVj8h5smjXAQXtmvIDVYDOlJZZoF9VAOCj6QfmJUH1NAGpCs1HDHbeOxGA
|
||||
OLCh4d3F4rScPqhGdtSt4W13VFIvXn2Qqoz9ufepZsee1SZqpcerxywx2wN9ZAzu+X8lTN
|
||||
i+TA2B3vWpqqucOEsp4JwDN+VMKZqKUGUDWcm/eHSaG6wq0q734LUlgM85TjaIg8QsNtWV
|
||||
giB1nWwsYIuH4rsFNFGEwURYdGBcw6idH0GZ7I4RaIB5F9oOza1d601E0APHYrtnx9yOiK
|
||||
nOtJ+5ZmVZovaDRfu1aQAAAQBU/EFaNUzoVhO04pS2L6BlByt963bOIsSJhdlEzek5AAli
|
||||
eaf1S/PD6xWCc0IGY+GZE0HPbhsKYanjqOpWldcA2T7fzf4oz4vFBfUkPYo/MLSlLCYsDd
|
||||
IH3wBkCssnfR5EkzNgxnOvq646Nl64BMvxwSIXGPktdq9ZALxViwricSRzCFURnh5vLHWU
|
||||
wBzSgAA0UlZ9E64GtAv066+AoZCp83GhTLRC4o0naE2e/K4op4BCFHLrZ8eXmDRK3NJj80
|
||||
Vkn+uhrk+SHmbjIhmS57Pv9p8TWyRvemph/nMUuZGKBUu2X+JQxggck0KigIrXjsmciCsM
|
||||
BIM3mYDDfjYbyVhTAAABAQDkV8O1bWUsAIqk7RU+iDZojN5kaO+zUvj1TafX8QX1sY6pu4
|
||||
Z2cfSEka1532BaehM95bQm7BCPw4cYg56XidmCQTZ9WaWqxVrOo48EKXUtZMZx6nKFOKlq
|
||||
MT2XTMnGT9n7kFCfEjSVkAjuJ9ZTFLOaoXAaVRnxeHQwOKaup5KKP9GSzNIw328U+96s3V
|
||||
WKHeT4pMjHBccgW/qX/tRRidZw5in5uBC9Ew5y3UACFTkNOnhUwVfyUNbBZJ2W36msQ3KD
|
||||
AN7nOrQHqhd3NFyCEy2ovIAKVBacr/VEX6EsRUshIehJzz8EY9f3kXL7WT2QDoz2giPeBJ
|
||||
HJdEpXt43UpszjAAABAQDVNpqNdHUlCs9XnbIvc6ZRrNh79wt65YFfvh/QEuA33KnA6Ri6
|
||||
EgnV5IdUWXS/UFaYcm2udydrBpVIVifSYl3sioHBylpri23BEy38PKwVXvghUtfpN6dWGn
|
||||
NZUG25fQPtIzqi+lo953ZjIj+Adi17AeVv4P4NiLrZeM9lXfWf2pEPOecxXs1IwAf9IiDQ
|
||||
WepAwRLsu42eEnHA+DSJPZUkSbISfM5X345k0g6EVATX/yLL3CsqClPzPtsqjh6rbEfFg3
|
||||
2OfIMcWV77gOlGWGQ+bUHc8kV6xJqV9QVacLWzfLvIqHF0wQMf8WLOVHEzkfiq4VjwhVqr
|
||||
/+FFvljm5nSDAAAAEW1pa2VAQzAyWTUwVEdKR0g4AQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+Lbo0NDxxhNPO2o971mAXkdH+sijkWgOAvk1ttkpbqHS3E/0V3yyJ30GCaj5oaR4zfbu09VUxDpQWZ1GSw4IgNZAOM0sXxvpL4W73m28wpJsat5GhHfY6+BkpUjoqY6wXjPgYi/2do+mVmzC0oRSFJIj7+A90Cz3B/BqWpnvt7zZAnvbttgx4LeXO6hP4oEh1i58ng5CEu2keuqEHmyfuzr6gK/+7zkZKWflIgLXweLa32Vbi2Bvm0fw/SQQMNYZ89LmiSoaLiQVO/AFc4JLavh2GLmFGSk/s+obienQ4J+8rkhFEnbt8KePSWhz6m5kkDGc8xu4mvri/DFHHvw8z9XQ8NSyXQ/ppH+9t7ReYX0UaptZ/ZNqvWnivoL39szNwPPyx5hPyHMsgc/f5GXWF2WxMub7UTOFoTEoh362Nd6AoYbidTbiHqocSdHEtAMzASx2R+PgTWlXKlkldXI2P30Ojzi+LQRSe7tHgeZBv8L4+7uowO0tPdmTs7kyDgtD5xPP1XSehgQDIbjqcAHhT+cnMGdXUn5oB2mKgeZQBiEtiVD0qLLssImL3aD1aprjDEwrEFs7ll4f76nanaVSL+hA2rH/Z0tgWbLTDG8kdtkiEBoVs54K8eDW5wgBDOeWLaM6vaqgH7IpFE8Cn/c1edmDW/tx7YKTCIAPnbCO0KQ== mike@C02Y50TGJGH8
|
||||
@@ -0,0 +1,11 @@
|
||||
Host *
|
||||
AddressFamily inet
|
||||
|
||||
Host {{hostname}}
|
||||
ProxyCommand bash -c '/usr/local/bin/cloudflared access ssh-gen --hostname %h; ssh -F /root/.ssh/short_lived_cert_config -tt %r@cfpipe-{{hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{hostname}}
|
||||
HostName {{hostname}}
|
||||
ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{hostname}}-cf_key
|
||||
CertificateFile ~/.cloudflared/{{hostname}}-cf_key-cert.pub
|
||||
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Cloudflared Integration tests
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
|
||||
from pexpect import pxssh
|
||||
|
||||
|
||||
class TestSSHBase(unittest.TestCase):
|
||||
"""
|
||||
SSH test base class containing constants and helper funcs
|
||||
"""
|
||||
|
||||
HOSTNAME = os.environ["SSH_HOSTNAME"]
|
||||
SSH_USER = os.environ["SSH_USER"]
|
||||
SSH_TARGET = f"{SSH_USER}@{HOSTNAME}"
|
||||
AUTHORIZED_KEYS_SSH_CONFIG = os.environ["AUTHORIZED_KEYS_SSH_CONFIG"]
|
||||
SHORT_LIVED_CERT_SSH_CONFIG = os.environ["SHORT_LIVED_CERT_SSH_CONFIG"]
|
||||
SSH_OPTIONS = {"StrictHostKeyChecking": "no"}
|
||||
|
||||
@classmethod
|
||||
def get_ssh_command(cls, pty=True):
|
||||
"""
|
||||
Return ssh command arg list. If pty is true, a PTY is forced for the session.
|
||||
"""
|
||||
cmd = [
|
||||
"ssh",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-F",
|
||||
cls.AUTHORIZED_KEYS_SSH_CONFIG,
|
||||
cls.SSH_TARGET,
|
||||
]
|
||||
if not pty:
|
||||
cmd += ["-T"]
|
||||
else:
|
||||
cmd += ["-tt"]
|
||||
|
||||
return cmd
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def ssh_session_manager(cls, *args, **kwargs):
|
||||
"""
|
||||
Context manager for interacting with a pxssh session.
|
||||
Disables pty echo on the remote server and ensures session is terminated afterward.
|
||||
"""
|
||||
session = pxssh.pxssh(options=cls.SSH_OPTIONS)
|
||||
|
||||
session.login(
|
||||
cls.HOSTNAME,
|
||||
username=cls.SSH_USER,
|
||||
original_prompt=r"[#@$]",
|
||||
ssh_config=kwargs.get("ssh_config", cls.AUTHORIZED_KEYS_SSH_CONFIG),
|
||||
ssh_tunnels=kwargs.get("ssh_tunnels", {}),
|
||||
)
|
||||
try:
|
||||
session.sendline("stty -echo")
|
||||
session.prompt()
|
||||
yield session
|
||||
finally:
|
||||
session.logout()
|
||||
|
||||
@staticmethod
|
||||
def get_command_output(session, cmd):
|
||||
"""
|
||||
Executes command on remote ssh server and waits for prompt.
|
||||
Returns command output
|
||||
"""
|
||||
session.sendline(cmd)
|
||||
session.prompt()
|
||||
return session.before.decode().strip()
|
||||
|
||||
def exec_command(self, cmd, shell=False):
|
||||
"""
|
||||
Executes command locally. Raises Assertion error for non-zero return code.
|
||||
Returns stdout and stderr
|
||||
"""
|
||||
proc = subprocess.Popen(
|
||||
cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=shell
|
||||
)
|
||||
raw_out, raw_err = proc.communicate()
|
||||
|
||||
out = raw_out.decode()
|
||||
err = raw_err.decode()
|
||||
self.assertEqual(proc.returncode, 0, msg=f"stdout: {out} stderr: {err}")
|
||||
return out.strip(), err.strip()
|
||||
|
||||
|
||||
class TestSSHCommandExec(TestSSHBase):
|
||||
"""
|
||||
Tests inline ssh command exec
|
||||
"""
|
||||
|
||||
# Name of file to be downloaded over SCP on remote server.
|
||||
REMOTE_SCP_FILENAME = os.environ["REMOTE_SCP_FILENAME"]
|
||||
|
||||
@classmethod
|
||||
def get_scp_base_command(cls):
|
||||
return [
|
||||
"scp",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-v",
|
||||
"-F",
|
||||
cls.AUTHORIZED_KEYS_SSH_CONFIG,
|
||||
]
|
||||
|
||||
@unittest.skip(
|
||||
"This creates files on the remote. Should be skipped until server is dockerized."
|
||||
)
|
||||
def test_verbose_scp_sink_mode(self):
|
||||
with tempfile.NamedTemporaryFile() as fl:
|
||||
self.exec_command(
|
||||
self.get_scp_base_command() + [fl.name, f"{self.SSH_TARGET}:"]
|
||||
)
|
||||
|
||||
def test_verbose_scp_source_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
self.exec_command(
|
||||
self.get_scp_base_command()
|
||||
+ [f"{self.SSH_TARGET}:{self.REMOTE_SCP_FILENAME}", tmpdirname]
|
||||
)
|
||||
local_filename = os.path.join(tmpdirname, self.REMOTE_SCP_FILENAME)
|
||||
|
||||
self.assertTrue(os.path.exists(local_filename))
|
||||
self.assertTrue(os.path.getsize(local_filename) > 0)
|
||||
|
||||
def test_pty_command(self):
|
||||
base_cmd = self.get_ssh_command()
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["whoami"])
|
||||
self.assertEqual(out.strip().lower(), self.SSH_USER.lower())
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["tty"])
|
||||
self.assertNotEqual(out, "not a tty")
|
||||
|
||||
def test_non_pty_command(self):
|
||||
base_cmd = self.get_ssh_command(pty=False)
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["whoami"])
|
||||
self.assertEqual(out.strip().lower(), self.SSH_USER.lower())
|
||||
|
||||
out, _ = self.exec_command(base_cmd + ["tty"])
|
||||
self.assertEqual(out, "not a tty")
|
||||
|
||||
|
||||
class TestSSHShell(TestSSHBase):
|
||||
"""
|
||||
Tests interactive SSH shell
|
||||
"""
|
||||
|
||||
# File path to a file on the remote server with root only read privileges.
|
||||
ROOT_ONLY_TEST_FILE_PATH = os.environ["ROOT_ONLY_TEST_FILE_PATH"]
|
||||
|
||||
def test_ssh_pty(self):
|
||||
with self.ssh_session_manager() as session:
|
||||
|
||||
# Test shell launched as correct user
|
||||
username = self.get_command_output(session, "whoami")
|
||||
self.assertEqual(username.lower(), self.SSH_USER.lower())
|
||||
|
||||
# Test USER env variable set
|
||||
user_var = self.get_command_output(session, "echo $USER")
|
||||
self.assertEqual(user_var.lower(), self.SSH_USER.lower())
|
||||
|
||||
# Test HOME env variable set to true user home.
|
||||
home_env = self.get_command_output(session, "echo $HOME")
|
||||
pwd = self.get_command_output(session, "pwd")
|
||||
self.assertEqual(pwd, home_env)
|
||||
|
||||
# Test shell launched in correct user home dir.
|
||||
self.assertIn(username, pwd)
|
||||
|
||||
# Ensure shell launched with correct user's permissions and privs.
|
||||
# Cant read root owned 0700 files.
|
||||
output = self.get_command_output(
|
||||
session, f"cat {self.ROOT_ONLY_TEST_FILE_PATH}"
|
||||
)
|
||||
self.assertIn("Permission denied", output)
|
||||
|
||||
def test_short_lived_cert_auth(self):
|
||||
with self.ssh_session_manager(
|
||||
ssh_config=self.SHORT_LIVED_CERT_SSH_CONFIG
|
||||
) as session:
|
||||
username = self.get_command_output(session, "whoami")
|
||||
self.assertEqual(username.lower(), self.SSH_USER.lower())
|
||||
|
||||
|
||||
unittest.main()
|
||||
@@ -0,0 +1,194 @@
|
||||
package sshgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
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) {
|
||||
pub, err := generateKeyPair(fullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return SignCert(token, string(pub))
|
||||
}
|
||||
|
||||
func SignCert(token, pubKey string) (string, error) {
|
||||
if token == "" {
|
||||
return "", errors.New("invalid token")
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse JWT")
|
||||
}
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to retrieve JWT claims")
|
||||
}
|
||||
|
||||
issuer, _, err := claims.StringClaim("iss")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to retrieve JWT iss")
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(&signPayload{
|
||||
PublicKey: pubKey,
|
||||
JWT: token,
|
||||
Issuer: issuer,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signPayload")
|
||||
}
|
||||
var res *http.Response
|
||||
if mockRequest != nil {
|
||||
res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
} else {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
res, err = client.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to send request")
|
||||
}
|
||||
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 "", errors.Wrap(err, "failed to decode HTTP response")
|
||||
}
|
||||
return signRes.Certificate, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
//empty manager implements the Manager but does nothing (for testing and to disable logging unless the logs are set)
|
||||
type emptyManager struct {
|
||||
}
|
||||
|
||||
type emptyWriteCloser struct {
|
||||
}
|
||||
|
||||
// NewEmptyManager creates a new instance of a log empty log manager that does nothing
|
||||
func NewEmptyManager() Manager {
|
||||
return &emptyManager{}
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
func (m *emptyManager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return &emptyWriteCloser{}, nil
|
||||
}
|
||||
|
||||
// emptyWriteCloser
|
||||
|
||||
func (w *emptyWriteCloser) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *emptyWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Generate go.capnp.out with:
|
||||
# capnp compile -o- go.capnp > go.capnp.out
|
||||
# Must run inside this directory to preserve paths.
|
||||
|
||||
@0xd12a1c51fedd6c88;
|
||||
|
||||
annotation package(file) :Text;
|
||||
annotation import(file) :Text;
|
||||
annotation doc(struct, field, enum) :Text;
|
||||
annotation tag(enumerant) :Text;
|
||||
annotation notag(enumerant) :Void;
|
||||
annotation customtype(field) :Text;
|
||||
annotation name(struct, field, union, enum, enumerant, interface, method, param, annotation, const, group) :Text;
|
||||
|
||||
$package("capnp");
|
||||
@@ -0,0 +1,167 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
logTimeFormat = "2006-01-02T15-04-05.000"
|
||||
megabyte = 1024 * 1024
|
||||
defaultFileSizeLimit = 100 * megabyte
|
||||
)
|
||||
|
||||
// Logger will buffer and write events to disk
|
||||
type Logger struct {
|
||||
sync.Mutex
|
||||
filename string
|
||||
file *os.File
|
||||
writeBuffer *bufio.Writer
|
||||
logger *logrus.Logger
|
||||
flushInterval time.Duration
|
||||
maxFileSize int64
|
||||
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, flushInterval time.Duration, maxFileSize int64) (*Logger, error) {
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger can't be nil")
|
||||
}
|
||||
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,
|
||||
flushInterval: flushInterval,
|
||||
maxFileSize: maxFileSize,
|
||||
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(l.flushInterval)
|
||||
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() >= l.maxFileSize
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const logFileName = "test-logger.log"
|
||||
|
||||
func createLogger(t *testing.T) *Logger {
|
||||
os.Remove(logFileName)
|
||||
l := logrus.New()
|
||||
logger, err := NewLogger(logFileName, l, time.Millisecond, 1024)
|
||||
if err != nil {
|
||||
t.Fatal("couldn't create the logger!", err)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
// AUTH-2115 TODO: fix this test
|
||||
//func TestWrite(t *testing.T) {
|
||||
// testStr := "hi"
|
||||
// logger := createLogger(t)
|
||||
// defer func() {
|
||||
// logger.Close()
|
||||
// os.Remove(logFileName)
|
||||
// }()
|
||||
//
|
||||
// logger.Write([]byte(testStr))
|
||||
// time.Sleep(2 * time.Millisecond)
|
||||
// data, err := ioutil.ReadFile(logFileName)
|
||||
// if err != nil {
|
||||
// t.Fatal("couldn't read the log file!", err)
|
||||
// }
|
||||
// checkStr := string(data)
|
||||
// if checkStr != testStr {
|
||||
// t.Fatal("file data doesn't match!")
|
||||
// }
|
||||
//}
|
||||
|
||||
func TestFilenameRotation(t *testing.T) {
|
||||
newName := rotationName("dir/bob/acoolloggername.log")
|
||||
|
||||
dir := filepath.Dir(newName)
|
||||
if dir != "dir/bob" {
|
||||
t.Fatal("rotation name doesn't respect the directory filepath:", newName)
|
||||
}
|
||||
|
||||
filename := filepath.Base(newName)
|
||||
if !strings.HasPrefix(filename, "acoolloggername") {
|
||||
t.Fatal("rotation filename is wrong:", filename)
|
||||
}
|
||||
|
||||
ext := filepath.Ext(newName)
|
||||
if ext != ".log" {
|
||||
t.Fatal("rotation file extension is wrong:", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotation(t *testing.T) {
|
||||
logger := createLogger(t)
|
||||
|
||||
for i := 0; i < 2000; i++ {
|
||||
logger.Write([]byte("a string for testing rotation\n"))
|
||||
}
|
||||
logger.Close()
|
||||
|
||||
count := 0
|
||||
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(info.Name(), "test-logger") {
|
||||
log.Println("deleting: ", path)
|
||||
os.Remove(path)
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if count < 2 {
|
||||
t.Fatal("rotation didn't roll files:", count)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package sshlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Manager be managing logs bruh
|
||||
type Manager interface {
|
||||
NewLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
NewSessionLogger(string, *logrus.Logger) (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
baseDirectory string
|
||||
}
|
||||
|
||||
// New creates a new instance of a log manager
|
||||
func New(baseDirectory string) Manager {
|
||||
return &manager{
|
||||
baseDirectory: baseDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) NewLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return NewLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
|
||||
func (m *manager) NewSessionLogger(name string, logger *logrus.Logger) (io.WriteCloser, error) {
|
||||
return NewSessionLogger(filepath.Join(m.baseDirectory, name), logger, time.Second, defaultFileSizeLimit)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Go = import "go.capnp";
|
||||
@0x8f43375162194466;
|
||||
$Go.package("sshlog");
|
||||
$Go.import("github.com/cloudflare/cloudflared/sshlog");
|
||||
|
||||
struct SessionLog {
|
||||
timestamp @0 :Text;
|
||||
content @1 :Data;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user