mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c76283a2b4 | |||
| ae374c0463 | |||
| 42246f986c | |||
| 80f387214c | |||
| a368fbbe9b | |||
| 32df01a9da | |||
| 6dcf3a4cbc | |||
| 96f11de7ab | |||
| d03469b6d3 | |||
| 0cf6ce9aeb | |||
| e8f55cc911 | |||
| 35cee13175 | |||
| 5376df5439 | |||
| db9b6541d0 | |||
| 5bd4028ea7 | |||
| 1b2a96f96b | |||
| d50fee4fa0 | |||
| 6b3e2b020b | |||
| 6624a24040 | |||
| 7b81cf8aa6 | |||
| 26f5f80811 | |||
| 29f4650e25 | |||
| a14aa0322c | |||
| 8f9bbcb9a0 | |||
| afc2cd38e1 | |||
| a5f67091bf | |||
| 464bb53049 | |||
| 6488843ac4 | |||
| 52ab2c8227 | |||
| 269351bbea | |||
| a83b6a2155 | |||
| a60c0273f5 | |||
| d6c2c4ee4a | |||
| 5b1bea7892 | |||
| 54b386188a | |||
| 386b02355a | |||
| 203b939614 | |||
| e729dfc51e | |||
| d5139d3882 | |||
| e31ff3a70f | |||
| dfe61fda88 | |||
| 053b2c17f1 | |||
| 7367827a11 | |||
| 7d7bdffde5 | |||
| 7e31b77646 | |||
| 87102a2646 | |||
| 789ca6f6f4 | |||
| cc2a1d1204 | |||
| 6aa48d2eb2 | |||
| 8b43454024 | |||
| 5e7ca14412 | |||
| b499c0fdba | |||
| bbf31377c2 | |||
| 8f4fd70783 | |||
| 92736b2677 | |||
| 379cb16efe | |||
| 43babbc2f9 | |||
| dfd1ca5fb5 | |||
| f51712bef9 | |||
| b0d31a0ef3 | |||
| dd614881b6 | |||
| 23e12cf5a3 | |||
| 9605f00c77 | |||
| 871c3a194f | |||
| 8ee1faf317 | |||
| c5bacf4d95 | |||
| 1ba5abfdb3 | |||
| 7173da9359 | |||
| 0676923d24 | |||
| ca7fbf43da | |||
| 6ea9b5c3ff | |||
| 1d96bccc04 | |||
| c5af83af66 | |||
| 759cd019be | |||
| 13bf65ce4e | |||
| 3a2e12818f | |||
| e14ec1a1fb | |||
| 3a9a0a0d75 | |||
| 068b148e05 | |||
| e5335b6c1b | |||
| fa841fc89a | |||
| e9a8c9850f | |||
| a52f47c9a9 | |||
| ad9559c66a | |||
| 7133eceb9b | |||
| e0c2758b64 | |||
| 5464408ea7 | |||
| 802e538c42 | |||
| 79065514ff | |||
| 1f6a330098 |
+25
-9
@@ -1,12 +1,28 @@
|
||||
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
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.13.3 as builder
|
||||
ENV GO111MODULE=on
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
|
||||
FROM gcr.io/distroless/base
|
||||
COPY --from=builder /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
|
||||
# ---
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian10:nonroot
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
# run as non-privileged user
|
||||
USER nonroot
|
||||
|
||||
# command / entrypoint of container
|
||||
ENTRYPOINT ["cloudflared", "--no-autoupdate"]
|
||||
CMD ["version"]
|
||||
|
||||
Generated
-682
@@ -1,682 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1a200e7e73293b75eb8e5c93d023b5472663432da0b663e1532624fcfede9ca8"
|
||||
name = "github.com/anmitsu/go-shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "648efa622239a2f6ff949fed78ee37b48d499ba4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1: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]]
|
||||
digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2"
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
packages = ["."]
|
||||
pruneopts = "T"
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[projects]]
|
||||
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",
|
||||
"coremain",
|
||||
"pb",
|
||||
"plugin",
|
||||
"plugin/cache",
|
||||
"plugin/cache/freq",
|
||||
"plugin/etcd/msg",
|
||||
"plugin/metrics",
|
||||
"plugin/metrics/vars",
|
||||
"plugin/pkg/cache",
|
||||
"plugin/pkg/dnstest",
|
||||
"plugin/pkg/dnsutil",
|
||||
"plugin/pkg/doh",
|
||||
"plugin/pkg/edns",
|
||||
"plugin/pkg/fuzz",
|
||||
"plugin/pkg/log",
|
||||
"plugin/pkg/nonwriter",
|
||||
"plugin/pkg/rcode",
|
||||
"plugin/pkg/response",
|
||||
"plugin/pkg/trace",
|
||||
"plugin/pkg/uniq",
|
||||
"plugin/pkg/watch",
|
||||
"plugin/test",
|
||||
"request",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "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"]
|
||||
pruneopts = "UT"
|
||||
revision = "e64a0ec8b42a61e2a9801dc1d0abe539dea79197"
|
||||
version = "v20"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
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]]
|
||||
digest = "1:d4268b2a09b1f736633577c4ac93f2a5356c73742fff5344e2451aeec60a7ad0"
|
||||
name = "github.com/equinox-io/equinox"
|
||||
packages = [
|
||||
".",
|
||||
"internal/go-update",
|
||||
"internal/go-update/internal/binarydist",
|
||||
"internal/go-update/internal/osext",
|
||||
"internal/osext",
|
||||
"proto",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "5205c98a6c11dc72747ce12fff6cd620a99fde05"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
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]]
|
||||
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",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
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]]
|
||||
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",
|
||||
"scram",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "3427c32cb71afc948325f299f040e53c1dd78979"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4a29eeb25603debe8f2098a9902c4d3851034cf70d33be428826e86e8c30a1b0"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "98ec13f34aabf44cc914c65a1cfb7b9bc815aef1"
|
||||
version = "v0.1.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d62282425ffb75047679d7e2c3b980eea7f82c05ef5fb9142ee617ebac6e7432"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
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]]
|
||||
digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71"
|
||||
name = "github.com/mholt/caddy"
|
||||
packages = [
|
||||
".",
|
||||
"caddyfile",
|
||||
"telemetry",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2b4b4b2e5544c2a11a486c1b631357aa2ddf766e50c1b2483cf809da2c511234"
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "73601d4aed9d844322611759d7f3619110b7c88e"
|
||||
version = "v1.1.8"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11e62d6050198055e6cd87ed57e5d8c669e84f839c16e16f192374d913d1a70d"
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"ext",
|
||||
"log",
|
||||
]
|
||||
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",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||
version = "v0.9.0-pre1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
pruneopts = "UT"
|
||||
revision = "14fe0d1b01d4d5fc031dd4bec1823bd3ebbe8016"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f119e3205d3a1f0f19dbd7038eb37528e2c6f0933269dc344e305951fb87d632"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "287d3e634a1e550c9e463dd7e5a75a422c614505"
|
||||
version = "v0.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a210815b437763623ecca8eb91e6a0bf4f2d6773c5a6c9aec0e28f19e5fd6deb"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/fs",
|
||||
"internal/util",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "499c85531f756d1129edd26485a5f73871eeb308"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1a23fdd843129ef761ffe7651bc5fe7c5b09fbe933e92783ab06cc11c37b7b37"
|
||||
name = "github.com/rifflock/lfshook"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b9218ef580f59a2e72dad1aa33d660150445d05a"
|
||||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6"
|
||||
name = "github.com/stretchr/testify"
|
||||
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",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"nacl/box",
|
||||
"nacl/secretbox",
|
||||
"poly1305",
|
||||
"salsa20/salsa",
|
||||
"ssh",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "34f69633bfdcf9db92f698f8487115767eebef81"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ded5b1b804f079c993f887e0da394aa54cdf8d2a6a31e62c66d94ebe6f75bc2a"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"context",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"internal/timeseries",
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"trace",
|
||||
"websocket",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "72f939374954d0a1b2a1e27dcda950e2724dd5c1"
|
||||
|
||||
[[projects]]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:583a0c80f5e3a9343d33aea4aead1e1afcc0043db66fdf961ddd1fe8cd3a4faf"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
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",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"serviceconfig",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f6d0f9ee430895e87ef1ceb5ac8f39725bafceef"
|
||||
version = "v1.24.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "altsrc-parse-durations"
|
||||
digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787"
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
packages = [
|
||||
".",
|
||||
"altsrc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d604b6ffeee878fbf084fd2761466b6649989cee"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:59f10c1537d2199d9115d946927fe31165959a95190849c82ff11e05803528b0"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "f221b8435cfb71e54062f6c6e99e9ade30b124d5"
|
||||
version = "v2.2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642"
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
packages = [
|
||||
".",
|
||||
"encoding/text",
|
||||
"internal/fulfiller",
|
||||
"internal/nodemap",
|
||||
"internal/packed",
|
||||
"internal/queue",
|
||||
"internal/schema",
|
||||
"internal/strquote",
|
||||
"pogs",
|
||||
"rpc",
|
||||
"rpc/internal/refcount",
|
||||
"schemas",
|
||||
"server",
|
||||
"std/capnp/rpc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "v2.17.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/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
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[prune.project]]
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
unused-packages = false
|
||||
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/facebookgo/grace"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/getsentry/raven-go"
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "=0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "=0.9.0-pre1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "=1.4.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "=1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
branch = "master" # master required by github.com/miekg/dns
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/sync"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
branch = "altsrc-parse-durations"
|
||||
|
||||
[[constraint]]
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "=2.17.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coredns/coredns"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/dns"
|
||||
version = "=1.1.8"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/mholt/caddy"
|
||||
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"
|
||||
@@ -29,7 +29,7 @@ clean:
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: container
|
||||
container:
|
||||
@@ -37,7 +37,7 @@ container:
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
go test -v -race $(VERSION_FLAGS) ./...
|
||||
go test -v -mod=vendor -race $(VERSION_FLAGS) ./...
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
@@ -82,6 +82,6 @@ tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
go vet -mod=vendor ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype
|
||||
go-sumtype $$(go list ./...)
|
||||
go-sumtype $$(go list -mod=vendor ./...)
|
||||
|
||||
@@ -1,3 +1,102 @@
|
||||
2020.3.1
|
||||
- 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1
|
||||
|
||||
2020.3.0
|
||||
- 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias
|
||||
- 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values.
|
||||
- 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel
|
||||
- 2020-03-13 TUN-2756: Set connection digest after reconnect.
|
||||
- 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address
|
||||
- 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable
|
||||
- 2020-03-19 TUN-2820: Serialized headers for Websockets
|
||||
- 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent
|
||||
- 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred.
|
||||
- 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly
|
||||
- 2020-03-02 TUN-2779: update sample HTML pages
|
||||
- 2020-03-04 TUN-2785: Use reconnect token by default
|
||||
- 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs
|
||||
- 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest
|
||||
- 2020-03-06 TUN-2761: Use the new header management functions in cloudflared
|
||||
- 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection
|
||||
- 2020-02-26 TUN-2767: Test for large headers
|
||||
- 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177)
|
||||
- 2020-02-28 TUN-2776: Add header serialization feature in cloudflared
|
||||
- 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns
|
||||
|
||||
2020.2.1
|
||||
- 2020-02-20 TUN-2745: Rename existing header management functions
|
||||
- 2020-02-21 TUN-2746: Add the new header management functions
|
||||
- 2020-02-25 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161)
|
||||
- 2020-02-25 Tweak HTTP host header. Fixes #107 (#168)
|
||||
- 2020-02-25 TUN-2765: Add list of features to tunnelrpc
|
||||
- 2020-02-19 TUN-2725: Specify in code that --edge is for internal testing only
|
||||
- 2020-02-19 TUN-2703: Muxer.Serve terminates when its context is Done
|
||||
- 2020-02-09 TUN-2717: Function to serialize/deserialize HTTP headers
|
||||
- 2020-02-05 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP.
|
||||
|
||||
2020.2.0
|
||||
- 2020-01-30 TUN-2651: Fix panic in h2mux reader when a stream error is encountered
|
||||
- 2020-01-27 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens"
|
||||
- 2020-01-28 TUN-2693: Metrics for ReconnectTunnel
|
||||
- 2020-01-28 TUN-2696: Add unknown registerRPCName
|
||||
- 2020-01-28 TUN-2699: Metrics for Authenticate RPCs
|
||||
- 2020-01-28 TUN-2690: cloudflared reconnect uses wrong context
|
||||
- 2020-01-29 TUN-2707: Inconsistent cardinality in tunnel error metrics
|
||||
- 2020-01-13 TUN-2645: Turn on reconnect tokens
|
||||
- 2019-12-23 TUN-2646: Make --edge flag work again for local development
|
||||
|
||||
2019.12.0
|
||||
- 2019-12-11 TUN-2631: only notify that activeStreamMap is closed if ignoreNewStreams=true
|
||||
- 2019-12-17 bug(cloudflared): Set the MaxIdleConnsPerHost of http.Transport to proxy-keepalive-connections (#155)
|
||||
- 2019-12-17 refactor(docker): optimize Dockerfile (#126)
|
||||
- 2019-12-19 Fix timer scheduling for systemd update service (#159)
|
||||
- 2019-12-13 TUN-2637: Manage edge IPs in a region-aware manner
|
||||
- 2019-12-03 bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154)
|
||||
- 2019-12-03 TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel
|
||||
- 2019-12-04 TUN-2555: origin/supervisor.go calls Authenticate
|
||||
- 2019-12-06 TUN-2554: cloudflared calls ReconnectTunnel
|
||||
- 2019-11-20 TUN-2575: Constructors + simpler conversions for AuthOutcome
|
||||
- 2019-11-22 Fix Docker build failure (#149)
|
||||
- 2019-11-21 TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration
|
||||
- 2019-11-22 TUN-2582: EventDigest field in tunnelrpc
|
||||
- 2019-11-22 Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade * The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value.
|
||||
- 2019-11-25 TUN-2591: ReconnectTunnel now sends EventDigest
|
||||
- 2019-11-21 TUN-2606: add DialEdge helpers
|
||||
- 2019-11-21 TUN-2607: add RPC stream helpers
|
||||
|
||||
2019.11.3
|
||||
- 2019-11-20 TUN-2562: Update Cloudflare Origin CA RSA root
|
||||
|
||||
2019.11.2
|
||||
- 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse
|
||||
- 2019-11-18 TUN-2563: Exposes config_version metrics
|
||||
|
||||
2019.11.1
|
||||
- 2019-11-12 Add db-connect, a SQL over HTTPS server
|
||||
- 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server
|
||||
- 2019-11-13 TUN-2178: public API to create new h2mux.MuxedStreamRequest
|
||||
- 2019-11-13 TUN-2490: respect original representation of HTTP request path
|
||||
- 2019-11-18 TUN-2547: TunnelRPC definitions for Authenticate flow
|
||||
- 2019-11-18 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow
|
||||
- 2019-11-05 TUN-2506: Expose active streams metrics
|
||||
|
||||
2019.11.0
|
||||
- 2019-11-04 TUN-2502: Switch to go modules
|
||||
- 2019-11-04 TUN-2500: Don't send client registration errors to Sentry
|
||||
- 2019-11-04 TUN-2489: Delete stream from activestreammap when read and write are both closed
|
||||
- 2019-11-05 TUN-2505: Terminate stream on receipt of RST_STREAM; MuxedStream.CloseWrite() should terminate the MuxedStream.Write() loop
|
||||
- 2019-10-30 TUN-2451: Log inavlid path
|
||||
- 2019-10-22 TUN-2425: Enable cloudflared to serve multiple Hello World servers by having each of them create its own ServeMux
|
||||
- 2019-10-22 AUTH-2173: Prepends access login url with scheme if one doesnt exist
|
||||
- 2019-10-23 TUN-2460: Configure according to the ClientConfig recevied from a successful Connect
|
||||
- 2019-10-23 AUTH-2177: Reads and writes error streams
|
||||
|
||||
2019.10.4
|
||||
- 2019-10-21 TUN-2450: Remove Brew publishing formula
|
||||
|
||||
2019.10.3
|
||||
- 2019-10-18 Fix #129: Excessive memory usage streaming large files (#142)
|
||||
|
||||
2019.10.2
|
||||
- 2019-10-17 AUTH-2167: Adds CLI option for host key directory
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
// A Pool must not be copied after first use.
|
||||
// https://golang.org/pkg/sync/#Pool
|
||||
buffers sync.Pool
|
||||
}
|
||||
|
||||
func NewPool(bufferSize int) *Pool {
|
||||
return &Pool{
|
||||
buffers: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, bufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Get() []byte {
|
||||
return p.buffers.Get().([]byte)
|
||||
}
|
||||
|
||||
func (p *Pool) Put(buf []byte) {
|
||||
p.buffers.Put(buf)
|
||||
}
|
||||
+15
-12
@@ -1,5 +1,5 @@
|
||||
pinned_go: &pinned_go go=1.12.7-1
|
||||
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
build:
|
||||
@@ -8,7 +8,6 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
@@ -20,7 +19,6 @@ stretch: &stretch
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
@@ -30,7 +28,6 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make release
|
||||
@@ -41,7 +38,6 @@ stretch: &stretch
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
@@ -52,7 +48,6 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
@@ -62,7 +57,6 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
@@ -73,7 +67,6 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
@@ -84,12 +77,22 @@ stretch: &stretch
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- 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"
|
||||
# cd to a non-module directory: https://github.com/golang/go/issues/24250
|
||||
- (cd / && go get github.com/BurntSushi/go-sumtype)
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- make test
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
test_dbconnect:
|
||||
compose:
|
||||
up-args:
|
||||
- --renew-anon-volumes
|
||||
- --abort-on-container-exit
|
||||
- --exit-code-from=cloudflared
|
||||
files:
|
||||
- dbconnect_tests/dbconnect.yaml
|
||||
|
||||
@@ -23,7 +23,7 @@ func ssh(c *cli.Context) error {
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh")
|
||||
}
|
||||
originURL := "https://" + hostname
|
||||
originURL := ensureURLScheme(hostname)
|
||||
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
|
||||
@@ -119,7 +119,7 @@ func Commands() []*cli.Command {
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: ssh,
|
||||
Aliases: []string{"rdp"},
|
||||
Aliases: []string{"rdp", "tcp"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
@@ -191,7 +191,8 @@ func login(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
logger := log.CreateLogger()
|
||||
args := c.Args()
|
||||
appURL, err := url.Parse(args.First())
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
@@ -211,6 +212,16 @@ func login(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureURLScheme prepends a URL with https:// if it doesnt have a scheme. http:// URLs will not be converted.
|
||||
func ensureURLScheme(url string) string {
|
||||
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = fmt.Sprintf("https://%s", url)
|
||||
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
@@ -294,7 +305,7 @@ func sshGen(c *cli.Context) error {
|
||||
return cli.ShowCommandHelp(c, "ssh-gen")
|
||||
}
|
||||
|
||||
originURL, err := url.Parse("https://" + hostname)
|
||||
originURL, err := url.Parse(ensureURLScheme(hostname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package access
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_ensureURLScheme(t *testing.T) {
|
||||
type args struct {
|
||||
url string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
|
||||
{"http scheme", args{"http://test"}, "https://test"},
|
||||
{"https scheme", args{"https://test"}, "https://test"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ensureURLScheme(tt.args.url); got != tt.want {
|
||||
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then s
|
||||
Description=Update Argo Tunnel
|
||||
|
||||
[Timer]
|
||||
OnUnitActiveSec=1d
|
||||
OnCalendar=daily
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
@@ -26,15 +26,15 @@ var logger = log.CreateLogger()
|
||||
type lock struct {
|
||||
lockFilePath string
|
||||
backoff *origin.BackoffHandler
|
||||
sigHandler *signalHandler
|
||||
sigHandler *signalHandler
|
||||
}
|
||||
|
||||
type signalHandler struct {
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
sigChannel chan os.Signal
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
func (s *signalHandler) register(handler func()){
|
||||
func (s *signalHandler) register(handler func()) {
|
||||
s.sigChannel = make(chan os.Signal, 1)
|
||||
signal.Notify(s.sigChannel, s.signals...)
|
||||
go func(s *signalHandler) {
|
||||
@@ -59,8 +59,8 @@ func newLock(path string) *lock {
|
||||
return &lock{
|
||||
lockFilePath: lockPath,
|
||||
backoff: &origin.BackoffHandler{MaxRetries: 7},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
sigHandler: &signalHandler{
|
||||
signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,8 @@ func newLock(path string) *lock {
|
||||
func (l *lock) Acquire() error {
|
||||
// Intercept SIGINT and SIGTERM to release lock before exiting
|
||||
l.sigHandler.register(func() {
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
l.deleteLockFile()
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
// Check for a path.lock file
|
||||
|
||||
@@ -45,7 +45,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", requestURL, resourceName)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, open it yourself and visit the URL above.\n", requestURL)
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, please visit the URL above directly in your browser.\n", requestURL)
|
||||
}
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
+105
-44
@@ -1,24 +1,25 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/trace"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/awsuploader"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
@@ -138,43 +138,7 @@ func Commands() []*cli.Command {
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: false,
|
||||
},
|
||||
{
|
||||
Name: "db",
|
||||
Action: func(c *cli.Context) error {
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
|
||||
fmt.Printf("\nSQL Database Password: ")
|
||||
pass, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
go sqlgateway.StartProxy(c, logger, string(pass))
|
||||
|
||||
raven.CapturePanic(func() { err = tunnel(c) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
Before: Before,
|
||||
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "db",
|
||||
Value: true,
|
||||
Usage: "Enable the SQL Gateway Proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: "",
|
||||
Usage: "Database connection string: db://user:pass",
|
||||
},
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
dbConnectCmd(),
|
||||
}
|
||||
|
||||
var subcommands []*cli.Command
|
||||
@@ -436,10 +400,16 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
return err
|
||||
}
|
||||
|
||||
reconnectCh := make(chan struct{}, 1)
|
||||
if c.IsSet("stdin-control") {
|
||||
logger.Warn("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
|
||||
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh)
|
||||
}()
|
||||
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
|
||||
@@ -633,6 +603,8 @@ func hostnameFromURI(uri string) string {
|
||||
return addPortIfMissing(u, 22)
|
||||
case "rdp":
|
||||
return addPortIfMissing(u, 3389)
|
||||
case "tcp":
|
||||
return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -644,6 +616,60 @@ func addPortIfMissing(uri *url.URL, port int) string {
|
||||
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
}
|
||||
|
||||
func dbConnectCmd() *cli.Command {
|
||||
cmd := dbconnect.Cmd()
|
||||
|
||||
// Append the tunnel commands so users can customize the daemon settings.
|
||||
cmd.Flags = appendFlags(Flags(), cmd.Flags...)
|
||||
|
||||
// Override before to run tunnel validation before dbconnect validation.
|
||||
cmd.Before = func(c *cli.Context) error {
|
||||
err := Before(c)
|
||||
if err == nil {
|
||||
err = dbconnect.CmdBefore(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Override action to setup the Proxy, then if successful, start the tunnel daemon.
|
||||
cmd.Action = func(c *cli.Context) error {
|
||||
err := dbconnect.CmdAction(c)
|
||||
if err == nil {
|
||||
err = tunnel(c)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// appendFlags will append extra flags to a slice of flags.
|
||||
//
|
||||
// The cli package will panic if two flags exist with the same name,
|
||||
// so if extraFlags contains a flag that was already defined, modify the
|
||||
// original flags to use the extra version.
|
||||
func appendFlags(flags []cli.Flag, extraFlags ...cli.Flag) []cli.Flag {
|
||||
for _, extra := range extraFlags {
|
||||
var found bool
|
||||
|
||||
// Check if an extra flag overrides an existing flag.
|
||||
for i, flag := range flags {
|
||||
if reflect.DeepEqual(extra.Names(), flag.Names()) {
|
||||
flags[i] = extra
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Append the extra flag if it has nothing to override.
|
||||
if !found {
|
||||
flags = append(flags, extra)
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -673,7 +699,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "edge",
|
||||
Usage: "Address of the Cloudflare tunnel server.",
|
||||
Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.",
|
||||
EnvVars: []string{"TUNNEL_EDGE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
@@ -960,6 +986,19 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_INTENT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-reconnect-token",
|
||||
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
|
||||
Value: true,
|
||||
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-quick-reconnects",
|
||||
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
|
||||
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "dial-edge-timeout",
|
||||
Usage: "Maximum wait time to set up a connection with the edge",
|
||||
@@ -1027,7 +1066,29 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Usage: "Absolute path of directory to save SSH host keys in",
|
||||
EnvVars: []string{"HOST_KEY_PATH"},
|
||||
Hidden: true,
|
||||
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "stdin-control",
|
||||
Usage: "Control the process using commands sent through stdin",
|
||||
EnvVars: []string{"STDIN-CONTROL"},
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stdinControl(reconnectCh chan struct{}) {
|
||||
for {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
command := scanner.Text()
|
||||
|
||||
switch command {
|
||||
case "reconnect":
|
||||
reconnectCh <- struct{}{}
|
||||
default:
|
||||
logger.Warn("Unknown command: ", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -197,17 +197,21 @@ func prepareTunnelConfig(
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialContext := (&net.Dialer{
|
||||
dialer := &net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext
|
||||
}
|
||||
if c.Bool("proxy-no-happy-eyeballs") {
|
||||
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
|
||||
}
|
||||
dialContext := dialer.DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
@@ -233,7 +237,6 @@ func prepareTunnelConfig(
|
||||
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)
|
||||
@@ -272,16 +275,18 @@ func prepareTunnelConfig(
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
TransportLogger: transportLogger,
|
||||
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
|
||||
UseReconnectToken: c.Bool("use-reconnect-token"),
|
||||
UseQuickReconnects: c.Bool("use-quick-reconnects"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (connection.EdgeServiceDiscoverer, error) {
|
||||
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (*edgediscovery.Edge, error) {
|
||||
// If --edge is specfied, resolve edge server addresses
|
||||
if len(c.StringSlice("edge")) > 0 {
|
||||
return connection.NewEdgeHostnameResolver(c.StringSlice("edge"))
|
||||
return edgediscovery.StaticEdge(logger, c.StringSlice("edge"))
|
||||
}
|
||||
// Otherwise lookup edge server addresses through service discovery
|
||||
return connection.NewEdgeAddrResolver(logger)
|
||||
return edgediscovery.ResolveEdge(logger)
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package sqlgateway
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/elgs/gosqljson"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Connection Connection `json:"connection"`
|
||||
Command string `json:"command"`
|
||||
Params []interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
SSLMode string `json:"sslmode"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Columns []string `json:"columns"`
|
||||
Rows [][]string `json:"rows"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Context *cli.Context
|
||||
Router *mux.Router
|
||||
Token string
|
||||
User string
|
||||
Password string
|
||||
Driver string
|
||||
Database string
|
||||
Logger *logrus.Logger
|
||||
}
|
||||
|
||||
func StartProxy(c *cli.Context, logger *logrus.Logger, password string) error {
|
||||
proxy := NewProxy(c, logger, password)
|
||||
|
||||
logger.Infof("Starting SQL Gateway Proxy on port %s", strings.Split(c.String("url"), ":")[1])
|
||||
|
||||
err := http.ListenAndServe(":"+strings.Split(c.String("url"), ":")[1], proxy.Router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randID(n int, c *cli.Context) string {
|
||||
charBytes := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = charBytes[rand.Intn(len(charBytes))]
|
||||
}
|
||||
return fmt.Sprintf("%s&%s", c.String("hostname"), b)
|
||||
}
|
||||
|
||||
// db://user@dbname
|
||||
func parseInfo(input string) (string, string, string) {
|
||||
p1 := strings.Split(input, "://")
|
||||
p2 := strings.Split(p1[1], "@")
|
||||
return p1[0], p2[0], p2[1]
|
||||
}
|
||||
|
||||
func NewProxy(c *cli.Context, logger *logrus.Logger, pass string) *Proxy {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
driver, user, dbname := parseInfo(c.String("address"))
|
||||
proxy := Proxy{
|
||||
Context: c,
|
||||
Router: mux.NewRouter(),
|
||||
Token: randID(64, c),
|
||||
Logger: logger,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Database: dbname,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf(`
|
||||
|
||||
--------------------
|
||||
SQL Gateway Proxy
|
||||
Token: %s
|
||||
--------------------
|
||||
|
||||
`, proxy.Token))
|
||||
|
||||
proxy.Router.HandleFunc("/", proxy.proxyRequest).Methods("POST")
|
||||
return &proxy
|
||||
}
|
||||
|
||||
func (proxy *Proxy) proxyRequest(rw http.ResponseWriter, req *http.Request) {
|
||||
var message Message
|
||||
response := Response{}
|
||||
|
||||
err := json.NewDecoder(req.Body).Decode(&message)
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if message.Connection.Token != proxy.Token {
|
||||
proxy.Logger.Error("Invalid token")
|
||||
http.Error(rw, "400 - Invalid token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", proxy.User, proxy.Password, proxy.Database, message.Connection.SSLMode)
|
||||
|
||||
db, err := sql.Open(proxy.Driver, connStr)
|
||||
defer db.Close()
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
proxy.Logger.Info("Forwarding SQL: ", message.Command)
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
headers, data, err := gosqljson.QueryDbToArray(db, "lower", message.Command, message.Params...)
|
||||
|
||||
if err != nil {
|
||||
proxy.Logger.Error(err)
|
||||
http.Error(rw, fmt.Sprintf("400 - %s", err.Error()), http.StatusBadRequest)
|
||||
return
|
||||
|
||||
} else {
|
||||
response = Response{headers, data, ""}
|
||||
}
|
||||
}
|
||||
json.NewEncoder(rw).Encode(response)
|
||||
}
|
||||
+12
-36
@@ -5,35 +5,27 @@ import (
|
||||
"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"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
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
|
||||
id uuid.UUID
|
||||
muxer *h2mux.Muxer
|
||||
addr *net.TCPAddr
|
||||
isLongLived bool
|
||||
longLivedID int
|
||||
}
|
||||
|
||||
func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) {
|
||||
func newConnection(muxer *h2mux.Muxer, addr *net.TCPAddr) (*Connection, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,6 +33,7 @@ func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error)
|
||||
return &Connection{
|
||||
id: id,
|
||||
muxer: muxer,
|
||||
addr: addr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -50,32 +43,15 @@ func (c *Connection) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// 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)
|
||||
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (tunnelpogs.ConnectResult, error) {
|
||||
tsClient, err := NewRPCClient(ctx, c.muxer, logger.WithField("rpc", "connect"), openStreamTimeout)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot create new RPC connection")
|
||||
}
|
||||
defer rpcConn.Close()
|
||||
|
||||
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)}
|
||||
|
||||
defer tsClient.Close()
|
||||
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,54 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// DialEdge makes a TLS connection to a Cloudflare edge node
|
||||
func DialEdge(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
tlsConfig *tls.Config,
|
||||
edgeTCPAddr *net.TCPAddr,
|
||||
) (net.Conn, error) {
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
|
||||
defer dialCancel()
|
||||
|
||||
dialer := net.Dialer{}
|
||||
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String())
|
||||
if err != nil {
|
||||
return nil, newDialError(err, "DialContext error")
|
||||
}
|
||||
tlsEdgeConn := tls.Client(edgeConn, tlsConfig)
|
||||
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
if err = tlsEdgeConn.Handshake(); err != nil {
|
||||
return nil, newDialError(err, "TLS handshake with edge error")
|
||||
}
|
||||
// clear the deadline on the conn; h2mux has its own timeouts
|
||||
tlsEdgeConn.SetDeadline(time.Time{})
|
||||
return tlsEdgeConn, nil
|
||||
}
|
||||
|
||||
// DialError is an error returned from DialEdge
|
||||
type DialError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func newDialError(err error, message string) error {
|
||||
return DialError{cause: errors.Wrap(err, message)}
|
||||
}
|
||||
|
||||
func (e DialError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e DialError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
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,63 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"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{
|
||||
[]*net.TCPAddr{
|
||||
&net.TCPAddr{Port: 1},
|
||||
&net.TCPAddr{Port: 2},
|
||||
&net.TCPAddr{Port: 3},
|
||||
&net.TCPAddr{Port: 4},
|
||||
},
|
||||
[]*net.TCPAddr{
|
||||
&net.TCPAddr{Port: 10},
|
||||
&net.TCPAddr{Port: 12},
|
||||
&net.TCPAddr{Port: 13},
|
||||
},
|
||||
[]*net.TCPAddr{
|
||||
&net.TCPAddr{Port: 21},
|
||||
&net.TCPAddr{Port: 22},
|
||||
&net.TCPAddr{Port: 23},
|
||||
&net.TCPAddr{Port: 24},
|
||||
&net.TCPAddr{Port: 25},
|
||||
},
|
||||
})
|
||||
assert.EqualValues(t, []*net.TCPAddr{
|
||||
&net.TCPAddr{Port: 1},
|
||||
&net.TCPAddr{Port: 10},
|
||||
&net.TCPAddr{Port: 21},
|
||||
&net.TCPAddr{Port: 2},
|
||||
&net.TCPAddr{Port: 12},
|
||||
&net.TCPAddr{Port: 22},
|
||||
&net.TCPAddr{Port: 3},
|
||||
&net.TCPAddr{Port: 13},
|
||||
&net.TCPAddr{Port: 23},
|
||||
&net.TCPAddr{Port: 4},
|
||||
&net.TCPAddr{Port: 24},
|
||||
&net.TCPAddr{Port: 25},
|
||||
}, result)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package connection
|
||||
|
||||
const (
|
||||
FeatureSerializedHeaders = "serialized_headers"
|
||||
)
|
||||
|
||||
var SupportedFeatures = []string{
|
||||
FeatureSerializedHeaders,
|
||||
}
|
||||
+62
-54
@@ -4,59 +4,77 @@ 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/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
defaultRetryAfter = time.Second * 5
|
||||
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
|
||||
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
|
||||
defaultRetryAfter = time.Second * 5
|
||||
packageNamespace = "connection"
|
||||
edgeManagerSubsystem = "edgemanager"
|
||||
)
|
||||
|
||||
// EdgeManager manages connections with the edge
|
||||
type EdgeManager struct {
|
||||
// streamHandler handles stream opened by the edge
|
||||
streamHandler h2mux.MuxedStreamHandler
|
||||
streamHandler *streamhandler.StreamHandler
|
||||
// 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
|
||||
serviceDiscoverer *edgediscovery.Edge
|
||||
// state is attributes of ConnectionManager that can change during runtime.
|
||||
state *edgeManagerState
|
||||
|
||||
logger *logrus.Entry
|
||||
|
||||
metrics *metrics
|
||||
}
|
||||
|
||||
// EdgeConnectionManagerConfigurable is the configurable attributes of a EdgeConnectionManager
|
||||
type metrics struct {
|
||||
// activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams
|
||||
activeStreams prometheus.Gauge
|
||||
}
|
||||
|
||||
func newMetrics(namespace, subsystem string) *metrics {
|
||||
return &metrics{
|
||||
activeStreams: h2mux.NewActiveStreamsMetrics(namespace, subsystem),
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeManagerConfigurable is the configurable attributes of a EdgeConnectionManager
|
||||
type EdgeManagerConfigurable struct {
|
||||
TunnelHostnames []h2mux.TunnelHostname
|
||||
*pogs.EdgeConnectionConfig
|
||||
*tunnelpogs.EdgeConnectionConfig
|
||||
}
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
CloudflaredID uuid.UUID
|
||||
Tags []pogs.Tag
|
||||
Tags []tunnelpogs.Tag
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
IntentLabel string
|
||||
}
|
||||
|
||||
func NewEdgeManager(
|
||||
streamHandler h2mux.MuxedStreamHandler,
|
||||
streamHandler *streamhandler.StreamHandler,
|
||||
edgeConnMgrConfigurable *EdgeManagerConfigurable,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer EdgeServiceDiscoverer,
|
||||
serviceDiscoverer *edgediscovery.Edge,
|
||||
cloudflaredConfig *CloudflaredConfig,
|
||||
logger *logrus.Logger,
|
||||
) *EdgeManager {
|
||||
@@ -67,33 +85,36 @@ func NewEdgeManager(
|
||||
serviceDiscoverer: serviceDiscoverer,
|
||||
state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential),
|
||||
logger: logger.WithField("subsystem", "connectionManager"),
|
||||
metrics: newMetrics(packageNamespace, edgeManagerSubsystem),
|
||||
}
|
||||
}
|
||||
|
||||
func (em *EdgeManager) Run(ctx context.Context) error {
|
||||
defer em.shutdown()
|
||||
|
||||
resolveEdgeIPTicker := time.Tick(resolveEdgeAddrTTL)
|
||||
// Currently, declarative tunnels don't have any concept of a stable connection
|
||||
// Each edge connection is transient and when it dies, it is replaced by a different one,
|
||||
// not restarted.
|
||||
// So in the future we should really change this so that n connections are stored individually
|
||||
connIndex := 0
|
||||
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 := em.newConnection(ctx, connIndex); connErr != nil {
|
||||
if !connErr.ShouldRetry {
|
||||
em.logger.WithError(connErr).Error(em.noRetryMessage())
|
||||
return connErr
|
||||
}
|
||||
em.logger.WithError(connErr).Error("cannot create new connection")
|
||||
} else {
|
||||
connIndex++
|
||||
}
|
||||
} else if em.state.shouldReduceConnection() {
|
||||
if err := em.closeConnection(ctx); err != nil {
|
||||
@@ -108,13 +129,16 @@ func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurab
|
||||
em.state.updateConfigurable(newConfigurable)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError {
|
||||
edgeIP := em.serviceDiscoverer.Addr()
|
||||
edgeConn, err := em.dialEdge(ctx, edgeIP)
|
||||
func (em *EdgeManager) newConnection(ctx context.Context, index int) *tunnelpogs.ConnectError {
|
||||
edgeTCPAddr, err := em.serviceDiscoverer.GetAddr(index)
|
||||
if err != nil {
|
||||
return retryConnection(fmt.Sprintf("edge address discovery error: %v", err))
|
||||
}
|
||||
configurable := em.state.getConfigurable()
|
||||
edgeConn, err := DialEdge(ctx, configurable.Timeout, em.tlsConfig, edgeTCPAddr)
|
||||
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{
|
||||
@@ -124,19 +148,19 @@ func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError {
|
||||
HeartbeatInterval: configurable.HeartbeatInterval,
|
||||
MaxHeartbeats: configurable.MaxFailedHeartbeats,
|
||||
Logger: em.logger.WithField("subsystem", "muxer"),
|
||||
})
|
||||
}, em.metrics.activeStreams)
|
||||
if err != nil {
|
||||
retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err))
|
||||
}
|
||||
|
||||
h2muxConn, err := newConnection(muxer, edgeIP)
|
||||
h2muxConn, err := newConnection(muxer, edgeTCPAddr)
|
||||
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{
|
||||
connResult, err := h2muxConn.Connect(ctx, &tunnelpogs.ConnectParameters{
|
||||
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
|
||||
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
|
||||
NumPreviousAttempts: 0,
|
||||
@@ -155,6 +179,10 @@ func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError {
|
||||
|
||||
em.state.newConnection(h2muxConn)
|
||||
em.logger.Infof("connected to %s", connResult.ConnectedTo())
|
||||
|
||||
if connResult.ClientConfig() != nil {
|
||||
em.streamHandler.UseConfiguration(ctx, connResult.ClientConfig())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -164,6 +192,7 @@ func (em *EdgeManager) closeConnection(ctx context.Context) error {
|
||||
return fmt.Errorf("no connection to close")
|
||||
}
|
||||
conn.Shutdown()
|
||||
// teardown will be handled by EdgeManager.serveConn in another goroutine
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -171,28 +200,7 @@ 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
|
||||
em.serviceDiscoverer.GiveBack(conn.addr)
|
||||
}
|
||||
|
||||
func (em *EdgeManager) noRetryMessage() string {
|
||||
@@ -221,14 +229,14 @@ func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCr
|
||||
}
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs uint8) bool {
|
||||
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs int) bool {
|
||||
ems.RLock()
|
||||
defer ems.RUnlock()
|
||||
expectedHAConns := ems.configurable.NumHAConnections
|
||||
expectedHAConns := int(ems.configurable.NumHAConnections)
|
||||
if availableEdgeAddrs < expectedHAConns {
|
||||
expectedHAConns = availableEdgeAddrs
|
||||
}
|
||||
return uint8(len(ems.conns)) < expectedHAConns
|
||||
return len(ems.conns) < expectedHAConns
|
||||
}
|
||||
|
||||
func (ems *edgeManagerState) shouldReduceConnection() bool {
|
||||
@@ -285,8 +293,8 @@ func (ems *edgeManagerState) getUserCredential() []byte {
|
||||
return ems.userCredential
|
||||
}
|
||||
|
||||
func retryConnection(cause string) *pogs.ConnectError {
|
||||
return &pogs.ConnectError{
|
||||
func retryConnection(cause string) *tunnelpogs.ConnectError {
|
||||
return &tunnelpogs.ConnectError{
|
||||
Cause: cause,
|
||||
RetryAfter: defaultRetryAfter,
|
||||
ShouldRetry: true,
|
||||
|
||||
+15
-15
@@ -1,16 +1,19 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"net"
|
||||
"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"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -42,22 +45,19 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type mockStreamHandler struct {
|
||||
}
|
||||
|
||||
func (msh *mockStreamHandler) ServeStream(*h2mux.MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockEdgeManager() *EdgeManager {
|
||||
newConfigChan := make(chan<- *pogs.ClientConfig)
|
||||
useConfigResultChan := make(<-chan *pogs.UseConfigurationResult)
|
||||
logger := logrus.New()
|
||||
edge := edgediscovery.MockEdge(logger, []*net.TCPAddr{})
|
||||
return NewEdgeManager(
|
||||
&mockStreamHandler{},
|
||||
streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger),
|
||||
configurable,
|
||||
[]byte{},
|
||||
nil,
|
||||
&mockEdgeServiceDiscoverer{},
|
||||
edge,
|
||||
cloudflaredConfig,
|
||||
logrus.New(),
|
||||
logger,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
numAddrs int
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
}
|
||||
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
|
||||
}
|
||||
|
||||
var _ quick.Generator = mockAddrs{}
|
||||
|
||||
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
port := uint16(rand.Intn(math.MaxUint16))
|
||||
numRegions := uint8(1 + rand.Intn(10))
|
||||
numAddrsPerRegion := uint8(1 + rand.Intn(32))
|
||||
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
|
||||
return reflect.ValueOf(result)
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupSRV that will return the SRV
|
||||
// records from mockAddrs.
|
||||
func mockNetLookupSRV(
|
||||
m mockAddrs,
|
||||
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
var addrs []*net.SRV
|
||||
for k := range m.addrMap {
|
||||
addr := k
|
||||
addrs = append(addrs, &addr)
|
||||
// We can't just do
|
||||
// addrs = append(addrs, &k)
|
||||
// `k` will be reused by subsequent loop iterations,
|
||||
// so all the copies of `&k` would point to the same location.
|
||||
}
|
||||
return func(_, _, _ string) (string, []*net.SRV, error) {
|
||||
return "", addrs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupIP that translates the SRV records
|
||||
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
|
||||
func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("No IPs for %v", host)
|
||||
}
|
||||
}
|
||||
|
||||
type mockEdgeServiceDiscoverer struct {
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Addr() (*net.TCPAddr, error) {
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 63102,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) AnyAddr() (*net.TCPAddr, error) {
|
||||
return &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 63102,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) ReplaceAddr(addr *net.TCPAddr) {}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) MarkAddrBad(addr *net.TCPAddr) {}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (mr *mockEdgeServiceDiscoverer) Refresh() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// NewRPCClient creates and returns a new RPC client, which will communicate
|
||||
// using a stream on the given muxer
|
||||
func NewRPCClient(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
logger *logrus.Entry,
|
||||
openStreamTimeout time.Duration,
|
||||
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer openStreamCancel()
|
||||
stream, err := muxer.OpenRPCStream(openStreamCtx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !isRPCStreamResponse(stream.Headers) {
|
||||
stream.Close()
|
||||
err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers)
|
||||
return
|
||||
}
|
||||
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(logger),
|
||||
)
|
||||
client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func isRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
return len(headers) == 1 &&
|
||||
headers[0].Name == ":status" &&
|
||||
headers[0].Value == "200"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Client is an interface to talk to any database.
|
||||
//
|
||||
// Currently, the only implementation is SQLClient, but its structure
|
||||
// should be designed to handle a MongoClient or RedisClient in the future.
|
||||
type Client interface {
|
||||
Ping(context.Context) error
|
||||
Submit(context.Context, *Command) (interface{}, error)
|
||||
}
|
||||
|
||||
// NewClient creates a database client based on its URL scheme.
|
||||
func NewClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
return NewSQLClient(ctx, originURL)
|
||||
}
|
||||
|
||||
// Command is a standard, non-vendor format for submitting database commands.
|
||||
//
|
||||
// When determining the scope of this struct, refer to the following litmus test:
|
||||
// Could this (roughly) conform to SQL, Document-based, and Key-value command formats?
|
||||
type Command struct {
|
||||
Statement string `json:"statement"`
|
||||
Arguments Arguments `json:"arguments,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Isolation string `json:"isolation,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Command: non empty statement (both in length and logic),
|
||||
// lowercase mode and isolation, non-zero timeout, and valid Arguments.
|
||||
func (cmd *Command) Validate() error {
|
||||
if cmd.Statement == "" {
|
||||
return fmt.Errorf("cannot provide an empty statement")
|
||||
}
|
||||
|
||||
if strings.Map(func(char rune) rune {
|
||||
if char == ';' || unicode.IsSpace(char) {
|
||||
return -1
|
||||
}
|
||||
return char
|
||||
}, cmd.Statement) == "" {
|
||||
return fmt.Errorf("cannot provide a statement with no logic: '%s'", cmd.Statement)
|
||||
}
|
||||
|
||||
cmd.Mode = strings.ToLower(cmd.Mode)
|
||||
cmd.Isolation = strings.ToLower(cmd.Isolation)
|
||||
|
||||
if cmd.Timeout.Nanoseconds() <= 0 {
|
||||
cmd.Timeout = 24 * time.Hour
|
||||
}
|
||||
|
||||
return cmd.Arguments.Validate()
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into a Command, which is also validated.
|
||||
func (cmd *Command) UnmarshalJSON(data []byte) error {
|
||||
// Alias is required to avoid infinite recursion from the default UnmarshalJSON.
|
||||
type Alias Command
|
||||
alias := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(cmd),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(data, &alias)
|
||||
if err == nil {
|
||||
err = cmd.Validate()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Arguments is a wrapper for either map-based or array-based Command arguments.
|
||||
//
|
||||
// Each field is mutually-exclusive and some Client implementations may not
|
||||
// support both fields (eg. MySQL does not accept named arguments).
|
||||
type Arguments struct {
|
||||
Named map[string]interface{}
|
||||
Positional []interface{}
|
||||
}
|
||||
|
||||
// Validate enforces the contract of Arguments: non nil, mutually exclusive, and no empty or reserved keys.
|
||||
func (args *Arguments) Validate() error {
|
||||
if args.Named == nil {
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
if args.Positional == nil {
|
||||
args.Positional = []interface{}{}
|
||||
}
|
||||
|
||||
if len(args.Named) > 0 && len(args.Positional) > 0 {
|
||||
return fmt.Errorf("both named and positional arguments cannot be specified: %+v and %+v", args.Named, args.Positional)
|
||||
}
|
||||
|
||||
for key := range args.Named {
|
||||
if key == "" {
|
||||
return fmt.Errorf("named arguments cannot contain an empty key: %+v", args.Named)
|
||||
}
|
||||
if !utf8.ValidString(key) {
|
||||
return fmt.Errorf("named argument does not conform to UTF-8 encoding: %s", key)
|
||||
}
|
||||
if strings.HasPrefix(key, "_") {
|
||||
return fmt.Errorf("named argument cannot start with a reserved keyword '_': %s", key)
|
||||
}
|
||||
if unicode.IsNumber([]rune(key)[0]) {
|
||||
return fmt.Errorf("named argument cannot start with a number: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a byte representation of JSON into Arguments, which is also validated.
|
||||
func (args *Arguments) UnmarshalJSON(data []byte) error {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
named, ok := obj.(map[string]interface{})
|
||||
if ok {
|
||||
args.Named = named
|
||||
} else {
|
||||
positional, ok := obj.([]interface{})
|
||||
if ok {
|
||||
args.Positional = positional
|
||||
} else {
|
||||
return fmt.Errorf("arguments must either be an object {\"0\":\"val\"} or an array [\"val\"]: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return args.Validate()
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommandValidateEmpty(t *testing.T) {
|
||||
stmts := []string{
|
||||
"",
|
||||
";",
|
||||
" \n\t",
|
||||
";\n;\t;",
|
||||
}
|
||||
|
||||
for _, stmt := range stmts {
|
||||
cmd := Command{Statement: stmt}
|
||||
|
||||
assert.Error(t, cmd.Validate(), stmt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"",
|
||||
"query",
|
||||
"ExEc",
|
||||
"PREPARE",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
cmd := Command{Statement: "Ok", Mode: mode}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), mode)
|
||||
assert.Equal(t, strings.ToLower(mode), cmd.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateIsolation(t *testing.T) {
|
||||
isos := []string{
|
||||
"",
|
||||
"default",
|
||||
"read_committed",
|
||||
"SNAPshot",
|
||||
}
|
||||
|
||||
for _, iso := range isos {
|
||||
cmd := Command{Statement: "Ok", Isolation: iso}
|
||||
|
||||
assert.NoError(t, cmd.Validate(), iso)
|
||||
assert.Equal(t, strings.ToLower(iso), cmd.Isolation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandValidateTimeout(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Timeout: 0}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.NotZero(t, cmd.Timeout)
|
||||
|
||||
cmd = Command{Statement: "Ok", Timeout: 1 * time.Second}
|
||||
|
||||
assert.NoError(t, cmd.Validate())
|
||||
assert.Equal(t, 1*time.Second, cmd.Timeout)
|
||||
}
|
||||
|
||||
func TestCommandValidateArguments(t *testing.T) {
|
||||
cmd := Command{Statement: "Ok", Arguments: Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
}}
|
||||
|
||||
assert.Error(t, cmd.Validate())
|
||||
}
|
||||
|
||||
func TestCommandUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{\"statement\":\"Ok\"}",
|
||||
"{\"statement\":\"Ok\",\"arguments\":[0, 3.14, \"apple\"],\"mode\":\"query\"}",
|
||||
"{\"statement\":\"Ok\",\"isolation\":\"read_uncommitted\",\"timeout\":1000}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"{}",
|
||||
"{\"argument\":{\"key\":\"val\"}}",
|
||||
"{\"statement\":[\"Ok\"]}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var cmd Command
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &cmd), str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateNotNil(t *testing.T) {
|
||||
args := Arguments{}
|
||||
|
||||
assert.NoError(t, args.Validate())
|
||||
assert.NotNil(t, args.Named)
|
||||
assert.NotNil(t, args.Positional)
|
||||
}
|
||||
|
||||
func TestArgumentsValidateMutuallyExclusive(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{},
|
||||
Arguments{Named: map[string]interface{}{"key": "val"}},
|
||||
Arguments{Positional: []interface{}{"val"}},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.NoError(t, arg.Validate())
|
||||
assert.False(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
|
||||
args = []Arguments{
|
||||
Arguments{
|
||||
Named: map[string]interface{}{"key": "val"},
|
||||
Positional: []interface{}{"val"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
assert.Error(t, arg.Validate())
|
||||
assert.True(t, len(arg.Named) > 0 && len(arg.Positional) > 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsValidateKeys(t *testing.T) {
|
||||
keys := []string{
|
||||
"",
|
||||
"_",
|
||||
"_key",
|
||||
"1",
|
||||
"1key",
|
||||
"\xf0\x28\x8c\xbc", // non-utf8
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
args := Arguments{Named: map[string]interface{}{key: "val"}}
|
||||
|
||||
assert.Error(t, args.Validate(), key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsUnmarshalJSON(t *testing.T) {
|
||||
strs := []string{
|
||||
"{}",
|
||||
"{\"key\":\"val\"}",
|
||||
"{\"key\":[1, 3.14, {\"key\":\"val\"}]}",
|
||||
"[]",
|
||||
"[\"key\",\"val\"]",
|
||||
"[{}]",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.NoError(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
|
||||
strs = []string{
|
||||
"",
|
||||
"\"",
|
||||
"1",
|
||||
"\"key\"",
|
||||
"{\"key\",\"val\"}",
|
||||
}
|
||||
|
||||
for _, str := range strs {
|
||||
var args Arguments
|
||||
assert.Error(t, json.Unmarshal([]byte(str), &args), str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
// Cmd is the entrypoint command for dbconnect.
|
||||
//
|
||||
// The tunnel package is responsible for appending this to tunnel.Commands().
|
||||
func Cmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Category: "Database Connect (ALPHA)",
|
||||
Name: "db-connect",
|
||||
Usage: "Access your SQL database from Cloudflare Workers or the browser",
|
||||
ArgsUsage: " ",
|
||||
Description: `
|
||||
Creates a connection between your database and the Cloudflare edge.
|
||||
Now you can execute SQL commands anywhere you can send HTTPS requests.
|
||||
|
||||
Connect your database with any of the following commands, you can also try the "playground" without a database:
|
||||
|
||||
cloudflared db-connect --hostname sql.mysite.com --url postgres://user:pass@localhost?sslmode=disable \
|
||||
--auth-domain mysite.cloudflareaccess.com --application-aud my-access-policy-tag
|
||||
cloudflared db-connect --hostname sql-dev.mysite.com --url mysql://localhost --insecure
|
||||
cloudflared db-connect --playground
|
||||
|
||||
Requests should be authenticated using Cloudflare Access, learn more about how to enable it here:
|
||||
|
||||
https://developers.cloudflare.com/access/service-auth/service-token/
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "url",
|
||||
Usage: "URL to the database (eg. postgres://user:pass@localhost?sslmode=disable)",
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Hostname to accept commands over HTTPS (eg. sql.mysite.com)",
|
||||
EnvVars: []string{"TUNNEL_HOSTNAME"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "auth-domain",
|
||||
Usage: "Cloudflare Access authentication domain for your account (eg. mysite.cloudflareaccess.com)",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_AUTH_DOMAIN"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "application-aud",
|
||||
Usage: "Cloudflare Access application \"AUD\" to verify JWTs from requests",
|
||||
EnvVars: []string{"TUNNEL_ACCESS_APPLICATION_AUD"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Disable authentication, the database will be open to the Internet",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_ACCESS_INSECURE"},
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "playground",
|
||||
Usage: "Run a temporary, in-memory SQLite3 database for testing",
|
||||
Value: false,
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "debug", // Make it more verbose than the tunnel default 'info'.
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: true,
|
||||
}),
|
||||
},
|
||||
Before: CmdBefore,
|
||||
Action: CmdAction,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CmdBefore runs some validation checks before running the command.
|
||||
func CmdBefore(c *cli.Context) error {
|
||||
// Show the help text is no flags are specified.
|
||||
if c.NumFlags() == 0 {
|
||||
return cli.ShowSubcommandHelp(c)
|
||||
}
|
||||
|
||||
// Hello-world and playground are synonymous with each other,
|
||||
// unset hello-world to prevent tunnel from initializing the hello package.
|
||||
if c.IsSet("hello-world") {
|
||||
c.Set("playground", "true")
|
||||
c.Set("hello-world", "false")
|
||||
}
|
||||
|
||||
// Unix-socket database urls are supported, but the logic is the same as url.
|
||||
if c.IsSet("unix-socket") {
|
||||
c.Set("url", c.String("unix-socket"))
|
||||
c.Set("unix-socket", "")
|
||||
}
|
||||
|
||||
// When playground mode is enabled, run with an in-memory database.
|
||||
if c.IsSet("playground") {
|
||||
c.Set("url", "sqlite3::memory:?cache=shared")
|
||||
c.Set("insecure", strconv.FormatBool(!c.IsSet("auth-domain") && !c.IsSet("application-aud")))
|
||||
}
|
||||
|
||||
// At this point, insecure configurations are valid.
|
||||
if c.Bool("insecure") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that secure configurations specify a hostname, domain, and tag for JWT validation.
|
||||
if !c.IsSet("hostname") || !c.IsSet("auth-domain") || !c.IsSet("application-aud") {
|
||||
log.Fatal("must specify --hostname, --auth-domain, and --application-aud unless you want to run in --insecure mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CmdAction starts the Proxy and sets the url in cli.Context to point to the Proxy address.
|
||||
func CmdAction(c *cli.Context) error {
|
||||
// STOR-612: sync with context in tunnel daemon.
|
||||
ctx := context.Background()
|
||||
|
||||
var proxy *Proxy
|
||||
var err error
|
||||
if c.Bool("insecure") {
|
||||
proxy, err = NewInsecureProxy(ctx, c.String("url"))
|
||||
} else {
|
||||
proxy, err = NewSecureProxy(ctx, c.String("url"), c.String("auth-domain"), c.String("application-aud"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
listenerC := make(chan net.Listener)
|
||||
defer close(listenerC)
|
||||
|
||||
// Since the Proxy should only talk to the tunnel daemon, find the next available
|
||||
// localhost port and start to listen to requests.
|
||||
go func() {
|
||||
err := proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Block until the the Proxy is online, retreive its address, and change the url to point to it.
|
||||
// This is effectively "handing over" control to the tunnel package so it can run the tunnel daemon.
|
||||
c.Set("url", "https://"+(<-listenerC).Addr().String())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"cloudflared", "db-connect", "--playground"},
|
||||
{"cloudflared", "db-connect", "--playground", "--hostname", "sql.mysite.com"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--insecure"},
|
||||
{"cloudflared", "db-connect", "--url", "sqlite3::memory:?cache=shared", "--hostname", "sql.mysite.com", "--auth-domain", "mysite.cloudflareaccess.com", "--application-aud", "aud"},
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "cloudflared",
|
||||
Commands: []*cli.Command{Cmd()},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.NoError(t, app.Run(test))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP server that proxies requests to a Client.
|
||||
type Proxy struct {
|
||||
client Client
|
||||
accessValidator *validation.Access
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In insecure mode, the Proxy will allow all Command requests.
|
||||
func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
|
||||
originURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "must provide a valid database url")
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, originURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = client.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not connect to the database")
|
||||
}
|
||||
|
||||
return &Proxy{client, nil, logrus.New()}, nil
|
||||
}
|
||||
|
||||
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
|
||||
//
|
||||
// In secure mode, the Proxy will reject any Command requests that are
|
||||
// not authenticated by Cloudflare Access with a valid JWT.
|
||||
func NewSecureProxy(ctx context.Context, origin, authDomain, applicationAUD string) (*Proxy, error) {
|
||||
proxy, err := NewInsecureProxy(ctx, origin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validator, err := validation.NewAccessValidator(ctx, authDomain, authDomain, applicationAUD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.accessValidator = validator
|
||||
|
||||
return proxy, err
|
||||
}
|
||||
|
||||
// IsInsecure gets whether the Proxy will accept a Command from any source.
|
||||
func (proxy *Proxy) IsInsecure() bool {
|
||||
return proxy.accessValidator == nil
|
||||
}
|
||||
|
||||
// IsAllowed checks whether a http.Request is allowed to receive data.
|
||||
//
|
||||
// By default, requests must pass through Cloudflare Access for authentication.
|
||||
// If the proxy is explcitly set to insecure mode, all requests will be allowed.
|
||||
func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
|
||||
if proxy.IsInsecure() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Access and Tunnel should prevent bad JWTs from even reaching the origin,
|
||||
// but validate tokens anyway as an abundance of caution.
|
||||
err := proxy.accessValidator.ValidateRequest(r.Context(), r)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Warn administrators that invalid JWTs are being rejected. This is indicative
|
||||
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
|
||||
if len(verbose) > 0 {
|
||||
proxy.httpLog(r, err).Error("Failed JWT authentication")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Start the Proxy at a given address and notify the listener channel when the server is online.
|
||||
func (proxy *Proxy) Start(ctx context.Context, addr string, listenerC chan<- net.Listener) error {
|
||||
// STOR-611: use a seperate listener and consider web socket support.
|
||||
httpListener, err := hello.CreateTLSListener(addr)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create listener at %s", addr)
|
||||
}
|
||||
|
||||
errC := make(chan error)
|
||||
defer close(errC)
|
||||
|
||||
// Starts the HTTP server and begins to serve requests.
|
||||
go func() {
|
||||
errC <- proxy.httpListen(ctx, httpListener)
|
||||
}()
|
||||
|
||||
// Continually ping the server until it comes online or 10 attempts fail.
|
||||
go func() {
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = http.Get("http://" + httpListener.Addr().String())
|
||||
|
||||
// Once no error was detected, notify the listener channel and return.
|
||||
if err == nil {
|
||||
listenerC <- httpListener
|
||||
return
|
||||
}
|
||||
|
||||
// Backoff between requests to ping the server.
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
errC <- errors.Wrap(err, "took too long for the http server to start")
|
||||
}()
|
||||
|
||||
return <-errC
|
||||
}
|
||||
|
||||
// httpListen starts the httpServer and blocks until the context closes.
|
||||
func (proxy *Proxy) httpListen(ctx context.Context, listener net.Listener) error {
|
||||
httpServer := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: proxy.httpRouter(),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
httpServer.Close()
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return httpServer.Serve(listener)
|
||||
}
|
||||
|
||||
// httpRouter creates a mux.Router for the Proxy.
|
||||
func (proxy *Proxy) httpRouter() *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/ping", proxy.httpPing()).Methods("GET", "HEAD")
|
||||
router.HandleFunc("/submit", proxy.httpSubmit()).Methods("POST")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// httpPing tests the connection to the database.
|
||||
//
|
||||
// By default, this endpoint is unauthenticated to allow for health checks.
|
||||
// To enable authentication, Cloudflare Access must be enabled on this route.
|
||||
func (proxy *Proxy) httpPing() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
err := proxy.client.Ping(ctx)
|
||||
|
||||
if err == nil {
|
||||
proxy.httpRespond(w, r, http.StatusOK, "")
|
||||
} else {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpSubmit sends a command to the database and returns its response.
|
||||
//
|
||||
// By default, this endpoint will reject requests that do not pass through Cloudflare Access.
|
||||
// To disable authentication, the --insecure flag must be specified in the command line.
|
||||
func (proxy *Proxy) httpSubmit() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !proxy.IsAllowed(r, true) {
|
||||
proxy.httpRespondErr(w, r, http.StatusForbidden, fmt.Errorf(""))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd Command
|
||||
err := json.NewDecoder(r.Body).Decode(&cmd)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
data, err := proxy.client.Submit(ctx, &cmd)
|
||||
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
proxy.httpRespondErr(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespond writes a status code and string response to the response writer.
|
||||
func (proxy *Proxy) httpRespond(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
// Only expose the message detail of the reponse if the request is not HEAD
|
||||
// and the user is authenticated. For example, this prevents an unauthenticated
|
||||
// failed health check from accidentally leaking sensitive information about the Client.
|
||||
if r.Method != http.MethodHead && proxy.IsAllowed(r) {
|
||||
if message == "" {
|
||||
message = http.StatusText(status)
|
||||
}
|
||||
fmt.Fprint(w, message)
|
||||
}
|
||||
}
|
||||
|
||||
// httpRespondErr is similar to httpRespond, except it formats errors to be more friendly.
|
||||
func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defaultStatus int, err error) {
|
||||
status, err := httpError(defaultStatus, err)
|
||||
|
||||
proxy.httpRespond(w, r, status, err.Error())
|
||||
if len(err.Error()) > 0 {
|
||||
proxy.httpLog(r, err).Warn("Database proxy error")
|
||||
}
|
||||
}
|
||||
|
||||
// httpLog returns a logrus.Entry that is formatted to output a request Cf-ray.
|
||||
func (proxy *Proxy) httpLog(r *http.Request, err error) *logrus.Entry {
|
||||
return proxy.logger.WithContext(r.Context()).WithField("CF-RAY", r.Header.Get("Cf-ray")).WithError(err)
|
||||
}
|
||||
|
||||
// httpError extracts common errors and returns an status code and friendly error.
|
||||
func httpError(defaultStatus int, err error) (int, error) {
|
||||
if err == nil {
|
||||
return http.StatusNotImplemented, fmt.Errorf("error expected but found none")
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return http.StatusBadRequest, fmt.Errorf("request body cannot be empty")
|
||||
}
|
||||
|
||||
if err == context.DeadlineExceeded {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
_, ok := err.(net.Error)
|
||||
if ok {
|
||||
return http.StatusRequestTimeout, err
|
||||
}
|
||||
|
||||
if err == context.Canceled {
|
||||
// Does not exist in Golang, but would be: http.StatusClientClosedWithoutResponse
|
||||
return 444, err
|
||||
}
|
||||
|
||||
return defaultStatus, err
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewInsecureProxy(t *testing.T) {
|
||||
origins := []string{
|
||||
"",
|
||||
":/",
|
||||
"http://localhost",
|
||||
"tcp://localhost:9000?debug=true",
|
||||
"mongodb://127.0.0.1",
|
||||
}
|
||||
|
||||
for _, origin := range origins {
|
||||
proxy, err := NewInsecureProxy(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyIsAllowed(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
req := httptest.NewRequest("GET", "https://1.1.1.1/ping", nil)
|
||||
assert.True(t, proxy.IsAllowed(req))
|
||||
|
||||
proxy = helperNewProxy(t, true)
|
||||
req.Header.Set("Cf-access-jwt-assertion", "xxx")
|
||||
assert.False(t, proxy.IsAllowed(req))
|
||||
}
|
||||
|
||||
func TestProxyStart(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
ctx := context.Background()
|
||||
listenerC := make(chan net.Listener)
|
||||
|
||||
err := proxy.Start(ctx, "1.1.1.1:", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:-1", listenerC)
|
||||
assert.Error(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 0)
|
||||
defer cancel()
|
||||
|
||||
err = proxy.Start(ctx, "127.0.0.1:", listenerC)
|
||||
assert.IsType(t, http.ErrServerClosed, err)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRouter(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
router := proxy.httpRouter()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
method string
|
||||
valid bool
|
||||
}{
|
||||
{"", "GET", false},
|
||||
{"/", "GET", false},
|
||||
{"/ping", "GET", true},
|
||||
{"/ping", "HEAD", true},
|
||||
{"/ping", "POST", false},
|
||||
{"/submit", "POST", true},
|
||||
{"/submit", "GET", false},
|
||||
{"/submit/extra", "POST", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
match := &mux.RouteMatch{}
|
||||
ok := router.Match(httptest.NewRequest(test.method, "https://1.1.1.1"+test.path, nil), match)
|
||||
|
||||
assert.True(t, ok == test.valid, test.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPPing(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpPing())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(2), res.ContentLength)
|
||||
|
||||
res, err = client.Head(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Equal(t, int64(-1), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmit(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
status int
|
||||
output string
|
||||
}{
|
||||
{"", http.StatusBadRequest, "request body cannot be empty"},
|
||||
{"{}", http.StatusBadRequest, "cannot provide an empty statement"},
|
||||
{"{\"statement\":\"Ok\"}", http.StatusUnprocessableEntity, "cannot provide invalid sql mode: ''"},
|
||||
{"{\"statement\":\"Ok\",\"mode\":\"query\"}", http.StatusUnprocessableEntity, "near \"Ok\": syntax error"},
|
||||
{"{\"statement\":\"CREATE TABLE t (a INT);\",\"mode\":\"exec\"}", http.StatusOK, "{\"last_insert_id\":0,\"rows_affected\":0}\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res, err := client.Post(server.URL, "application/json", strings.NewReader(test.input))
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.status, res.StatusCode)
|
||||
if res.StatusCode > http.StatusOK {
|
||||
assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-type"))
|
||||
} else {
|
||||
assert.Equal(t, "application/json", res.Header.Get("Content-type"))
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
str := string(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.output, str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPSubmitForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(proxy.httpSubmit())
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.Zero(t, res.ContentLength)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespond(t *testing.T) {
|
||||
proxy := helperNewProxy(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(5), res.ContentLength)
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
assert.Equal(t, []byte("Hello"), data)
|
||||
}
|
||||
|
||||
func TestProxyHTTPRespondForbidden(t *testing.T) {
|
||||
proxy := helperNewProxy(t, true)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.httpRespond(w, r, http.StatusAccepted, "Hello")
|
||||
}))
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
res, err := client.Get(server.URL)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, int64(0), res.ContentLength)
|
||||
}
|
||||
|
||||
func TestHTTPError(t *testing.T) {
|
||||
_, errTimeout := net.DialTimeout("tcp", "127.0.0.1", 0)
|
||||
assert.Error(t, errTimeout)
|
||||
|
||||
tests := []struct {
|
||||
input error
|
||||
status int
|
||||
output error
|
||||
}{
|
||||
{nil, http.StatusNotImplemented, fmt.Errorf("error expected but found none")},
|
||||
{io.EOF, http.StatusBadRequest, fmt.Errorf("request body cannot be empty")},
|
||||
{context.DeadlineExceeded, http.StatusRequestTimeout, nil},
|
||||
{context.Canceled, 444, nil},
|
||||
{errTimeout, http.StatusRequestTimeout, nil},
|
||||
{fmt.Errorf(""), http.StatusInternalServerError, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
status, err := httpError(http.StatusInternalServerError, test.input)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, test.status, status)
|
||||
if test.output == nil {
|
||||
test.output = test.input
|
||||
}
|
||||
assert.Equal(t, test.output, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperNewProxy(t *testing.T, secure ...bool) *Proxy {
|
||||
t.Helper()
|
||||
|
||||
proxy, err := NewSecureProxy(context.Background(), "file::memory:?cache=shared", "test.cloudflareaccess.com", "")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, proxy)
|
||||
|
||||
if len(secure) == 0 {
|
||||
proxy.accessValidator = nil // Mark as insecure
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xo/dburl"
|
||||
|
||||
// SQL drivers self-register with the database/sql package.
|
||||
// https://github.com/golang/go/wiki/SQLDrivers
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SQLClient is a Client that talks to a SQL database.
|
||||
type SQLClient struct {
|
||||
Dialect string
|
||||
driver *sqlx.DB
|
||||
}
|
||||
|
||||
// NewSQLClient creates a SQL client based on its URL scheme.
|
||||
func NewSQLClient(ctx context.Context, originURL *url.URL) (Client, error) {
|
||||
res, err := dburl.Parse(originURL.String())
|
||||
if err != nil {
|
||||
helpText := fmt.Sprintf("supported drivers: %+q, see documentation for more details: %s", sql.Drivers(), "https://godoc.org/github.com/xo/dburl")
|
||||
return nil, fmt.Errorf("could not parse sql database url '%s': %s\n%s", originURL, err.Error(), helpText)
|
||||
}
|
||||
|
||||
// Establishes the driver, but does not test the connection.
|
||||
driver, err := sqlx.Open(res.Driver, res.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open sql driver %s: %s\n%s", res.Driver, err.Error(), res.DSN)
|
||||
}
|
||||
|
||||
// Closes the driver, will occur when the context finishes.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
driver.Close()
|
||||
}()
|
||||
|
||||
return &SQLClient{driver.DriverName(), driver}, nil
|
||||
}
|
||||
|
||||
// Ping verifies a connection to the database is still alive.
|
||||
func (client *SQLClient) Ping(ctx context.Context) error {
|
||||
return client.driver.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Submit queries or executes a command to the SQL database.
|
||||
func (client *SQLClient) Submit(ctx context.Context, cmd *Command) (interface{}, error) {
|
||||
txx, err := cmd.ValidateSQL(client.Dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, cmd.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var res interface{}
|
||||
|
||||
// Get the next available sql.Conn and submit the Command.
|
||||
err = sqlConn(ctx, client.driver, txx, func(conn *sql.Conn) error {
|
||||
stmt := cmd.Statement
|
||||
args := cmd.Arguments.Positional
|
||||
|
||||
if cmd.Mode == "query" {
|
||||
res, err = sqlQuery(ctx, conn, stmt, args)
|
||||
} else {
|
||||
res, err = sqlExec(ctx, conn, stmt, args)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ValidateSQL extends the contract of Command for SQL dialects:
|
||||
// mode is conformed, arguments are []sql.NamedArg, and isolation is a sql.IsolationLevel.
|
||||
//
|
||||
// When the command should not be wrapped in a transaction, *sql.TxOptions and error will both be nil.
|
||||
func (cmd *Command) ValidateSQL(dialect string) (*sql.TxOptions, error) {
|
||||
err := cmd.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode, err := sqlMode(cmd.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mutates Arguments to only use positional arguments with the type sql.NamedArg.
|
||||
// This is a required by the sql.Driver before submitting arguments.
|
||||
cmd.Arguments.sql(dialect)
|
||||
|
||||
iso, err := sqlIsolation(cmd.Isolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When isolation is out-of-range, this is indicative that no
|
||||
// transaction should be executed and sql.TxOptions should be nil.
|
||||
if iso < sql.LevelDefault {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// In query mode, execute the transaction in read-only, unless it's Microsoft SQL
|
||||
// which does not support that type of transaction.
|
||||
readOnly := mode == "query" && dialect != "mssql"
|
||||
|
||||
return &sql.TxOptions{Isolation: iso, ReadOnly: readOnly}, nil
|
||||
}
|
||||
|
||||
// sqlConn gets the next available sql.Conn in the connection pool and runs a function to use it.
|
||||
//
|
||||
// If the transaction options are nil, run the useIt function outside a transaction.
|
||||
// This is potentially an unsafe operation if the command does not clean up its state.
|
||||
func sqlConn(ctx context.Context, driver *sqlx.DB, txx *sql.TxOptions, useIt func(*sql.Conn) error) error {
|
||||
conn, err := driver.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// If transaction options are specified, begin and defer a rollback to catch errors.
|
||||
var tx *sql.Tx
|
||||
if txx != nil {
|
||||
tx, err = conn.BeginTx(ctx, txx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
}
|
||||
|
||||
err = useIt(conn)
|
||||
|
||||
// Check if useIt was successful and a transaction exists before committing.
|
||||
if err == nil && tx != nil {
|
||||
err = tx.Commit()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlQuery queries rows on a sql.Conn and returns an array of result objects.
|
||||
func sqlQuery(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) ([]map[string]interface{}, error) {
|
||||
rows, err := conn.QueryContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlRows(rows)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sqlExec executes a command on a sql.Conn and returns the result of the operation.
|
||||
func sqlExec(ctx context.Context, conn *sql.Conn, stmt string, args []interface{}) (sqlResult, error) {
|
||||
exec, err := conn.ExecContext(ctx, stmt, args...)
|
||||
if err == nil {
|
||||
return sqlResultFrom(exec), nil
|
||||
}
|
||||
return sqlResult{}, err
|
||||
}
|
||||
|
||||
// sql mutates Arguments to contain a positional []sql.NamedArg.
|
||||
//
|
||||
// The actual return type is []interface{} due to the native Golang
|
||||
// function signatures for sql.Exec and sql.Query being generic.
|
||||
func (args *Arguments) sql(dialect string) {
|
||||
result := args.Positional
|
||||
|
||||
for i, val := range result {
|
||||
result[i] = sqlArg("", val, dialect)
|
||||
}
|
||||
|
||||
for key, val := range args.Named {
|
||||
result = append(result, sqlArg(key, val, dialect))
|
||||
}
|
||||
|
||||
args.Positional = result
|
||||
args.Named = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// sqlArg creates a sql.NamedArg from a key-value pair and an optional dialect.
|
||||
//
|
||||
// Certain dialects will need to wrap objects, such as arrays, to conform its driver requirements.
|
||||
func sqlArg(key, val interface{}, dialect string) sql.NamedArg {
|
||||
switch reflect.ValueOf(val).Kind() {
|
||||
|
||||
// PostgreSQL and Clickhouse require arrays to be wrapped before
|
||||
// being inserted into the driver interface.
|
||||
case reflect.Slice, reflect.Array:
|
||||
switch dialect {
|
||||
case "postgres":
|
||||
val = pq.Array(val)
|
||||
case "clickhouse":
|
||||
val = clickhouse.Array(val)
|
||||
}
|
||||
}
|
||||
|
||||
return sql.Named(fmt.Sprint(key), val)
|
||||
}
|
||||
|
||||
// sqlIsolation tries to match a string to a sql.IsolationLevel.
|
||||
func sqlIsolation(str string) (sql.IsolationLevel, error) {
|
||||
if str == "none" {
|
||||
return sql.IsolationLevel(-1), nil
|
||||
}
|
||||
|
||||
for iso := sql.LevelDefault; ; iso++ {
|
||||
if iso > sql.LevelLinearizable {
|
||||
return -1, fmt.Errorf("cannot provide an invalid sql isolation level: '%s'", str)
|
||||
}
|
||||
|
||||
if str == "" || strings.EqualFold(iso.String(), strings.ReplaceAll(str, "_", " ")) {
|
||||
return iso, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sqlMode tries to match a string to a command mode: 'query' or 'exec' for now.
|
||||
func sqlMode(str string) (string, error) {
|
||||
switch str {
|
||||
case "query", "exec":
|
||||
return str, nil
|
||||
default:
|
||||
return "", fmt.Errorf("cannot provide invalid sql mode: '%s'", str)
|
||||
}
|
||||
}
|
||||
|
||||
// sqlRows scans through a SQL result set and returns an array of objects.
|
||||
func sqlRows(rows *sql.Rows) ([]map[string]interface{}, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract columns from result")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
// Some drivers do not support type extraction, so fail silently and continue.
|
||||
types = make([]*sql.ColumnType, len(columns))
|
||||
}
|
||||
|
||||
values := make([]interface{}, len(columns))
|
||||
pointers := make([]interface{}, len(columns))
|
||||
|
||||
var results []map[string]interface{}
|
||||
for rows.Next() {
|
||||
for i := range columns {
|
||||
pointers[i] = &values[i]
|
||||
}
|
||||
rows.Scan(pointers...)
|
||||
|
||||
// Convert a row, an array of values, into an object where
|
||||
// each key is the name of its respective column.
|
||||
entry := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
entry[col] = sqlValue(values[i], types[i])
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// sqlValue handles special cases where sql.Rows does not return a "human-readable" object.
|
||||
func sqlValue(val interface{}, col *sql.ColumnType) interface{} {
|
||||
bytes, ok := val.([]byte)
|
||||
if ok {
|
||||
// Opportunistically check for embeded JSON and convert it to a first-class object.
|
||||
var embeded interface{}
|
||||
if json.Unmarshal(bytes, &embeded) == nil {
|
||||
return embeded
|
||||
}
|
||||
|
||||
// STOR-604: investigate a way to coerce PostgreSQL arrays '{a, b, ...}' into JSON.
|
||||
// Although easy with strings, it becomes more difficult with special types like INET[].
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// sqlResult is a thin wrapper around sql.Result.
|
||||
type sqlResult struct {
|
||||
LastInsertId int64 `json:"last_insert_id"`
|
||||
RowsAffected int64 `json:"rows_affected"`
|
||||
}
|
||||
|
||||
// sqlResultFrom converts sql.Result into a JSON-marshable sqlResult.
|
||||
func sqlResultFrom(res sql.Result) sqlResult {
|
||||
insertID, errID := res.LastInsertId()
|
||||
rowsAffected, errRows := res.RowsAffected()
|
||||
|
||||
// If an error occurs when extracting the result, it is because the
|
||||
// driver does not support that specific field. Instead of passing this
|
||||
// to the user, omit the field in the response.
|
||||
if errID != nil {
|
||||
insertID = -1
|
||||
}
|
||||
if errRows != nil {
|
||||
rowsAffected = -1
|
||||
}
|
||||
|
||||
return sqlResult{insertID, rowsAffected}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package dbconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kshvakov/clickhouse"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewSQLClient(t *testing.T) {
|
||||
originURLs := []string{
|
||||
"postgres://localhost",
|
||||
"cockroachdb://localhost:1337",
|
||||
"postgresql://user:pass@127.0.0.1",
|
||||
"mysql://localhost",
|
||||
"clickhouse://127.0.0.1:9000/?debug",
|
||||
"sqlite3::memory:",
|
||||
"file:test.db?cache=shared",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.NoError(t, err, originURL)
|
||||
}
|
||||
|
||||
originURLs = []string{
|
||||
"",
|
||||
"/",
|
||||
"http://localhost",
|
||||
"coolthing://user:pass@127.0.0.1",
|
||||
}
|
||||
|
||||
for _, originURL := range originURLs {
|
||||
origin, _ := url.Parse(originURL)
|
||||
_, err := NewSQLClient(context.Background(), origin)
|
||||
|
||||
assert.Error(t, err, originURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentsSQL(t *testing.T) {
|
||||
args := []Arguments{
|
||||
Arguments{
|
||||
Positional: []interface{}{
|
||||
"val", 10, 3.14,
|
||||
},
|
||||
},
|
||||
Arguments{
|
||||
Named: map[string]interface{}{
|
||||
"key": time.Unix(0, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var nameType sql.NamedArg
|
||||
for _, arg := range args {
|
||||
arg.sql("")
|
||||
for _, named := range arg.Positional {
|
||||
assert.IsType(t, nameType, named)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
key interface{}
|
||||
val interface{}
|
||||
dialect string
|
||||
arg sql.NamedArg
|
||||
}{
|
||||
{"key", "val", "mssql", sql.Named("key", "val")},
|
||||
{0, 1, "sqlite3", sql.Named("0", 1)},
|
||||
{1, []string{"a", "b", "c"}, "postgres", sql.Named("1", pq.Array([]string{"a", "b", "c"}))},
|
||||
{"in", []uint{0, 1}, "clickhouse", sql.Named("in", clickhouse.Array([]uint{0, 1}))},
|
||||
{"", time.Unix(0, 0), "", sql.Named("", time.Unix(0, 0))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
arg := sqlArg(test.key, test.val, test.dialect)
|
||||
assert.Equal(t, test.arg, arg, test.key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLIsolation(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
iso sql.IsolationLevel
|
||||
}{
|
||||
{"", sql.LevelDefault},
|
||||
{"DEFAULT", sql.LevelDefault},
|
||||
{"read_UNcommitted", sql.LevelReadUncommitted},
|
||||
{"serializable", sql.LevelSerializable},
|
||||
{"none", sql.IsolationLevel(-1)},
|
||||
{"SNAP shot", -2},
|
||||
{"blah", -2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
iso, err := sqlIsolation(test.str)
|
||||
|
||||
if test.iso < -1 {
|
||||
assert.Error(t, err, test.str)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.iso, iso, test.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLMode(t *testing.T) {
|
||||
modes := []string{
|
||||
"query",
|
||||
"exec",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
actual, err := sqlMode(mode)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.ToLower(mode), actual, mode)
|
||||
}
|
||||
|
||||
modes = []string{
|
||||
"",
|
||||
"blah",
|
||||
}
|
||||
|
||||
for _, mode := range modes {
|
||||
_, err := sqlMode(mode)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func helperRows(mockRows *sqlmock.Rows) *sql.Rows {
|
||||
db, mock, _ := sqlmock.New()
|
||||
mock.ExpectQuery("SELECT").WillReturnRows(mockRows)
|
||||
rows, _ := db.Query("SELECT")
|
||||
return rows
|
||||
}
|
||||
|
||||
func TestSQLRows(t *testing.T) {
|
||||
actual, err := sqlRows(helperRows(sqlmock.NewRows(
|
||||
[]string{"name", "age", "dept"}).
|
||||
AddRow("alice", 19, "prod")))
|
||||
expected := []map[string]interface{}{map[string]interface{}{
|
||||
"name": "alice",
|
||||
"age": int64(19),
|
||||
"dept": "prod"}}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestSQLValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
input interface{}
|
||||
output interface{}
|
||||
}{
|
||||
{"hello", "hello"},
|
||||
{1, 1},
|
||||
{false, false},
|
||||
{[]byte("random"), "random"},
|
||||
{[]byte("{\"json\":true}"), map[string]interface{}{"json": true}},
|
||||
{[]byte("[]"), []interface{}{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.output, sqlValue(test.input, nil), test.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResultFrom(t *testing.T) {
|
||||
res := sqlResultFrom(sqlmock.NewResult(1, 2))
|
||||
assert.Equal(t, sqlResult{1, 2}, res)
|
||||
|
||||
res = sqlResultFrom(sqlmock.NewErrorResult(fmt.Errorf("")))
|
||||
assert.Equal(t, sqlResult{-1, -1}, res)
|
||||
}
|
||||
|
||||
func helperSQLite3(t *testing.T) (context.Context, Client) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
url, _ := url.Parse("file::memory:?cache=shared")
|
||||
|
||||
sqlite3, err := NewSQLClient(ctx, url)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return ctx, sqlite3
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
err := sqlite3.Ping(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSubmit(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "CREATE TABLE t (a INTEGER, b FLOAT, c TEXT, d BLOB);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{
|
||||
1,
|
||||
3.14,
|
||||
"text",
|
||||
"blob",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "UPDATE t SET c = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t WHERE a = ?;",
|
||||
Mode: "query",
|
||||
Arguments: Arguments{
|
||||
Positional: []interface{}{1},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
|
||||
resf, ok := res.([]map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, map[string]interface{}{
|
||||
"a": int64(1),
|
||||
"b": 3.14,
|
||||
"c": nil,
|
||||
"d": "blob",
|
||||
}, resf[0])
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{1, 1}, res)
|
||||
}
|
||||
|
||||
func TestSubmitTransaction(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
res, err = sqlite3.Submit(ctx, &Command{
|
||||
Statement: "BEGIN; CREATE TABLE tt (a INT); COMMIT;",
|
||||
Mode: "exec",
|
||||
Isolation: "none",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sqlResult{0, 0}, res)
|
||||
|
||||
rows, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM tt;",
|
||||
Mode: "query",
|
||||
Isolation: "repeatable_read",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rows)
|
||||
}
|
||||
|
||||
func TestSubmitTimeout(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
Timeout: 1 * time.Nanosecond,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitMode(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "notanoption",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
|
||||
func TestSubmitEmpty(t *testing.T) {
|
||||
ctx, sqlite3 := helperSQLite3(t)
|
||||
|
||||
res, err := sqlite3.Submit(ctx, &Command{
|
||||
Statement: "; ; ; ;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, res)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# docker-compose -f ./dbconnect_tests/dbconnect.yaml up --build --force-recreate --renew-anon-volumes --exit-code-from cloudflared
|
||||
|
||||
version: "2.3"
|
||||
networks:
|
||||
test-dbconnect-network:
|
||||
driver: bridge
|
||||
services:
|
||||
cloudflared:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: dev.Dockerfile
|
||||
command: go test github.com/cloudflare/cloudflared/dbconnect_tests -v
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
mssql:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DBCONNECT_INTEGRATION_TEST: "true"
|
||||
POSTGRESQL_URL: postgres://postgres:secret@postgres/db?sslmode=disable
|
||||
MYSQL_URL: mysql://root:secret@mysql/db?tls=false
|
||||
MSSQL_URL: mssql://sa:secret12345!@mssql
|
||||
CLICKHOUSE_URL: clickhouse://clickhouse:9000/db
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
postgres:
|
||||
image: postgres:11.4-alpine
|
||||
environment:
|
||||
POSTGRES_DB: db
|
||||
POSTGRES_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: db
|
||||
MYSQL_ROOT_PASSWORD: secret
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-CU8-ubuntu
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
SA_PASSWORD: secret12345!
|
||||
healthcheck:
|
||||
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "secret12345!", "-Q", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
clickhouse:
|
||||
image: yandex/clickhouse-server:19.11
|
||||
healthcheck:
|
||||
test: ["CMD", "clickhouse-client", "--query", "SELECT 1"]
|
||||
start_period: 3s
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks:
|
||||
- test-dbconnect-network
|
||||
@@ -0,0 +1,265 @@
|
||||
package dbconnect_tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/dbconnect"
|
||||
)
|
||||
|
||||
func TestIntegrationPostgreSQL(t *testing.T) {
|
||||
ctx, pq := helperNewSQLClient(t, "POSTGRESQL_URL")
|
||||
|
||||
err := pq.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a TEXT, b UUID, c JSON, d INET[], e SERIAL);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES ($1, $2, $3, $4);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"text",
|
||||
"6b8d686d-bd8e-43bc-b09a-cfcbbe702c10",
|
||||
"{\"bool\":true,\"array\":[\"a\", 1, 3.14],\"embed\":{\"num\":21}}",
|
||||
[]string{"1.1.1.1", "1.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET b = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "text",
|
||||
"b": nil,
|
||||
"c": map[string]interface{}{
|
||||
"bool": true,
|
||||
"array": []interface{}{"a", float64(1), 3.14},
|
||||
"embed": map[string]interface{}{"num": float64(21)},
|
||||
},
|
||||
"d": "{1.1.1.1,1.0.0.1}",
|
||||
"e": int64(1),
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = pq.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMySQL(t *testing.T) {
|
||||
ctx, my := helperNewSQLClient(t, "MYSQL_URL")
|
||||
|
||||
err := my.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a CHAR, b TINYINT, c FLOAT, d JSON, e YEAR);",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"a",
|
||||
10,
|
||||
3.14,
|
||||
"{\"bool\":true}",
|
||||
2000,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "ALTER TABLE t ADD COLUMN f GEOMETRY;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "a",
|
||||
"b": float64(10),
|
||||
"c": 3.14,
|
||||
"d": map[string]interface{}{"bool": true},
|
||||
"e": float64(2000),
|
||||
"f": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = my.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationMSSQL(t *testing.T) {
|
||||
ctx, ms := helperNewSQLClient(t, "MSSQL_URL")
|
||||
|
||||
err := ms.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a BIT, b DECIMAL, c MONEY, d TEXT);",
|
||||
Mode: "exec"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
0,
|
||||
3,
|
||||
"$0.99",
|
||||
"text",
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "UPDATE t SET d = NULL;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": false,
|
||||
"b": float64(3),
|
||||
"c": float64(0.99),
|
||||
"d": nil,
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ms.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegrationClickhouse(t *testing.T) {
|
||||
ctx, ch := helperNewSQLClient(t, "CLICKHOUSE_URL")
|
||||
|
||||
err := ch.Ping(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "CREATE TABLE t (a UUID, b String, c Float64, d UInt32, e Int16, f Array(Enum8('a'=1, 'b'=2, 'c'=3))) engine=Memory;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "INSERT INTO t VALUES (?, ?, ?, ?, ?, ?);",
|
||||
Mode: "exec",
|
||||
Arguments: dbconnect.Arguments{
|
||||
Positional: []interface{}{
|
||||
"ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"",
|
||||
3.14,
|
||||
314,
|
||||
-144,
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "SELECT * FROM t;",
|
||||
Mode: "query",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, make([]map[string]interface{}, 0), res)
|
||||
|
||||
actual := res.([]map[string]interface{})[0]
|
||||
expected := map[string]interface{}{
|
||||
"a": "ec65f626-6f50-4c86-9628-6314ef1edacd",
|
||||
"b": "",
|
||||
"c": float64(3.14),
|
||||
"d": uint32(314),
|
||||
"e": int16(-144),
|
||||
"f": []string{"a", "b", "c"},
|
||||
}
|
||||
assert.EqualValues(t, expected, actual)
|
||||
|
||||
_, err = ch.Submit(ctx, &dbconnect.Command{
|
||||
Statement: "DROP TABLE t;",
|
||||
Mode: "exec",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func helperNewSQLClient(t *testing.T, env string) (context.Context, dbconnect.Client) {
|
||||
_, ok := os.LookupEnv("DBCONNECT_INTEGRATION_TEST")
|
||||
if ok {
|
||||
t.Helper()
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
val, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
log.Fatalf("must provide database url as environment variable: %s", env)
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(val)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot provide invalid database url: %s=%s", env, val)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := dbconnect.NewSQLClient(ctx, parsed)
|
||||
if err != nil {
|
||||
log.Fatalf("could not start test client: %s", err)
|
||||
}
|
||||
|
||||
return ctx, client
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM golang:1.12 as builder
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
RUN apt-get update
|
||||
COPY . .
|
||||
@@ -0,0 +1,135 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"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
|
||||
|
||||
subsystemEdgeAddrResolver = "edgeAddrResolver"
|
||||
)
|
||||
|
||||
// Redeclare network functions so they can be overridden in tests.
|
||||
var (
|
||||
netLookupSRV = net.LookupSRV
|
||||
netLookupIP = net.LookupIP
|
||||
)
|
||||
|
||||
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
// But both of these cases miss out on a key feature from the stdlib:
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
|
||||
// Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it.
|
||||
// See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552
|
||||
var fallbackLookupSRV = lookupSRVWithDOT
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
`Please try the following things to diagnose this issue:`,
|
||||
` 1. ensure that argotunnel.com is returning "origintunneld" service records.`,
|
||||
` Run your system's equivalent of: dig srv _origintunneld._tcp.argotunnel.com`,
|
||||
` 2. ensure that your DNS resolver is not returning compressed SRV records.`,
|
||||
` See GitHub issue https://github.com/golang/go/issues/27546`,
|
||||
` For example, you could use Cloudflare's 1.1.1.1 as your resolver:`,
|
||||
` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`,
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(logger *logrus.Entry) ([][]*net.TCPAddr, error) {
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
if fallbackErr != nil || len(fallbackAddrs) == 0 {
|
||||
// use the original DNS error `err` in messages, not `fallbackErr`
|
||||
logger.Errorln("Error looking up Cloudflare edge IPs: the DNS query failed:", err)
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
logger.Errorln(s)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
|
||||
}
|
||||
// Accept the fallback results and keep going
|
||||
addrs = fallbackAddrs
|
||||
}
|
||||
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
for _, addr := range addrs {
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
|
||||
return resolvedIPsPerCNAME, nil
|
||||
}
|
||||
|
||||
func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||
var dialer net.Dialer
|
||||
conn, err := dialer.DialContext(ctx, "tcp", dotServerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := &tls.Config{ServerName: dotServerName}
|
||||
return tls.Client(conn, tlsConfig), nil
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dotTimeout)
|
||||
defer cancel()
|
||||
return r.LookupSRV(ctx, srvService, srvProto, srvName)
|
||||
}
|
||||
|
||||
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
ips, err := netLookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("SRV record %v had no IPs", srv)
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
|
||||
// of the hostname's IP addresses
|
||||
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEdgeDiscovery(t *testing.T) {
|
||||
mockAddrs := newMockAddrs(19, 2, 5)
|
||||
netLookupSRV = mockNetLookupSRV(mockAddrs)
|
||||
netLookupIP = mockNetLookupIP(mockAddrs)
|
||||
|
||||
expectedAddrSet := map[string]bool{}
|
||||
for _, addrs := range mockAddrs.addrMap {
|
||||
for _, addr := range addrs {
|
||||
expectedAddrSet[addr.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
addrLists, err := edgeDiscovery(logrus.New().WithFields(logrus.Fields{}))
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
for _, addr := range addrs {
|
||||
actualAddrSet[addr.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedAddrSet, actualAddrSet)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
numAddrs int
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
}
|
||||
return mockAddrs{addrMap: addrMap, numAddrs: numAddrs}
|
||||
}
|
||||
|
||||
var _ quick.Generator = mockAddrs{}
|
||||
|
||||
func (mockAddrs) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
port := uint16(rand.Intn(math.MaxUint16))
|
||||
numRegions := uint8(1 + rand.Intn(10))
|
||||
numAddrsPerRegion := uint8(1 + rand.Intn(32))
|
||||
result := newMockAddrs(port, numRegions, numAddrsPerRegion)
|
||||
return reflect.ValueOf(result)
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupSRV that will return the SRV
|
||||
// records from mockAddrs.
|
||||
func mockNetLookupSRV(
|
||||
m mockAddrs,
|
||||
) func(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
var addrs []*net.SRV
|
||||
for k := range m.addrMap {
|
||||
addr := k
|
||||
addrs = append(addrs, &addr)
|
||||
// We can't just do
|
||||
// addrs = append(addrs, &k)
|
||||
// `k` will be reused by subsequent loop iterations,
|
||||
// so all the copies of `&k` would point to the same location.
|
||||
}
|
||||
return func(_, _, _ string) (string, []*net.SRV, error) {
|
||||
return "", addrs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a function compatible with net.LookupIP that translates the SRV records
|
||||
// from mockAddrs into IP addresses, based on the TCP addresses in mockAddrs.
|
||||
func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("No IPs for %v", host)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
|
||||
// redundancy purposes.
|
||||
type Region struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
|
||||
// NewRegion creates a region with the given addresses, which are all unused.
|
||||
func NewRegion(addrs []*net.TCPAddr) Region {
|
||||
// The zero value of UsedBy is Unused(), so we can just initialize the map's values with their
|
||||
// zero values.
|
||||
m := make(map[*net.TCPAddr]UsedBy)
|
||||
for _, addr := range addrs {
|
||||
m[addr] = Unused()
|
||||
}
|
||||
return Region{connFor: m}
|
||||
}
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection in this region.
|
||||
// Returns nil if the connection isn't using any IP.
|
||||
func (r *Region) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
for addr, used := range r.connFor {
|
||||
if used.Used && used.ConnID == connID {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AvailableAddrs counts how many unused addresses this region contains.
|
||||
func (r Region) AvailableAddrs() int {
|
||||
n := 0
|
||||
for _, usedby := range r.connFor {
|
||||
if !usedby.Used {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// GetUnusedIP returns a random unused address in this region.
|
||||
// Returns nil if all addresses are in use.
|
||||
func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
for addr, usedby := range r.connFor {
|
||||
if !usedby.Used && addr != excluding {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the address, assigning it to a proxy connection.
|
||||
func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
r.connFor[addr] = InUse(connID)
|
||||
}
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the region.
|
||||
func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
for addr := range r.connFor {
|
||||
return addr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GiveBack the address, ensuring it is no longer assigned to an IP.
|
||||
// Returns true if the address is in this region.
|
||||
func (r Region) GiveBack(addr *net.TCPAddr) (ok bool) {
|
||||
if _, ok := r.connFor[addr]; !ok {
|
||||
return false
|
||||
}
|
||||
r.connFor[addr] = Unused()
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegion_New(t *testing.T) {
|
||||
r := NewRegion([]*net.TCPAddr{&addr0, &addr1, &addr2})
|
||||
fmt.Println(r.connFor)
|
||||
if r.AvailableAddrs() != 3 {
|
||||
t.Errorf("r.AvailableAddrs() == %v but want 3", r.AvailableAddrs())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
connID int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
}{
|
||||
{
|
||||
name: "happy trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 0},
|
||||
want: &addr0,
|
||||
},
|
||||
{
|
||||
name: "sad trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 1},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "sad test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{connID: 3},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "happy test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{connID: 1},
|
||||
want: &addr1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &Region{
|
||||
connFor: tt.fields.connFor,
|
||||
}
|
||||
if got := r.AddrUsedBy(tt.args.connID); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Region.AddrUsedBy() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "contains addresses",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "all free",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: Unused(),
|
||||
}},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "all used",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := Region{
|
||||
connFor: tt.fields.connFor,
|
||||
}
|
||||
if got := r.AvailableAddrs(); got != tt.want {
|
||||
t.Errorf("Region.AvailableAddrs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
excluding *net.TCPAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
}{
|
||||
{
|
||||
name: "happy test with excluding set",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{excluding: &addr0},
|
||||
want: &addr1,
|
||||
},
|
||||
{
|
||||
name: "happy test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{excluding: nil},
|
||||
want: &addr1,
|
||||
},
|
||||
{
|
||||
name: "sad test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{excluding: nil},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
}},
|
||||
args: args{excluding: &addr0},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := Region{
|
||||
connFor: tt.fields.connFor,
|
||||
}
|
||||
if got := r.GetUnusedIP(tt.args.excluding); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Region.GetUnusedIP() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegion_GiveBack(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
addr *net.TCPAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantOk bool
|
||||
availableAfter int
|
||||
}{
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr1},
|
||||
wantOk: true,
|
||||
availableAfter: 1,
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr2},
|
||||
wantOk: false,
|
||||
availableAfter: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := Region{
|
||||
connFor: tt.fields.connFor,
|
||||
}
|
||||
if gotOk := r.GiveBack(tt.args.addr); gotOk != tt.wantOk {
|
||||
t.Errorf("Region.GiveBack() = %v, want %v", gotOk, tt.wantOk)
|
||||
}
|
||||
if tt.availableAfter != r.AvailableAddrs() {
|
||||
t.Errorf("Region.AvailableAddrs() = %v, want %v", r.AvailableAddrs(), tt.availableAfter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegion_GetAnyAddress(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "Sad test -- GetAnyAddress should only fail if the region is empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "Happy test (all addresses unused)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
}},
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "Happy test (GetAnyAddress can still return addresses used by proxy conns)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
&addr0: InUse(2),
|
||||
}},
|
||||
wantNil: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := Region{
|
||||
connFor: tt.fields.connFor,
|
||||
}
|
||||
if got := r.GetAnyAddress(); tt.wantNil != (got == nil) {
|
||||
t.Errorf("Region.GetAnyAddress() = %v, but should it return nil? %v", got, tt.wantNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Regions stores Cloudflare edge network IPs, partitioned into two regions.
|
||||
// This is NOT thread-safe. Users of this package should use it with a lock.
|
||||
type Regions struct {
|
||||
region1 Region
|
||||
region2 Region
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Constructors
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(logger *logrus.Entry) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrLists) < 2 {
|
||||
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(addrLists))
|
||||
}
|
||||
return &Regions{
|
||||
region1: NewRegion(addrLists[0]),
|
||||
region2: NewRegion(addrLists[1]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StaticEdge creates a list of edge addresses from the list of hostnames.
|
||||
// Mainly used for testing connectivity.
|
||||
func StaticEdge(hostnames []string) (*Regions, error) {
|
||||
addrs, err := ResolveAddrs(hostnames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewNoResolve(addrs), nil
|
||||
}
|
||||
|
||||
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
|
||||
// You probably only need this for testing.
|
||||
func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
region1 := make([]*net.TCPAddr, 0)
|
||||
region2 := make([]*net.TCPAddr, 0)
|
||||
for i, v := range addrs {
|
||||
if i%2 == 0 {
|
||||
region1 = append(region1, v)
|
||||
} else {
|
||||
region2 = append(region2, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &Regions{
|
||||
region1: NewRegion(region1),
|
||||
region2: NewRegion(region2),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Methods
|
||||
// ------------------------------------
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the larger region.
|
||||
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
if addr := rs.region1.GetAnyAddress(); addr != nil {
|
||||
return addr
|
||||
}
|
||||
return rs.region2.GetAnyAddress()
|
||||
}
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection.
|
||||
// Returns nil if the connection isn't using an address.
|
||||
func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
if addr := rs.region1.AddrUsedBy(connID); addr != nil {
|
||||
return addr
|
||||
}
|
||||
return rs.region2.AddrUsedBy(connID)
|
||||
}
|
||||
|
||||
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
|
||||
// evenly across both regions.
|
||||
func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAddr {
|
||||
var addr *net.TCPAddr
|
||||
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
|
||||
addr = rs.region1.GetUnusedIP(excluding)
|
||||
rs.region1.Use(addr, connID)
|
||||
} else {
|
||||
addr = rs.region2.GetUnusedIP(excluding)
|
||||
rs.region2.Use(addr, connID)
|
||||
}
|
||||
|
||||
if addr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mark the address as used and return it
|
||||
return addr
|
||||
}
|
||||
|
||||
// AvailableAddrs returns how many edge addresses aren't used.
|
||||
func (rs *Regions) AvailableAddrs() int {
|
||||
return rs.region1.AvailableAddrs() + rs.region2.AvailableAddrs()
|
||||
}
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (rs *Regions) GiveBack(addr *net.TCPAddr) bool {
|
||||
if found := rs.region1.GiveBack(addr); found {
|
||||
return found
|
||||
}
|
||||
return rs.region2.GiveBack(addr)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
)
|
||||
|
||||
func makeRegions() Regions {
|
||||
r1 := NewRegion([]*net.TCPAddr{&addr0, &addr1})
|
||||
r2 := NewRegion([]*net.TCPAddr{&addr2, &addr3})
|
||||
return Regions{region1: r1, region2: r2}
|
||||
}
|
||||
|
||||
func TestRegions_AddrUsedBy(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
addr1 := rs.GetUnusedAddr(nil, 1)
|
||||
assert.Equal(t, addr1, rs.AddrUsedBy(1))
|
||||
addr2 := rs.GetUnusedAddr(nil, 2)
|
||||
assert.Equal(t, addr2, rs.AddrUsedBy(2))
|
||||
addr3 := rs.GetUnusedAddr(nil, 3)
|
||||
assert.Equal(t, addr3, rs.AddrUsedBy(3))
|
||||
}
|
||||
|
||||
func TestRegions_Giveback_Region1(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
rs.region1.Use(&addr0, 0)
|
||||
rs.region1.Use(&addr1, 1)
|
||||
rs.region2.Use(&addr2, 2)
|
||||
rs.region2.Use(&addr3, 3)
|
||||
|
||||
assert.Equal(t, 0, rs.AvailableAddrs())
|
||||
|
||||
rs.GiveBack(&addr0)
|
||||
assert.Equal(t, &addr0, rs.GetUnusedAddr(nil, 3))
|
||||
}
|
||||
func TestRegions_Giveback_Region2(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
rs.region1.Use(&addr0, 0)
|
||||
rs.region1.Use(&addr1, 1)
|
||||
rs.region2.Use(&addr2, 2)
|
||||
rs.region2.Use(&addr3, 3)
|
||||
|
||||
assert.Equal(t, 0, rs.AvailableAddrs())
|
||||
|
||||
rs.GiveBack(&addr2)
|
||||
assert.Equal(t, &addr2, rs.GetUnusedAddr(nil, 2))
|
||||
}
|
||||
|
||||
func TestRegions_GetUnusedAddr_OneAddrLeft(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
|
||||
rs.region1.Use(&addr0, 0)
|
||||
rs.region1.Use(&addr1, 1)
|
||||
rs.region2.Use(&addr2, 2)
|
||||
|
||||
assert.Equal(t, 1, rs.AvailableAddrs())
|
||||
assert.Equal(t, &addr3, rs.GetUnusedAddr(nil, 3))
|
||||
}
|
||||
|
||||
func TestRegions_GetUnusedAddr_Excluding_Region1(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
|
||||
rs.region1.Use(&addr0, 0)
|
||||
rs.region1.Use(&addr1, 1)
|
||||
|
||||
assert.Equal(t, 2, rs.AvailableAddrs())
|
||||
assert.Equal(t, &addr3, rs.GetUnusedAddr(&addr2, 3))
|
||||
}
|
||||
|
||||
func TestRegions_GetUnusedAddr_Excluding_Region2(t *testing.T) {
|
||||
rs := makeRegions()
|
||||
|
||||
rs.region2.Use(&addr2, 0)
|
||||
rs.region2.Use(&addr3, 1)
|
||||
|
||||
assert.Equal(t, 2, rs.AvailableAddrs())
|
||||
assert.Equal(t, &addr1, rs.GetUnusedAddr(&addr0, 1))
|
||||
}
|
||||
|
||||
func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
type args struct {
|
||||
addrs []*net.TCPAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "one address",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0}},
|
||||
},
|
||||
{
|
||||
name: "two addresses",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0, &addr1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
regions := NewNoResolve(tt.args.addrs)
|
||||
RegionsIsBalanced(t, regions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RegionsIsBalanced(t *testing.T, rs *Regions) {
|
||||
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
|
||||
assert.True(t, abs(delta) <= 1)
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
return -x
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package allregions
|
||||
|
||||
type UsedBy struct {
|
||||
ConnID int
|
||||
Used bool
|
||||
}
|
||||
|
||||
func InUse(connID int) UsedBy {
|
||||
return UsedBy{ConnID: connID, Used: true}
|
||||
}
|
||||
|
||||
func Unused() UsedBy {
|
||||
return UsedBy{}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
subsystem = "edgediscovery"
|
||||
)
|
||||
|
||||
var errNoAddressesLeft = fmt.Errorf("There are no free edge addresses left")
|
||||
|
||||
// Edge finds addresses on the Cloudflare edge and hands them out to connections.
|
||||
type Edge struct {
|
||||
regions *allregions.Regions
|
||||
sync.Mutex
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Constructors
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
|
||||
// to connections.
|
||||
func ResolveEdge(l *logrus.Logger) (*Edge, error) {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
regions, err := allregions.ResolveEdge(logger)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
|
||||
func StaticEdge(l *logrus.Logger, hostnames []string) (*Edge, error) {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
regions, err := allregions.StaticEdge(hostnames)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
regions: regions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
|
||||
func MockEdge(l *logrus.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
logger := l.WithField("subsystem", subsystem)
|
||||
regions := allregions.NewNoResolve(addrs)
|
||||
return &Edge{
|
||||
logger: logger,
|
||||
regions: regions,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Methods
|
||||
// ------------------------------------
|
||||
|
||||
// GetAddrForRPC gives this connection an edge Addr.
|
||||
func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
addr := ed.regions.GetAnyAddress()
|
||||
if addr == nil {
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
|
||||
func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
logger := ed.logger.WithFields(logrus.Fields{
|
||||
"connID": connID,
|
||||
"function": "GetAddr",
|
||||
})
|
||||
|
||||
// If this connection has already used an edge addr, return it.
|
||||
if addr := ed.regions.AddrUsedBy(connID); addr != nil {
|
||||
logger.Debug("Returning same address back to proxy connection")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Otherwise, give it an unused one
|
||||
addr := ed.regions.GetUnusedAddr(nil, connID)
|
||||
if addr == nil {
|
||||
logger.Debug("No addresses left to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
logger.Debug("Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
|
||||
func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
logger := ed.logger.WithFields(logrus.Fields{
|
||||
"connID": connID,
|
||||
"function": "GetDifferentAddr",
|
||||
})
|
||||
|
||||
oldAddr := ed.regions.AddrUsedBy(connID)
|
||||
if oldAddr != nil {
|
||||
ed.regions.GiveBack(oldAddr)
|
||||
}
|
||||
addr := ed.regions.GetUnusedAddr(oldAddr, connID)
|
||||
if addr == nil {
|
||||
logger.Debug("No addresses left to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
logger.Debug("Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// AvailableAddrs returns how many unused addresses there are left.
|
||||
func (ed *Edge) AvailableAddrs() int {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
return ed.regions.AvailableAddrs()
|
||||
}
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
ed.logger.WithField("function", "GiveBack").Debug("Address now unused")
|
||||
return ed.regions.GiveBack(addr)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
}
|
||||
)
|
||||
|
||||
func TestGiveBack(t *testing.T) {
|
||||
l := logrus.New()
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
const connID = 0
|
||||
addr, err := edge.GetAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, addr)
|
||||
assert.Equal(t, 3, edge.AvailableAddrs())
|
||||
|
||||
// Get it back
|
||||
edge.GiveBack(addr)
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
}
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
l := logrus.New()
|
||||
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
tunnelConnID := 0
|
||||
|
||||
// Use the IP for a tunnel
|
||||
addrTunnel, err := edge.GetAddr(tunnelConnID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure the IP can be used for RPC too
|
||||
addrRPC, err := edge.GetAddrForRPC()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, addrTunnel, addrRPC)
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
l := logrus.New()
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Get a connection
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
addr, err := edge.GetAddrForRPC()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, addr)
|
||||
|
||||
// Using an address for RPC shouldn't consume it
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
|
||||
// Get it back
|
||||
edge.GiveBack(addr)
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
}
|
||||
|
||||
func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
l := logrus.New()
|
||||
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
addr, err := edge.GetAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, addr)
|
||||
|
||||
// If that edge address is "bad", there's no alternative address.
|
||||
_, err = edge.GetDifferentAddr(connID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNoAddrsLeft(t *testing.T) {
|
||||
l := logrus.New()
|
||||
|
||||
// Make an edge with no addresses
|
||||
edge := MockEdge(l, []*net.TCPAddr{})
|
||||
|
||||
_, err := edge.GetAddr(2)
|
||||
assert.Error(t, err)
|
||||
_, err = edge.GetAddrForRPC()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetAddr(t *testing.T) {
|
||||
l := logrus.New()
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
const connID = 0
|
||||
addr, err := edge.GetAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, addr)
|
||||
|
||||
// If the same connection requests another address, it should get the same one.
|
||||
addr2, err := edge.GetAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, addr, addr2)
|
||||
}
|
||||
|
||||
func TestGetDifferentAddr(t *testing.T) {
|
||||
l := logrus.New()
|
||||
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
const connID = 0
|
||||
addr, err := edge.GetAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, addr)
|
||||
assert.Equal(t, 3, edge.AvailableAddrs())
|
||||
|
||||
// If the same connection requests another address, it should get the same one.
|
||||
addr2, err := edge.GetDifferentAddr(connID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, addr, addr2)
|
||||
assert.Equal(t, 3, edge.AvailableAddrs())
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
module github.com/cloudflare/cloudflared
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/aws/aws-sdk-go v1.25.8
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 // indirect
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
|
||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
||||
github.com/coredns/coredns v1.2.0
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
|
||||
github.com/equinox-io/equinox v1.2.0
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
|
||||
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/google/certificate-transparency-go v1.1.0 // indirect
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kshvakov/clickhouse v1.3.11
|
||||
github.com/lib/pq v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.4
|
||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.11.0
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
|
||||
github.com/miekg/dns v1.1.27
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/opentracing/opentracing-go v1.1.0 // indirect
|
||||
github.com/philhofer/fwd v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.0.0
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
|
||||
github.com/prometheus/common v0.7.0 // indirect
|
||||
github.com/prometheus/procfs v0.0.5 // indirect
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be
|
||||
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
|
||||
google.golang.org/grpc v1.24.0 // indirect
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7
|
||||
)
|
||||
|
||||
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector
|
||||
// have (int, string)
|
||||
// want (prometheus.ProcessCollectorOpts)
|
||||
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v0.9.0-pre1
|
||||
@@ -0,0 +1,455 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
|
||||
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.25.8 h1:n7I+HUUXjun2CsX7JK+1hpRIkZrlKhd3nayeb+Xmavs=
|
||||
github.com/aws/aws-sdk-go v1.25.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk=
|
||||
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 h1:6/yVvBsKeAw05IUj4AzvrxaCnDjN4nUqKjW9+w5wixg=
|
||||
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 h1:QrGfkZDnMxcWHaYDdB7CmqS9i26OAnUj/xcus/abYkY=
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93/go.mod h1:QiTe66jFdP7cUKMCCf/WrvDyYdtdmdZfVcdoLbzaKVY=
|
||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc h1:Dvk3ySBsOm5EviLx6VCyILnafPcQinXGP5jbTdHUJgE=
|
||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc/go.mod h1:HlgKKR8V5a1wroIDDIz3/A+T+9Janfq+7n1P5sEFdi0=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
|
||||
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
|
||||
github.com/coredns/coredns v1.2.0 h1:YEI38K2BJYzL/SxO2tZFD727T/C68DqVWkBQjT0sWPU=
|
||||
github.com/coredns/coredns v1.2.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmqg700pbXhzvmoaXKyfxWRkjRym7Q=
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a h1:W8b4lQ4tFF21aspRGoBuCNV6V2fFJBF+pm1J6OY8Lys=
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0 h1:epsH3lb7KVbXHYk7LYGN5EiE0MxcevHU85CKITJ0wUY=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/equinox-io/equinox v1.2.0 h1:bBS7Ou+Y7Jwgmy8TWSYxEh85WctuFn7FPlgbUzX4DBA=
|
||||
github.com/equinox-io/equinox v1.2.0/go.mod h1:6s3HJB0PYUNgs0mxmI8fHdfVl3TQ25ieA/PVfr+eyVo=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg=
|
||||
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg=
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434 h1:mOp33BLbcbJ8fvTAmZacbBiOASfxN+MLcLxymZCIrGE=
|
||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434/go.mod h1:KigFdumBXUPSwzLDbeuzyt0elrL7+CP7TKuhrhT4bcU=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/kkTFdWhctH96grJ7qiy7bMdiZcIvPKs=
|
||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 h1:gF8ngtda767ddth2SH0YSAhswhz6qUkvyI9EZFYCWJA=
|
||||
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
|
||||
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
|
||||
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
|
||||
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
|
||||
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
|
||||
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
|
||||
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
|
||||
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
|
||||
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
|
||||
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
|
||||
github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
|
||||
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
|
||||
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
|
||||
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
|
||||
github.com/golangci/golangci-lint v1.17.2-0.20190910081718-bad04bb7378f/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=
|
||||
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
|
||||
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
|
||||
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
|
||||
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/certificate-transparency-go v1.1.0 h1:10MlrYzh5wfkToxWI4yJzffsxLfxcEDlOATMx/V9Kzw=
|
||||
github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36jiHGfuSK1fHICIg6sUkRxPAbCs=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA=
|
||||
github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kshvakov/clickhouse v1.3.11 h1:dtzTJY0fCA+MWkLyuKZaNPkmSwdX4gh8+Klic9NB1Lw=
|
||||
github.com/kshvakov/clickhouse v1.3.11/go.mod h1:/SVBAcqF3u7rxQ9sTWCZwf8jzzvxiZGeQvtmSF2BBEc=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b h1:/BbY4n99iMazlr2igipph+hj0MwlZIWpcsP8Iy+na+s=
|
||||
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
|
||||
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
|
||||
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.9.0-pre1 h1:AWTOhsOI9qxeirTuA0A4By/1Es1+y9EcCGY6bBZ2fhM=
|
||||
github.com/prometheus/client_golang v0.9.0-pre1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
|
||||
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
|
||||
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0 h1:6DtWz8hNS4qbq0OCRPhdBMG9E2qKTSDKlwnP3dmZvuA=
|
||||
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0/go.mod h1:A47W3pdWONaZmXuLZgfKLAVgUY0qvfTRM5vVDKS40S4=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190605220351-eb0b1bdb6ae6/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd h1:84VQPzup3IpKLxuIAZjHMhVjJ8fZ4/i3yUnj3k6fUdw=
|
||||
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0 h1:E8PjVFdj/SLDKB0hvb70KTbMbYVHjqztiQdSkIg8E+I=
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0/go.mod h1:fYaTe2FS96wZZwR17YTDHwG+Mw6fmyqJNxN2eNCGPCI=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8 h1:/pLAskKF+d5SawboKd8GB8ew4ClHDbt2c3K9EBFeRGU=
|
||||
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8/go.mod h1:cKXr3E0k4aosgycml1b5z33BVV6hai1Kh7uDgFOkbcs=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
|
||||
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7 h1:CZoOFlTPbKfAShKYrMuUfYbnXexFT1rYRUX1SPnrdE4=
|
||||
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7/go.mod h1:TMGa8HWGJkXiq4nHe9Zu/JgRF5oUtg4XizFC+Vexbec=
|
||||
+37
-20
@@ -3,6 +3,7 @@ package h2mux
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
@@ -12,23 +13,28 @@ type activeStreamMap struct {
|
||||
sync.RWMutex
|
||||
// streams tracks open streams.
|
||||
streams map[uint32]*MuxedStream
|
||||
// streamsEmpty is a chan that should be closed when no more streams are open.
|
||||
streamsEmpty chan struct{}
|
||||
// nextStreamID is the next ID to use on our side of the connection.
|
||||
// This is odd for clients, even for servers.
|
||||
nextStreamID uint32
|
||||
// maxPeerStreamID is the ID of the most recent stream opened by the peer.
|
||||
maxPeerStreamID uint32
|
||||
// activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams
|
||||
activeStreams prometheus.Gauge
|
||||
|
||||
// ignoreNewStreams is true when the connection is being shut down. New streams
|
||||
// cannot be registered.
|
||||
ignoreNewStreams bool
|
||||
// streamsEmpty is a chan that will be closed when no more streams are open.
|
||||
streamsEmptyChan chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func newActiveStreamMap(useClientStreamNumbers bool) *activeStreamMap {
|
||||
func newActiveStreamMap(useClientStreamNumbers bool, activeStreams prometheus.Gauge) *activeStreamMap {
|
||||
m := &activeStreamMap{
|
||||
streams: make(map[uint32]*MuxedStream),
|
||||
streamsEmpty: make(chan struct{}),
|
||||
nextStreamID: 1,
|
||||
streams: make(map[uint32]*MuxedStream),
|
||||
streamsEmptyChan: make(chan struct{}),
|
||||
nextStreamID: 1,
|
||||
activeStreams: activeStreams,
|
||||
}
|
||||
// Client initiated stream uses odd stream ID, server initiated stream uses even stream ID
|
||||
if !useClientStreamNumbers {
|
||||
@@ -37,6 +43,13 @@ func newActiveStreamMap(useClientStreamNumbers bool) *activeStreamMap {
|
||||
return m
|
||||
}
|
||||
|
||||
// This function should be called while `m` is locked.
|
||||
func (m *activeStreamMap) notifyStreamsEmpty() {
|
||||
m.closeOnce.Do(func() {
|
||||
close(m.streamsEmptyChan)
|
||||
})
|
||||
}
|
||||
|
||||
// Len returns the number of active streams.
|
||||
func (m *activeStreamMap) Len() int {
|
||||
m.RLock()
|
||||
@@ -63,6 +76,7 @@ func (m *activeStreamMap) Set(newStream *MuxedStream) bool {
|
||||
return false
|
||||
}
|
||||
m.streams[newStream.streamID] = newStream
|
||||
m.activeStreams.Inc()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -70,31 +84,33 @@ func (m *activeStreamMap) Set(newStream *MuxedStream) bool {
|
||||
func (m *activeStreamMap) Delete(streamID uint32) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
delete(m.streams, streamID)
|
||||
if len(m.streams) == 0 && m.streamsEmpty != nil {
|
||||
close(m.streamsEmpty)
|
||||
m.streamsEmpty = nil
|
||||
if _, ok := m.streams[streamID]; ok {
|
||||
delete(m.streams, streamID)
|
||||
m.activeStreams.Dec()
|
||||
}
|
||||
|
||||
// shutting down, and now the map is empty
|
||||
if m.ignoreNewStreams && len(m.streams) == 0 {
|
||||
m.notifyStreamsEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown blocks new streams from being created. It returns a channel that receives an event
|
||||
// once the last stream has closed, or nil if a shutdown is in progress.
|
||||
func (m *activeStreamMap) Shutdown() <-chan struct{} {
|
||||
// Shutdown blocks new streams from being created.
|
||||
// It returns `done`, a channel that is closed once the last stream has closed
|
||||
// and `progress`, whether a shutdown was already in progress
|
||||
func (m *activeStreamMap) Shutdown() (done <-chan struct{}, alreadyInProgress bool) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.ignoreNewStreams {
|
||||
// already shutting down
|
||||
return nil
|
||||
return m.streamsEmptyChan, true
|
||||
}
|
||||
m.ignoreNewStreams = true
|
||||
done := make(chan struct{})
|
||||
if len(m.streams) == 0 {
|
||||
// nothing to shut down
|
||||
close(done)
|
||||
return done
|
||||
// there are no streams to wait for
|
||||
m.notifyStreamsEmpty()
|
||||
}
|
||||
m.streamsEmpty = done
|
||||
return done
|
||||
return m.streamsEmptyChan, false
|
||||
}
|
||||
|
||||
// AcquireLocalID acquires a new stream ID for a stream you're opening.
|
||||
@@ -162,4 +178,5 @@ func (m *activeStreamMap) Abort() {
|
||||
stream.Close()
|
||||
}
|
||||
m.ignoreNewStreams = true
|
||||
m.notifyStreamsEmpty()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShutdown(t *testing.T) {
|
||||
const numStreams = 1000
|
||||
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
|
||||
|
||||
// Add all the streams
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
go func(streamID int) {
|
||||
defer wg.Done()
|
||||
stream := &MuxedStream{streamID: uint32(streamID)}
|
||||
ok := m.Set(stream)
|
||||
assert.True(t, ok)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
|
||||
|
||||
shutdownChan, alreadyInProgress := m.Shutdown()
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
assert.Fail(t, "before Shutdown(), shutdownChan shouldn't be closed")
|
||||
default:
|
||||
}
|
||||
assert.False(t, alreadyInProgress)
|
||||
|
||||
shutdownChan2, alreadyInProgress2 := m.Shutdown()
|
||||
assert.Equal(t, shutdownChan, shutdownChan2, "repeated calls to Shutdown() should return the same channel")
|
||||
assert.True(t, alreadyInProgress2, "repeated calls to Shutdown() should return true for 'in progress'")
|
||||
|
||||
// Delete all the streams
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
go func(streamID int) {
|
||||
defer wg.Done()
|
||||
m.Delete(uint32(streamID))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
assert.Equal(t, 0, m.Len(), "All the streams should have been deleted")
|
||||
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
default:
|
||||
assert.Fail(t, "After all the streams are deleted, shutdownChan should have been closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyBeforeShutdown(t *testing.T) {
|
||||
const numStreams = 1000
|
||||
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
|
||||
|
||||
// Add all the streams
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
go func(streamID int) {
|
||||
defer wg.Done()
|
||||
stream := &MuxedStream{streamID: uint32(streamID)}
|
||||
ok := m.Set(stream)
|
||||
assert.True(t, ok)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
|
||||
|
||||
// Delete all the streams, bringing m to size 0
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
go func(streamID int) {
|
||||
defer wg.Done()
|
||||
m.Delete(uint32(streamID))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
assert.Equal(t, 0, m.Len(), "All the streams should have been deleted")
|
||||
|
||||
// Add one stream back
|
||||
const soloStreamID = uint32(0)
|
||||
ok := m.Set(&MuxedStream{streamID: soloStreamID})
|
||||
assert.True(t, ok)
|
||||
|
||||
shutdownChan, alreadyInProgress := m.Shutdown()
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
assert.Fail(t, "before Shutdown(), shutdownChan shouldn't be closed")
|
||||
default:
|
||||
}
|
||||
assert.False(t, alreadyInProgress)
|
||||
|
||||
shutdownChan2, alreadyInProgress2 := m.Shutdown()
|
||||
assert.Equal(t, shutdownChan, shutdownChan2, "repeated calls to Shutdown() should return the same channel")
|
||||
assert.True(t, alreadyInProgress2, "repeated calls to Shutdown() should return true for 'in progress'")
|
||||
|
||||
// Remove the remaining stream
|
||||
m.Delete(soloStreamID)
|
||||
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
default:
|
||||
assert.Fail(t, "After all the streams are deleted, shutdownChan should have been closed")
|
||||
}
|
||||
}
|
||||
|
||||
type noopBuffer struct {
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
func (t *noopBuffer) Read(p []byte) (n int, err error) { return len(p), nil }
|
||||
func (t *noopBuffer) Write(p []byte) (n int, err error) { return len(p), nil }
|
||||
func (t *noopBuffer) Reset() {}
|
||||
func (t *noopBuffer) Len() int { return 0 }
|
||||
func (t *noopBuffer) Close() error { t.isClosed = true; return nil }
|
||||
func (t *noopBuffer) Closed() bool { return t.isClosed }
|
||||
|
||||
type noopReadyList struct{}
|
||||
|
||||
func (_ *noopReadyList) Signal(streamID uint32) {}
|
||||
|
||||
func TestAbort(t *testing.T) {
|
||||
const numStreams = 1000
|
||||
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
|
||||
|
||||
var openedStreams sync.Map
|
||||
|
||||
// Add all the streams
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numStreams)
|
||||
for i := 0; i < numStreams; i++ {
|
||||
go func(streamID int) {
|
||||
defer wg.Done()
|
||||
stream := &MuxedStream{
|
||||
streamID: uint32(streamID),
|
||||
readBuffer: &noopBuffer{},
|
||||
writeBuffer: &noopBuffer{},
|
||||
readyList: &noopReadyList{},
|
||||
}
|
||||
ok := m.Set(stream)
|
||||
assert.True(t, ok)
|
||||
|
||||
openedStreams.Store(stream.streamID, stream)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
assert.Equal(t, numStreams, m.Len(), "All the streams should have been added")
|
||||
|
||||
shutdownChan, alreadyInProgress := m.Shutdown()
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
assert.Fail(t, "before Abort(), shutdownChan shouldn't be closed")
|
||||
default:
|
||||
}
|
||||
assert.False(t, alreadyInProgress)
|
||||
|
||||
m.Abort()
|
||||
assert.Equal(t, numStreams, m.Len(), "Abort() shouldn't delete any streams")
|
||||
openedStreams.Range(func(key interface{}, value interface{}) bool {
|
||||
stream := value.(*MuxedStream)
|
||||
readBuffer := stream.readBuffer.(*noopBuffer)
|
||||
writeBuffer := stream.writeBuffer.(*noopBuffer)
|
||||
return assert.True(t, readBuffer.isClosed && writeBuffer.isClosed, "Abort() should have closed all the streams")
|
||||
})
|
||||
|
||||
select {
|
||||
case <-shutdownChan:
|
||||
default:
|
||||
assert.Fail(t, "after Abort(), shutdownChan should have been closed")
|
||||
}
|
||||
|
||||
// multiple aborts shouldn't cause any issues
|
||||
m.Abort()
|
||||
m.Abort()
|
||||
m.Abort()
|
||||
}
|
||||
@@ -542,7 +542,10 @@ func (w *h2DictWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (w *h2DictWriter) Close() error {
|
||||
return w.comp.Close()
|
||||
if w.comp != nil {
|
||||
return w.comp.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From http2/hpack
|
||||
|
||||
+56
-23
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
@@ -19,7 +20,7 @@ const (
|
||||
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size in http2 spec
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultWriteBufferMaxLen int = 1024 * 1024 * 512 // 500mb
|
||||
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
|
||||
|
||||
SettingMuxerMagic http2.SettingID = 0x42db
|
||||
MuxerMagicOrigin uint32 = 0xa2e43c8b
|
||||
@@ -89,10 +90,6 @@ type Muxer struct {
|
||||
compressionQuality CompressionPreset
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
func RPCHeaders() []Header {
|
||||
return []Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
@@ -107,6 +104,7 @@ func Handshake(
|
||||
w io.WriteCloser,
|
||||
r io.ReadCloser,
|
||||
config MuxerConfig,
|
||||
activeStreamsMetrics prometheus.Gauge,
|
||||
) (*Muxer, error) {
|
||||
// Set default config values
|
||||
if config.Timeout == 0 {
|
||||
@@ -130,7 +128,7 @@ func Handshake(
|
||||
newStreamChan: make(chan MuxedStreamRequest),
|
||||
abortChan: make(chan struct{}),
|
||||
readyList: NewReadyList(),
|
||||
streams: newActiveStreamMap(config.IsClient),
|
||||
streams: newActiveStreamMap(config.IsClient, activeStreamsMetrics),
|
||||
}
|
||||
|
||||
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
|
||||
@@ -323,24 +321,51 @@ func joinErrorsWithTimeout(errChan <-chan error, receiveCount int, timeout time.
|
||||
func (m *Muxer) Serve(ctx context.Context) error {
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err := m.muxReader.run(m.config.Logger)
|
||||
m.explicitShutdown.Fuse(false)
|
||||
m.r.Close()
|
||||
m.abort()
|
||||
return err
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
err := m.muxReader.run(m.config.Logger)
|
||||
m.explicitShutdown.Fuse(false)
|
||||
m.r.Close()
|
||||
m.abort()
|
||||
ch <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := m.muxWriter.run(m.config.Logger)
|
||||
m.explicitShutdown.Fuse(false)
|
||||
m.w.Close()
|
||||
m.abort()
|
||||
return err
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
err := m.muxWriter.run(m.config.Logger)
|
||||
m.explicitShutdown.Fuse(false)
|
||||
m.w.Close()
|
||||
m.abort()
|
||||
ch <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := m.muxMetricsUpdater.run(m.config.Logger)
|
||||
return err
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
err := m.muxMetricsUpdater.run(m.config.Logger)
|
||||
ch <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
err := errGroup.Wait()
|
||||
@@ -351,9 +376,11 @@ func (m *Muxer) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Shutdown is called to initiate the "happy path" of muxer termination.
|
||||
func (m *Muxer) Shutdown() {
|
||||
// It blocks new streams from being created.
|
||||
// It returns a channel that is closed when the last stream has been closed.
|
||||
func (m *Muxer) Shutdown() <-chan struct{} {
|
||||
m.explicitShutdown.Fuse(true)
|
||||
m.muxReader.Shutdown()
|
||||
return m.muxReader.Shutdown()
|
||||
}
|
||||
|
||||
// IsUnexpectedTunnelError identifies errors that are expected when shutting down the h2mux tunnel.
|
||||
@@ -388,7 +415,7 @@ func isConnectionClosedError(err error) bool {
|
||||
// Called by proxy server and tunnel
|
||||
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 {
|
||||
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, body)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
|
||||
@@ -397,10 +424,9 @@ func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, nil)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
|
||||
@@ -425,6 +451,13 @@ func (m *Muxer) MakeMuxedStreamRequest(ctx context.Context, request MuxedStreamR
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) CloseStreamRead(stream *MuxedStream) {
|
||||
stream.CloseRead()
|
||||
if stream.WriteClosed() {
|
||||
m.streams.Delete(stream.streamID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) AwaitResponseHeaders(ctx context.Context, stream *MuxedStream) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
+28
-20
@@ -43,7 +43,7 @@ type DefaultMuxerPair struct {
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewDefaultMuxerPair(t assert.TestingT, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: MuxerConfig{
|
||||
@@ -55,6 +55,8 @@ func NewDefaultMuxerPair(t assert.TestingT, f MuxedStreamFunc) *DefaultMuxerPair
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
@@ -65,15 +67,17 @@ func NewDefaultMuxerPair(t assert.TestingT, f MuxedStreamFunc) *DefaultMuxerPair
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
assert.NoError(t, p.Handshake(testName))
|
||||
return p
|
||||
}
|
||||
|
||||
func NewCompressedMuxerPair(t assert.TestingT, quality CompressionSetting, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
func NewCompressedMuxerPair(t assert.TestingT, testName string, quality CompressionSetting, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: MuxerConfig{
|
||||
@@ -83,6 +87,8 @@ func NewCompressedMuxerPair(t assert.TestingT, quality CompressionSetting, f Mux
|
||||
Name: "origin",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
@@ -91,24 +97,26 @@ func NewCompressedMuxerPair(t assert.TestingT, quality CompressionSetting, f Mux
|
||||
Name: "edge",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
assert.NoError(t, p.Handshake(testName))
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Handshake() error {
|
||||
func (p *DefaultMuxerPair) Handshake(testName string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
|
||||
defer cancel()
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() (err error) {
|
||||
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
|
||||
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, NewActiveStreamsMetrics(testName, "edge"))
|
||||
return errors.Wrap(err, "edge handshake failure")
|
||||
})
|
||||
errGroup.Go(func() (err error) {
|
||||
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
|
||||
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, NewActiveStreamsMetrics(testName, "origin"))
|
||||
return errors.Wrap(err, "origin handshake failure")
|
||||
})
|
||||
|
||||
@@ -161,7 +169,7 @@ func TestHandshake(t *testing.T) {
|
||||
f := func(stream *MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
AssertIfPipeReadable(t, muxPair.OriginConn)
|
||||
AssertIfPipeReadable(t, muxPair.EdgeConn)
|
||||
}
|
||||
@@ -191,7 +199,7 @@ func TestSingleStream(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
@@ -262,7 +270,7 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
@@ -309,7 +317,7 @@ func TestMultipleStreams(t *testing.T) {
|
||||
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
maxStreams := 64
|
||||
@@ -402,7 +410,7 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
errGroup, _ := errgroup.WithContext(context.Background())
|
||||
@@ -461,7 +469,7 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
log.Debugf("Handler ends")
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
@@ -516,7 +524,7 @@ func TestUnexpectedShutdown(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
@@ -564,7 +572,7 @@ func EchoHandler(stream *MuxedStream) error {
|
||||
|
||||
func TestOpenAfterDisconnect(t *testing.T) {
|
||||
for i := 0; i < 3; i++ {
|
||||
muxPair := NewDefaultMuxerPair(t, EchoHandler)
|
||||
muxPair := NewDefaultMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), i), EchoHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
switch i {
|
||||
@@ -591,7 +599,7 @@ func TestOpenAfterDisconnect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHPACK(t *testing.T) {
|
||||
muxPair := NewDefaultMuxerPair(t, EchoHandler)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), EchoHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
@@ -724,7 +732,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
|
||||
return nil
|
||||
})
|
||||
muxPair := NewCompressedMuxerPair(t, q, f)
|
||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -918,7 +926,7 @@ func TestSampleSiteWithDictionaries(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
||||
muxPair := NewCompressedMuxerPair(t, q, sampleSiteHandler(files))
|
||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -957,7 +965,7 @@ func TestLongSiteWithDictionaries(t *testing.T) {
|
||||
files, err := loadSampleFiles(paths)
|
||||
assert.NoError(t, err)
|
||||
for q := CompressionNone; q <= CompressionMedium; q++ {
|
||||
muxPair := NewCompressedMuxerPair(t, q, sampleSiteHandler(files))
|
||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
|
||||
muxPair.Serve(t)
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
@@ -998,7 +1006,7 @@ func BenchmarkOpenStream(b *testing.B) {
|
||||
})
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(b, f)
|
||||
muxPair := NewDefaultMuxerPair(b, fmt.Sprintf("%s_%d", b.Name(), i), f)
|
||||
muxPair.Serve(b)
|
||||
b.StartTimer()
|
||||
openStreams(b, muxPair, streams)
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
var headerEncoding = base64.RawStdEncoding
|
||||
|
||||
const (
|
||||
RequestUserHeadersField = "cf-cloudflared-request-headers"
|
||||
ResponseUserHeadersField = "cf-cloudflared-response-headers"
|
||||
)
|
||||
|
||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||
// to an HTTP/1 Request object destined for the local origin web service.
|
||||
// This operation includes conversion of the pseudo-headers into their closest
|
||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
||||
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
switch strings.ToLower(header.Name) {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
// noop - use the preexisting scheme from h1.URL
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
|
||||
// However, this HTTP/1 Request object belongs to the Go standard library,
|
||||
// whose URL package makes some opinionated decisions about the encoding of
|
||||
// URL characters: see the docs of https://godoc.org/net/url#URL,
|
||||
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
|
||||
// which is always used when computing url.URL.String(), whether we'd like it or not.
|
||||
//
|
||||
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
|
||||
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
|
||||
// is treated differently when HTTP_PROXY is set!
|
||||
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
|
||||
//
|
||||
// This means we are subject to the behavior of net/url's function `shouldEscape`
|
||||
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
|
||||
|
||||
if header.Value == "*" {
|
||||
h1.URL.Path = "*"
|
||||
continue
|
||||
}
|
||||
// Due to the behavior of validation.ValidateUrl, h1.URL may
|
||||
// already have a partial value, with or without a trailing slash.
|
||||
base := h1.URL.String()
|
||||
base = strings.TrimRight(base, "/")
|
||||
// But we know :path begins with '/', because we handled '*' above - see RFC7540
|
||||
requestURL, err := url.Parse(base + header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
|
||||
}
|
||||
h1.URL = requestURL
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
case "connection", "upgrade":
|
||||
// for websocket header support
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
default:
|
||||
// Ignore any other header;
|
||||
// User headers will be read from `RequestUserHeadersField`
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Find and parse user headers serialized into a single one
|
||||
userHeaders, err := ParseUserHeaders(RequestUserHeadersField, h2)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse user headers")
|
||||
}
|
||||
for _, userHeader := range userHeaders {
|
||||
h1.Header.Add(http.CanonicalHeaderKey(userHeader.Name), userHeader.Value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header, error) {
|
||||
for _, header := range headers {
|
||||
if header.Name == headerNameToParseFrom {
|
||||
return DeserializeHeaders(header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%v header not found", RequestUserHeadersField)
|
||||
}
|
||||
|
||||
func IsControlHeader(headerName string) bool {
|
||||
headerName = strings.ToLower(headerName)
|
||||
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func IsWebsocketClientHeader(headerName string) bool {
|
||||
headerName = strings.ToLower(headerName)
|
||||
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
headerName == "connection" ||
|
||||
headerName == "upgrade"
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
h2 = []Header{
|
||||
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
||||
}
|
||||
userHeaders := http.Header{}
|
||||
for header, values := range h1.Header {
|
||||
for _, value := range values {
|
||||
if strings.ToLower(header) == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
|
||||
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
if _, ok := userHeaders[header]; ok {
|
||||
userHeaders[header] = append(userHeaders[header], value)
|
||||
} else {
|
||||
userHeaders[header] = []string{value}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
|
||||
|
||||
return h2
|
||||
}
|
||||
|
||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||
// and then joining them in the format of [key:value;]
|
||||
func SerializeHeaders(h1Headers http.Header) string {
|
||||
var serializedHeaders []string
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
|
||||
headerEncoding.Encode(encodedName, []byte(headerName))
|
||||
|
||||
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
|
||||
headerEncoding.Encode(encodedValue, []byte(headerValue))
|
||||
|
||||
serializedHeaders = append(
|
||||
serializedHeaders,
|
||||
strings.Join(
|
||||
[]string{string(encodedName), string(encodedValue)},
|
||||
":",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(serializedHeaders, ";")
|
||||
}
|
||||
|
||||
// Deserialize headers serialized by `SerializeHeader`
|
||||
func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
|
||||
const unableToDeserializeErr = "Unable to deserialize headers"
|
||||
|
||||
var deserialized []Header
|
||||
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
|
||||
if len(serializedPair) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
serializedHeaderParts := strings.Split(serializedPair, ":")
|
||||
if len(serializedHeaderParts) != 2 {
|
||||
return nil, errors.New(unableToDeserializeErr)
|
||||
}
|
||||
|
||||
serializedName := serializedHeaderParts[0]
|
||||
serializedValue := serializedHeaderParts[1]
|
||||
deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
|
||||
deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
|
||||
|
||||
if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
|
||||
deserialized = append(deserialized, Header{
|
||||
Name: string(deserializedName),
|
||||
Value: string(deserializedValue),
|
||||
})
|
||||
}
|
||||
|
||||
return deserialized, nil
|
||||
}
|
||||
|
||||
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
|
||||
var serializedHeaderChunks []string
|
||||
for _, headerChunk := range headers {
|
||||
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
|
||||
}
|
||||
|
||||
return []Header{{
|
||||
headersField,
|
||||
strings.Join(serializedHeaderChunks, ";"),
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type ByName []Header
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool {
|
||||
if a[i].Name == a[j].Name {
|
||||
return a[i].Value < a[j].Value
|
||||
}
|
||||
|
||||
return a[i].Name < a[j].Name
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockHeaders := http.Header{
|
||||
"Mock header 1": {"Mock value 1"},
|
||||
"Mock header 2": {"Mock value 2"},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]Header{{
|
||||
RequestUserHeadersField,
|
||||
SerializeHeaders(http.Header{}),
|
||||
}},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(http.Header{}, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
{Name: ":path", Value: "//bad_path/"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
{Name: ":path", Value: "/?query=mock%20value"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
{Name: ":path", Value: "/mock%20path"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
|
||||
type testCase struct {
|
||||
path string
|
||||
want string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
path: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
path: "//",
|
||||
want: "//",
|
||||
},
|
||||
{
|
||||
path: "/test",
|
||||
want: "/test",
|
||||
},
|
||||
{
|
||||
path: "//test",
|
||||
want: "//test",
|
||||
},
|
||||
{
|
||||
// https://github.com/cloudflare/cloudflared/issues/81
|
||||
path: "//test/",
|
||||
want: "//test/",
|
||||
},
|
||||
{
|
||||
path: "/%2Ftest",
|
||||
want: "/%2Ftest",
|
||||
},
|
||||
{
|
||||
path: "//%20test",
|
||||
want: "//%20test",
|
||||
},
|
||||
{
|
||||
// https://github.com/cloudflare/cloudflared/issues/124
|
||||
path: "/test?get=somthing%20a",
|
||||
want: "/test?get=somthing%20a",
|
||||
},
|
||||
{
|
||||
path: "/%20",
|
||||
want: "/%20",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode ' '
|
||||
path: "/ ",
|
||||
want: "/%20",
|
||||
},
|
||||
{
|
||||
path: "/ a ",
|
||||
want: "/%20a%20",
|
||||
},
|
||||
{
|
||||
path: "/a%20b",
|
||||
want: "/a%20b",
|
||||
},
|
||||
{
|
||||
path: "/foo/bar;param?query#frag",
|
||||
want: "/foo/bar;param?query#frag",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
|
||||
path: "/a␠b",
|
||||
want: "/a%E2%90%A0b",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-ä",
|
||||
want: "/a-umlaut-%C3%A4",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-%C3%A4",
|
||||
want: "/a-umlaut-%C3%A4",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-%c3%a4",
|
||||
want: "/a-umlaut-%c3%a4",
|
||||
},
|
||||
{
|
||||
// here the second '#' is treated as part of the fragment
|
||||
path: "/a#b#c",
|
||||
want: "/a#b%23c",
|
||||
},
|
||||
{
|
||||
path: "/a#b␠c",
|
||||
want: "/a#b%E2%90%A0c",
|
||||
},
|
||||
{
|
||||
path: "/a#b%20c",
|
||||
want: "/a#b%20c",
|
||||
},
|
||||
{
|
||||
path: "/a#b c",
|
||||
want: "/a#b%20c",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode '\'
|
||||
path: "/\\",
|
||||
want: "/%5C",
|
||||
},
|
||||
{
|
||||
path: "/a\\",
|
||||
want: "/a%5C",
|
||||
},
|
||||
{
|
||||
path: "/a,b.c.",
|
||||
want: "/a,b.c.",
|
||||
},
|
||||
{
|
||||
path: "/.",
|
||||
want: "/.",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode '`'
|
||||
path: "/a`",
|
||||
want: "/a%60",
|
||||
},
|
||||
{
|
||||
path: "/a[0]",
|
||||
want: "/a[0]",
|
||||
},
|
||||
{
|
||||
path: "/?a[0]=5 &b[]=",
|
||||
want: "/?a[0]=5 &b[]=",
|
||||
},
|
||||
{
|
||||
path: "/?a=%22b%20%22",
|
||||
want: "/?a=%22b%20%22",
|
||||
},
|
||||
}
|
||||
|
||||
for index, testCase := range testCases {
|
||||
requestURL := "https://example.com"
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []Header{
|
||||
{Name: ":path", Value: testCase.path},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
assert.NoError(t, headersConversionErr)
|
||||
|
||||
assert.Equal(t,
|
||||
http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
},
|
||||
request.Header)
|
||||
|
||||
assert.Equal(t,
|
||||
"https://example.com"+testCase.want,
|
||||
request.URL.String(),
|
||||
"Failed URL index: %v %#v", index, testCase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
|
||||
config := &quick.Config{
|
||||
Values: func(args []reflect.Value, rand *rand.Rand) {
|
||||
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
|
||||
},
|
||||
}
|
||||
|
||||
type testOrigin struct {
|
||||
url string
|
||||
|
||||
expectedScheme string
|
||||
expectedBasePath string
|
||||
}
|
||||
testOrigins := []testOrigin{
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/api",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/api/",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
{
|
||||
url: "https://origin.hostname.example.com:8080/api",
|
||||
expectedScheme: "https",
|
||||
expectedBasePath: "https://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
}
|
||||
|
||||
// use multiple schemes to demonstrate that the URL is based on the
|
||||
// origin's scheme, not the :scheme header
|
||||
for _, testScheme := range []string{"http", "https"} {
|
||||
for _, testOrigin := range testOrigins {
|
||||
assertion := func(testPath string) bool {
|
||||
const expectedMethod = "POST"
|
||||
const expectedHostname = "request.hostname.example.com"
|
||||
|
||||
h2 := []Header{
|
||||
{Name: ":method", Value: expectedMethod},
|
||||
{Name: ":scheme", Value: testScheme},
|
||||
{Name: ":authority", Value: expectedHostname},
|
||||
{Name: ":path", Value: testPath},
|
||||
{Name: RequestUserHeadersField, Value: ""},
|
||||
}
|
||||
h1, err := http.NewRequest("GET", testOrigin.url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = H2RequestHeadersToH1Request(h2, h1)
|
||||
return assert.NoError(t, err) &&
|
||||
assert.Equal(t, expectedMethod, h1.Method) &&
|
||||
assert.Equal(t, expectedHostname, h1.Host) &&
|
||||
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
|
||||
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
|
||||
}
|
||||
err := quick.Check(assertion, config)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomASCIIPrintableChar(rand *rand.Rand) int {
|
||||
// smallest printable ASCII char is 32, largest is 126
|
||||
const startPrintable = 32
|
||||
const endPrintable = 127
|
||||
return startPrintable + rand.Intn(endPrintable-startPrintable)
|
||||
}
|
||||
|
||||
// randomASCIIText generates an ASCII string, some of whose characters may be
|
||||
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
|
||||
// between 1 and `maxLength`.
|
||||
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
|
||||
length := minLength + rand.Intn(maxLength)
|
||||
result := ""
|
||||
for i := 0; i < length; i++ {
|
||||
c := randomASCIIPrintableChar(rand)
|
||||
|
||||
// 1/4 chance of using percent encoding when not necessary
|
||||
if c == '%' || rand.Intn(4) == 0 {
|
||||
result += fmt.Sprintf("%%%02X", c)
|
||||
} else {
|
||||
result += string(c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL path,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
re, err := regexp.Compile("[^/;,]*")
|
||||
require.NoError(t, err)
|
||||
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL query,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
return "?" + strings.ReplaceAll(text, "#", "%23")
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
u, err := url.Parse("#" + text)
|
||||
require.NoError(t, err)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
|
||||
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
|
||||
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
|
||||
result := randomHTTP1Path(t, rand, 1, 64)
|
||||
if rand.Intn(2) == 1 {
|
||||
result += randomHTTP1Query(rand, 1, 32)
|
||||
}
|
||||
if rand.Intn(2) == 1 {
|
||||
result += randomHTTP1Fragment(t, rand, 1, 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []Header) {
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
h2muxHeaders = append(h2muxHeaders, Header{name, value})
|
||||
}
|
||||
}
|
||||
|
||||
return h2muxHeaders
|
||||
}
|
||||
|
||||
func TestSerializeHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockHeaders := http.Header{
|
||||
"Mock-Header-One": {"Mock header one value", "three"},
|
||||
"Mock-Header-Two-Long": {"Mock header two value\nlong"},
|
||||
":;": {":;", ";:"},
|
||||
":": {":"},
|
||||
";": {";"},
|
||||
";;": {";;"},
|
||||
"Empty values": {"", ""},
|
||||
"": {"Empty key"},
|
||||
"control\tcharacter\b\n": {"value\n\b\t"},
|
||||
";\v:": {":\v;"},
|
||||
}
|
||||
|
||||
for header, values := range mockHeaders {
|
||||
for _, value := range values {
|
||||
// Note that Golang's http library is opinionated;
|
||||
// at this point every header name will be title-cased in order to comply with the HTTP RFC
|
||||
// This means our proxy is not completely transparent when it comes to proxying headers
|
||||
request.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
serializedHeaders := SerializeHeaders(request.Header)
|
||||
|
||||
// Sanity check: the headers serialized to something that's not an empty string
|
||||
assert.NotEqual(t, "", serializedHeaders)
|
||||
|
||||
// Deserialize back, and ensure we get the same set of headers
|
||||
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 13, len(deserializedHeaders))
|
||||
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders)
|
||||
|
||||
sort.Sort(ByName(deserializedHeaders))
|
||||
sort.Sort(ByName(h2muxExpectedHeaders))
|
||||
|
||||
assert.True(
|
||||
t,
|
||||
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders),
|
||||
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
func TestSerializeNoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
serializedHeaders := SerializeHeaders(request.Header)
|
||||
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(deserializedHeaders))
|
||||
}
|
||||
|
||||
func TestDeserializeMalformed(t *testing.T) {
|
||||
var err error
|
||||
|
||||
malformedData := []string{
|
||||
"malformed data",
|
||||
"bW9jawo=", // "mock"
|
||||
"bW9jawo=:ZGF0YQo=:bW9jawo=", // "mock:data:mock"
|
||||
"::",
|
||||
}
|
||||
|
||||
for _, malformedValue := range malformedData {
|
||||
_, err = DeserializeHeaders(malformedValue)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHeaders(t *testing.T) {
|
||||
mockUserHeadersToSerialize := http.Header{
|
||||
"Mock-Header-One": {"1", "1.5"},
|
||||
"Mock-Header-Two": {"2"},
|
||||
"Mock-Header-Three": {"3"},
|
||||
}
|
||||
|
||||
mockHeaders := []Header{
|
||||
{Name: "One", Value: "1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||
}
|
||||
|
||||
expectedHeaders := []Header{
|
||||
{Name: "Mock-Header-One", Value: "1"},
|
||||
{Name: "Mock-Header-One", Value: "1.5"},
|
||||
{Name: "Mock-Header-Two", Value: "2"},
|
||||
{Name: "Mock-Header-Three", Value: "3"},
|
||||
}
|
||||
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
|
||||
}
|
||||
|
||||
func TestParseHeadersNoSerializedHeader(t *testing.T) {
|
||||
mockHeaders := []Header{
|
||||
{Name: "One", Value: "1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
}
|
||||
|
||||
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
|
||||
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
|
||||
}
|
||||
|
||||
func TestIsControlHeader(t *testing.T) {
|
||||
controlHeaders := []string{
|
||||
// Anything that begins with cf-
|
||||
"cf-sample-header",
|
||||
"CF-SAMPLE-HEADER",
|
||||
"Cf-Sample-Header",
|
||||
|
||||
// Any http2 pseudoheader
|
||||
":sample-pseudo-header",
|
||||
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
}
|
||||
|
||||
for _, header := range controlHeaders {
|
||||
assert.True(t, IsControlHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlHeader(t *testing.T) {
|
||||
notControlHeaders := []string{
|
||||
"Mock-header",
|
||||
"Another-sample-header",
|
||||
}
|
||||
|
||||
for _, header := range notControlHeaders {
|
||||
assert.False(t, IsControlHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
mockHeaders := http.Header{
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"Content-Length": {"123"},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
Header: mockHeaders,
|
||||
}
|
||||
|
||||
headers := H1ResponseToH2ResponseHeaders(&mockResponse)
|
||||
|
||||
serializedHeadersIndex := -1
|
||||
for i, header := range headers {
|
||||
if header.Name == ResponseUserHeadersField {
|
||||
serializedHeadersIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotEqual(t, -1, serializedHeadersIndex)
|
||||
actualControlHeaders := append(
|
||||
headers[:serializedHeadersIndex],
|
||||
headers[serializedHeadersIndex+1:]...,
|
||||
)
|
||||
expectedControlHeaders := []Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: "content-length", Value: "123"},
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
|
||||
|
||||
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
|
||||
expectedUserHeaders := []Header{
|
||||
{Name: "User-header-one", Value: ""},
|
||||
{Name: "User-header-two", Value: "1"},
|
||||
{Name: "User-header-two", Value: "2"},
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
|
||||
}
|
||||
|
||||
// The purpose of this test is to check that our code and the http.Header
|
||||
// implementation don't throw validation errors about header size
|
||||
func TestHeaderSize(t *testing.T) {
|
||||
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
|
||||
largeHeaders := http.Header{
|
||||
"User-header": {largeValue},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
Header: largeHeaders,
|
||||
}
|
||||
|
||||
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
|
||||
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
for _, header := range serializedHeaders {
|
||||
request.Header.Set(header.Name, header.Value)
|
||||
}
|
||||
|
||||
for _, header := range serializedHeaders {
|
||||
if header.Name != ResponseUserHeadersField {
|
||||
continue
|
||||
}
|
||||
|
||||
deserializedHeaders, err := DeserializeHeaders(header.Value)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func randSeq(n int) string {
|
||||
randomizer := rand.New(rand.NewSource(17))
|
||||
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[randomizer.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
+41
-12
@@ -17,6 +17,12 @@ type ReadWriteClosedCloser interface {
|
||||
Closed() bool
|
||||
}
|
||||
|
||||
// MuxedStreamDataSignaller is a write-only *ReadyList
|
||||
type MuxedStreamDataSignaller interface {
|
||||
// Non-blocking: call this when data is ready to be sent for the given stream ID.
|
||||
Signal(ID uint32)
|
||||
}
|
||||
|
||||
// MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data.
|
||||
type MuxedStream struct {
|
||||
streamID uint32
|
||||
@@ -55,8 +61,8 @@ type MuxedStream struct {
|
||||
// This is the amount of bytes that are in the peer's receive window
|
||||
// (how much data we can send from this stream).
|
||||
sendWindow uint32
|
||||
// Reference to the muxer's readyList; signal this for stream data to be sent.
|
||||
readyList *ReadyList
|
||||
// The muxer's readyList
|
||||
readyList MuxedStreamDataSignaller
|
||||
// The headers that should be sent, and a flag so we only send them once.
|
||||
headersSent bool
|
||||
writeHeaders []Header
|
||||
@@ -88,7 +94,7 @@ func (th TunnelHostname) IsSet() bool {
|
||||
return th != ""
|
||||
}
|
||||
|
||||
func NewStream(config MuxerConfig, writeHeaders []Header, readyList *ReadyList, dictionaries h2Dictionaries) *MuxedStream {
|
||||
func NewStream(config MuxerConfig, writeHeaders []Header, readyList MuxedStreamDataSignaller, dictionaries h2Dictionaries) *MuxedStream {
|
||||
return &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
@@ -137,9 +143,10 @@ func (s *MuxedStream) Write(p []byte) (int, error) {
|
||||
// If the buffer is full, block till there is more room.
|
||||
// Use a loop to recheck the buffer size after the lock is reacquired.
|
||||
for s.writeBufferMaxLen <= s.writeBuffer.Len() {
|
||||
s.writeLock.Unlock()
|
||||
<-s.writeBufferHasSpace
|
||||
s.writeLock.Lock()
|
||||
s.awaitWriteBufferHasSpace()
|
||||
if s.writeEOF {
|
||||
return totalWritten, io.EOF
|
||||
}
|
||||
}
|
||||
amountToWrite := len(p) - totalWritten
|
||||
spaceAvailable := s.writeBufferMaxLen - s.writeBuffer.Len()
|
||||
@@ -188,10 +195,19 @@ func (s *MuxedStream) CloseWrite() error {
|
||||
if c, ok := s.writeBuffer.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
// Allow MuxedStream::Write() to terminate its loop with err=io.EOF, if needed
|
||||
s.notifyWriteBufferHasSpace()
|
||||
// We need to send something over the wire, even if it's an END_STREAM with no data
|
||||
s.writeNotify()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MuxedStream) WriteClosed() bool {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
return s.writeEOF
|
||||
}
|
||||
|
||||
func (s *MuxedStream) WriteHeaders(headers []Header) error {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
@@ -232,6 +248,23 @@ func (s *MuxedStream) TunnelHostname() TunnelHostname {
|
||||
return s.tunnelHostname
|
||||
}
|
||||
|
||||
// Block until a value is sent on writeBufferHasSpace.
|
||||
// Must be called while holding writeLock
|
||||
func (s *MuxedStream) awaitWriteBufferHasSpace() {
|
||||
s.writeLock.Unlock()
|
||||
<-s.writeBufferHasSpace
|
||||
s.writeLock.Lock()
|
||||
}
|
||||
|
||||
// Send a value on writeBufferHasSpace without blocking.
|
||||
// Must be called while holding writeLock
|
||||
func (s *MuxedStream) notifyWriteBufferHasSpace() {
|
||||
select {
|
||||
case s.writeBufferHasSpace <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MuxedStream) getReceiveWindow() uint32 {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
@@ -351,17 +384,13 @@ func (s *MuxedStream) getChunk() *streamChunk {
|
||||
sendData: !s.sentEOF,
|
||||
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
|
||||
}
|
||||
|
||||
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
|
||||
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
|
||||
s.sendWindow -= uint32(writeLen)
|
||||
|
||||
// Non-blocking channel send. This will allow MuxedStream::Write() to continue, if needed
|
||||
// Allow MuxedStream::Write() to continue, if needed
|
||||
if s.writeBuffer.Len() < s.writeBufferMaxLen {
|
||||
select {
|
||||
case s.writeBufferHasSpace <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
s.notifyWriteBufferHasSpace()
|
||||
}
|
||||
|
||||
// When we write the chunk, we'll write the WINDOW_UPDATE frame if needed
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-collections/collections/queue"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -299,3 +300,14 @@ func (r *rate) get() (curr, min, max uint64) {
|
||||
defer r.lock.RUnlock()
|
||||
return r.curr, r.min, r.max
|
||||
}
|
||||
|
||||
func NewActiveStreamsMetrics(namespace, subsystem string) prometheus.Gauge {
|
||||
activeStreams := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "active_streams",
|
||||
Help: "Number of active streams created by all muxers.",
|
||||
})
|
||||
prometheus.MustRegister(activeStreams)
|
||||
return activeStreams
|
||||
}
|
||||
|
||||
+16
-5
@@ -51,10 +51,12 @@ type MuxReader struct {
|
||||
dictionaries h2Dictionaries
|
||||
}
|
||||
|
||||
func (r *MuxReader) Shutdown() {
|
||||
done := r.streams.Shutdown()
|
||||
if done == nil {
|
||||
return
|
||||
// Shutdown blocks new streams from being created.
|
||||
// It returns a channel that is closed once the last stream has closed.
|
||||
func (r *MuxReader) Shutdown() <-chan struct{} {
|
||||
done, alreadyInProgress := r.streams.Shutdown()
|
||||
if alreadyInProgress {
|
||||
return done
|
||||
}
|
||||
r.sendGoAway(http2.ErrCodeNo)
|
||||
go func() {
|
||||
@@ -62,6 +64,7 @@ func (r *MuxReader) Shutdown() {
|
||||
<-done
|
||||
r.r.Close()
|
||||
}()
|
||||
return done
|
||||
}
|
||||
|
||||
func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
@@ -94,7 +97,12 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
switch e := err.(type) {
|
||||
case http2.StreamError:
|
||||
errLogger.Warn("stream error")
|
||||
r.streamError(e.StreamID, e.Code)
|
||||
// Ideally we wouldn't return here, since that aborts the muxer.
|
||||
// We should communicate the error to the relevant MuxedStream
|
||||
// data structure, so that callers of MuxedStream.Read() and
|
||||
// MuxedStream.Write() would see it. Then we could `continue`
|
||||
// and keep the muxer going.
|
||||
return r.streamError(e.StreamID, e.Code)
|
||||
case http2.ConnectionError:
|
||||
errLogger.Warn("connection error")
|
||||
return r.connectionError(err)
|
||||
@@ -125,6 +133,9 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
if streamID == 0 {
|
||||
return ErrInvalidStream
|
||||
}
|
||||
if stream, ok := r.streams.Get(streamID); ok {
|
||||
stream.Close()
|
||||
}
|
||||
r.streams.Delete(streamID)
|
||||
case *http2.PingFrame:
|
||||
r.receivePingData(f)
|
||||
|
||||
@@ -59,7 +59,7 @@ func assertOpenStreamSucceed(t *testing.T, stream *MuxedStream, err error) {
|
||||
|
||||
func TestMissingHeaders(t *testing.T) {
|
||||
originHandler := &mockOriginStreamHandler{}
|
||||
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), originHandler.ServeStream)
|
||||
muxPair.Serve(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
@@ -83,7 +83,7 @@ func TestMissingHeaders(t *testing.T) {
|
||||
|
||||
func TestReceiveHeaderData(t *testing.T) {
|
||||
originHandler := &mockOriginStreamHandler{}
|
||||
muxPair := NewDefaultMuxerPair(t, originHandler.ServeStream)
|
||||
muxPair := NewDefaultMuxerPair(t, t.Name(), originHandler.ServeStream)
|
||||
muxPair.Serve(t)
|
||||
|
||||
reqHeaders := []Header{
|
||||
|
||||
+48
-17
@@ -54,6 +54,13 @@ type MuxedStreamRequest struct {
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func NewMuxedStreamRequest(stream *MuxedStream, body io.Reader) MuxedStreamRequest {
|
||||
return MuxedStreamRequest{
|
||||
stream: stream,
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MuxedStreamRequest) flushBody() {
|
||||
io.Copy(r.stream, r.body)
|
||||
r.stream.CloseWrite()
|
||||
@@ -243,28 +250,52 @@ func (w *MuxWriter) encodeHeaders(headers []Header) ([]byte, error) {
|
||||
// writeHeaders writes a block of encoded headers, splitting it into multiple frames if necessary.
|
||||
func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error {
|
||||
encodedHeaders, err := w.encodeHeaders(headers)
|
||||
if err != nil {
|
||||
if err != nil || len(encodedHeaders) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
blockSize := int(w.maxFrameSize)
|
||||
endHeaders := len(encodedHeaders) == 0
|
||||
for !endHeaders && err == nil {
|
||||
blockFragment := encodedHeaders
|
||||
if len(encodedHeaders) > blockSize {
|
||||
blockFragment = blockFragment[:blockSize]
|
||||
encodedHeaders = encodedHeaders[blockSize:]
|
||||
// Send CONTINUATION frame if the headers can't be fit into 1 frame
|
||||
err = w.f.WriteContinuation(streamID, endHeaders, blockFragment)
|
||||
} else {
|
||||
endHeaders = true
|
||||
err = w.f.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: streamID,
|
||||
EndHeaders: endHeaders,
|
||||
BlockFragment: blockFragment,
|
||||
})
|
||||
// CONTINUATION is unnecessary; the headers fit within the blockSize
|
||||
if len(encodedHeaders) < blockSize {
|
||||
return w.f.WriteHeaders(http2.HeadersFrameParam{
|
||||
StreamID: streamID,
|
||||
EndHeaders: true,
|
||||
BlockFragment: encodedHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
choppedHeaders := chopEncodedHeaders(encodedHeaders, blockSize)
|
||||
// len(choppedHeaders) is at least 2
|
||||
if err := w.f.WriteHeaders(http2.HeadersFrameParam{StreamID: streamID, EndHeaders: false, BlockFragment: choppedHeaders[0]}); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 1; i < len(choppedHeaders)-1; i++ {
|
||||
if err := w.f.WriteContinuation(streamID, false, choppedHeaders[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
if err := w.f.WriteContinuation(streamID, true, choppedHeaders[len(choppedHeaders)-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Partition a slice of bytes into `len(slice) / blockSize` slices of length `blockSize`
|
||||
func chopEncodedHeaders(headers []byte, chunkSize int) [][]byte {
|
||||
var divided [][]byte
|
||||
|
||||
for i := 0; i < len(headers); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
|
||||
if end > len(headers) {
|
||||
end = len(headers)
|
||||
}
|
||||
|
||||
divided = append(divided, headers[i:end])
|
||||
}
|
||||
|
||||
return divided
|
||||
}
|
||||
|
||||
func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package h2mux
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestChopEncodedHeaders(t *testing.T) {
|
||||
mockEncodedHeaders := make([]byte, 5)
|
||||
for i := range mockEncodedHeaders {
|
||||
mockEncodedHeaders[i] = byte(i)
|
||||
}
|
||||
chopped := chopEncodedHeaders(mockEncodedHeaders, 4)
|
||||
|
||||
assert.Equal(t, 2, len(chopped))
|
||||
assert.Equal(t, []byte{0, 1, 2, 3}, chopped[0])
|
||||
assert.Equal(t, []byte{4}, chopped[1])
|
||||
}
|
||||
|
||||
func TestChopEncodedEmptyHeaders(t *testing.T) {
|
||||
mockEncodedHeaders := make([]byte, 0)
|
||||
chopped := chopEncodedHeaders(mockEncodedHeaders, 3)
|
||||
|
||||
assert.Equal(t, 0, len(chopped))
|
||||
}
|
||||
@@ -80,7 +80,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -128,7 +128,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
@@ -113,7 +113,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -161,7 +161,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
@@ -109,7 +109,7 @@ ghost.init({
|
||||
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
|
||||
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
|
||||
<meta class="swiftype" name="language" data-type="string" content="en" />
|
||||
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
|
||||
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
|
||||
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
|
||||
<script>
|
||||
var trackRecruitingLink = function(role, url) {
|
||||
@@ -157,7 +157,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
|
||||
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
+6
-5
@@ -56,7 +56,7 @@ const indexTemplate = `
|
||||
<path class="st2" d="M93.6 12.8h-.5c-.1 0-.2.1-.3.2l-.7 2.4c-.3 1-.2 2 .3 2.6.5.6 1.2 1 2.1 1.1l3.7.2c.1 0 .2.1.3.1.1.1.1.2 0 .3-.1.2-.2.3-.4.3l-3.8.2c-2.1.1-4.3 1.8-5.1 3.8l-.2.9c-.1.1 0 .3.2.3h13.2c.2 0 .3-.1.3-.3.2-.8.4-1.7.4-2.6 0-5.2-4.3-9.5-9.5-9.5"/>
|
||||
<path class="st3" d="M104.4 30.8c-.5 0-.9-.4-.9-.9s.4-.9.9-.9.9.4.9.9-.4.9-.9.9m0-1.6c-.4 0-.7.3-.7.7 0 .4.3.7.7.7.4 0 .7-.3.7-.7 0-.4-.3-.7-.7-.7m.4 1.2h-.2l-.2-.3h-.2v.3h-.2v-.9h.5c.2 0 .3.1.3.3 0 .1-.1.2-.2.3l.2.3zm-.3-.5c.1 0 .1 0 .1-.1s-.1-.1-.1-.1h-.3v.3h.3zM14.8 29H17v6h3.8v1.9h-6zM23.1 32.9c0-2.3 1.8-4.1 4.3-4.1s4.2 1.8 4.2 4.1-1.8 4.1-4.3 4.1c-2.4 0-4.2-1.8-4.2-4.1m6.3 0c0-1.2-.8-2.2-2-2.2s-2 1-2 2.1.8 2.1 2 2.1c1.2.2 2-.8 2-2M34.3 33.4V29h2.2v4.4c0 1.1.6 1.7 1.5 1.7s1.5-.5 1.5-1.6V29h2.2v4.4c0 2.6-1.5 3.7-3.7 3.7-2.3-.1-3.7-1.2-3.7-3.7M45 29h3.1c2.8 0 4.5 1.6 4.5 3.9s-1.7 4-4.5 4h-3V29zm3.1 5.9c1.3 0 2.2-.7 2.2-2s-.9-2-2.2-2h-.9v4h.9zM55.7 29H62v1.9h-4.1v1.3h3.7V34h-3.7v2.9h-2.2zM65.1 29h2.2v6h3.8v1.9h-6zM76.8 28.9H79l3.4 8H80l-.6-1.4h-3.1l-.6 1.4h-2.3l3.4-8zm2 4.9l-.9-2.2-.9 2.2h1.8zM85.2 29h3.7c1.2 0 2 .3 2.6.9.5.5.7 1.1.7 1.8 0 1.2-.6 2-1.6 2.4l1.9 2.8H90l-1.6-2.4h-1v2.4h-2.2V29zm3.6 3.8c.7 0 1.2-.4 1.2-.9 0-.6-.5-.9-1.2-.9h-1.4v1.9h1.4zM95.3 29h6.4v1.8h-4.2V32h3.8v1.8h-3.8V35h4.3v1.9h-6.5zM10 33.9c-.3.7-1 1.2-1.8 1.2-1.2 0-2-1-2-2.1s.8-2.1 2-2.1c.9 0 1.6.6 1.9 1.3h2.3c-.4-1.9-2-3.3-4.2-3.3-2.4 0-4.3 1.8-4.3 4.1s1.8 4.1 4.2 4.1c2.1 0 3.7-1.4 4.2-3.2H10z"/>
|
||||
</svg>
|
||||
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created your first tunnel!</h1>
|
||||
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created a tunnel!</h1>
|
||||
<p class="f6 f5-m f4-l measure lh-copy fw3">
|
||||
Argo Tunnel exposes locally running applications to the internet by
|
||||
running an encrypted, virtual tunnel from your laptop or server to
|
||||
@@ -103,15 +103,16 @@ func StartHelloWorldServer(logger *logrus.Logger, listener net.Listener, shutdow
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Addr: listener.Addr().String(), Handler: nil}
|
||||
muxer := http.NewServeMux()
|
||||
muxer.HandleFunc("/uptime", uptimeHandler(time.Now()))
|
||||
muxer.HandleFunc("/ws", websocketHandler(logger, upgrader))
|
||||
muxer.HandleFunc("/", rootHandler(serverName))
|
||||
httpServer := &http.Server{Addr: listener.Addr().String(), Handler: muxer}
|
||||
go func() {
|
||||
<-shutdownC
|
||||
httpServer.Close()
|
||||
}()
|
||||
|
||||
http.HandleFunc("/uptime", uptimeHandler(time.Now()))
|
||||
http.HandleFunc("/ws", websocketHandler(logger, upgrader))
|
||||
http.HandleFunc("/", rootHandler(serverName))
|
||||
err := httpServer.Serve(listener)
|
||||
return err
|
||||
}
|
||||
|
||||
+5
-1
@@ -2,6 +2,7 @@ package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -33,13 +34,16 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
|
||||
}
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.Handle("/healthcheck", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "OK\n")
|
||||
}))
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err = server.Serve(l)
|
||||
}()
|
||||
logger.WithField("addr", l.Addr()).Info("Starting metrics server")
|
||||
logger.WithField("addr", fmt.Sprintf("%v/metrics", l.Addr())).Info("Starting metrics server")
|
||||
// server.Serve will hang if server.Shutdown is called before the server is
|
||||
// fully started up. So add artificial delay.
|
||||
time.Sleep(startupTime)
|
||||
|
||||
@@ -92,3 +92,8 @@ func (b BackoffHandler) GetBaseTime() time.Duration {
|
||||
}
|
||||
return b.BaseTime
|
||||
}
|
||||
|
||||
// Retries returns the number of retries consumed so far.
|
||||
func (b *BackoffHandler) Retries() int {
|
||||
return int(b.retries)
|
||||
}
|
||||
|
||||
+168
-69
@@ -9,6 +9,12 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsNamespace = "cloudflared"
|
||||
tunnelSubsystem = "tunnel"
|
||||
muxerSubsystem = "muxer"
|
||||
)
|
||||
|
||||
type muxerMetrics struct {
|
||||
rtt *prometheus.GaugeVec
|
||||
rttMin *prometheus.GaugeVec
|
||||
@@ -32,6 +38,7 @@ type muxerMetrics struct {
|
||||
|
||||
type TunnelMetrics struct {
|
||||
haConnections prometheus.Gauge
|
||||
activeStreams prometheus.Gauge
|
||||
totalRequests prometheus.Counter
|
||||
requestsPerTunnel *prometheus.CounterVec
|
||||
// concurrentRequestsLock is a mutex for concurrentRequests and maxConcurrentRequests
|
||||
@@ -51,9 +58,11 @@ type TunnelMetrics struct {
|
||||
// oldServerLocations stores the last server the tunnel was connected to
|
||||
oldServerLocations map[string]string
|
||||
|
||||
regSuccess prometheus.Counter
|
||||
regFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
regSuccess *prometheus.CounterVec
|
||||
regFail *prometheus.CounterVec
|
||||
authSuccess prometheus.Counter
|
||||
authFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
tunnelsHA tunnelsForHA
|
||||
@@ -63,8 +72,10 @@ type TunnelMetrics struct {
|
||||
func newMuxerMetrics() *muxerMetrics {
|
||||
rtt := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "rtt",
|
||||
Help: "Round-trip time in millisecond",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt",
|
||||
Help: "Round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -72,8 +83,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
rttMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "rtt_min",
|
||||
Help: "Shortest round-trip time in millisecond",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_min",
|
||||
Help: "Shortest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -81,8 +94,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
rttMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "rtt_max",
|
||||
Help: "Longest round-trip time in millisecond",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_max",
|
||||
Help: "Longest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -90,8 +105,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
receiveWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "receive_window_ave",
|
||||
Help: "Average receive window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_ave",
|
||||
Help: "Average receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -99,8 +116,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
sendWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "send_window_ave",
|
||||
Help: "Average send window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_ave",
|
||||
Help: "Average send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -108,8 +127,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
receiveWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "receive_window_min",
|
||||
Help: "Smallest receive window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_min",
|
||||
Help: "Smallest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -117,8 +138,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
receiveWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "receive_window_max",
|
||||
Help: "Largest receive window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_max",
|
||||
Help: "Largest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -126,8 +149,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
sendWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "send_window_min",
|
||||
Help: "Smallest send window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_min",
|
||||
Help: "Smallest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -135,8 +160,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
sendWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "send_window_max",
|
||||
Help: "Largest send window size in bytes",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_max",
|
||||
Help: "Largest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -144,8 +171,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
inBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "inbound_bytes_per_sec_curr",
|
||||
Help: "Current inbounding bytes per second, 0 if there is no incoming connection",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_curr",
|
||||
Help: "Current inbounding bytes per second, 0 if there is no incoming connection",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -153,8 +182,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
inBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "inbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero inbounding bytes per second",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -162,8 +193,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
inBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "inbound_bytes_per_sec_max",
|
||||
Help: "Maximum inbounding bytes per second",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_max",
|
||||
Help: "Maximum inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -171,8 +204,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
outBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "outbound_bytes_per_sec_curr",
|
||||
Help: "Current outbounding bytes per second, 0 if there is no outgoing traffic",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_curr",
|
||||
Help: "Current outbounding bytes per second, 0 if there is no outgoing traffic",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -180,8 +215,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
outBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "outbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero outbounding bytes per second",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -189,8 +226,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
outBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "outbound_bytes_per_sec_max",
|
||||
Help: "Maximum outbounding bytes per second",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_max",
|
||||
Help: "Maximum outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -198,8 +237,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
compBytesBefore := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "comp_bytes_before",
|
||||
Help: "Bytes sent via cross-stream compression, pre compression",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_before",
|
||||
Help: "Bytes sent via cross-stream compression, pre compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -207,8 +248,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
compBytesAfter := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "comp_bytes_after",
|
||||
Help: "Bytes sent via cross-stream compression, post compression",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_after",
|
||||
Help: "Bytes sent via cross-stream compression, post compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -216,8 +259,10 @@ func newMuxerMetrics() *muxerMetrics {
|
||||
|
||||
compRateAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "comp_rate_ave",
|
||||
Help: "Average outbound cross-stream compression ratio",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_rate_ave",
|
||||
Help: "Average outbound cross-stream compression ratio",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -274,22 +319,30 @@ func convertRTTMilliSec(t time.Duration) float64 {
|
||||
func NewTunnelMetrics() *TunnelMetrics {
|
||||
haConnections := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "ha_connections",
|
||||
Help: "Number of active ha connections",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "ha_connections",
|
||||
Help: "Number of active ha connections",
|
||||
})
|
||||
prometheus.MustRegister(haConnections)
|
||||
|
||||
activeStreams := h2mux.NewActiveStreamsMetrics(metricsNamespace, tunnelSubsystem)
|
||||
|
||||
totalRequests := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "total_requests",
|
||||
Help: "Amount of requests proxied through all the tunnels",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "total_requests",
|
||||
Help: "Amount of requests proxied through all the tunnels",
|
||||
})
|
||||
prometheus.MustRegister(totalRequests)
|
||||
|
||||
requestsPerTunnel := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "requests_per_tunnel",
|
||||
Help: "Amount of requests proxied through each tunnel",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "requests_per_tunnel",
|
||||
Help: "Amount of requests proxied through each tunnel",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -297,8 +350,10 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
concurrentRequestsPerTunnel := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "concurrent_requests_per_tunnel",
|
||||
Help: "Concurrent requests proxied through each tunnel",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "concurrent_requests_per_tunnel",
|
||||
Help: "Concurrent requests proxied through each tunnel",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -306,8 +361,10 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "max_concurrent_requests_per_tunnel",
|
||||
Help: "Largest number of concurrent requests proxied through each tunnel so far",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "max_concurrent_requests_per_tunnel",
|
||||
Help: "Largest number of concurrent requests proxied through each tunnel so far",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
@@ -315,15 +372,19 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
timerRetries := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "timer_retries",
|
||||
Help: "Unacknowledged heart beats count",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "timer_retries",
|
||||
Help: "Unacknowledged heart beats count",
|
||||
})
|
||||
prometheus.MustRegister(timerRetries)
|
||||
|
||||
responseByCode := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "response_by_code",
|
||||
Help: "Count of responses by HTTP status code",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "response_by_code",
|
||||
Help: "Count of responses by HTTP status code",
|
||||
},
|
||||
[]string{"status_code"},
|
||||
)
|
||||
@@ -331,8 +392,10 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
responseCodePerTunnel := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "response_code_per_tunnel",
|
||||
Help: "Count of responses by HTTP status code fore each tunnel",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "response_code_per_tunnel",
|
||||
Help: "Count of responses by HTTP status code fore each tunnel",
|
||||
},
|
||||
[]string{"connection_id", "status_code"},
|
||||
)
|
||||
@@ -340,8 +403,10 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
serverLocations := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "server_locations",
|
||||
Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "server_locations",
|
||||
Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.",
|
||||
},
|
||||
[]string{"connection_id", "location"},
|
||||
)
|
||||
@@ -349,40 +414,72 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
|
||||
rpcFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_rpc_fail",
|
||||
Help: "Count of RPC connection errors by type",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_rpc_fail",
|
||||
Help: "Count of RPC connection errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
[]string{"error", "rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(rpcFail)
|
||||
|
||||
registerFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_register_fail",
|
||||
Help: "Count of tunnel registration errors by type",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_register_fail",
|
||||
Help: "Count of tunnel registration errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
[]string{"error", "rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(registerFail)
|
||||
|
||||
userHostnamesCounts := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "user_hostnames_counts",
|
||||
Help: "Which user hostnames cloudflared is serving",
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "user_hostnames_counts",
|
||||
Help: "Which user hostnames cloudflared is serving",
|
||||
},
|
||||
[]string{"userHostname"},
|
||||
)
|
||||
prometheus.MustRegister(userHostnamesCounts)
|
||||
|
||||
registerSuccess := prometheus.NewCounter(
|
||||
registerSuccess := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_register_success",
|
||||
Help: "Count of successful tunnel registrations",
|
||||
})
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_register_success",
|
||||
Help: "Count of successful tunnel registrations",
|
||||
},
|
||||
[]string{"rpcName"},
|
||||
)
|
||||
prometheus.MustRegister(registerSuccess)
|
||||
|
||||
authSuccess := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_authenticate_success",
|
||||
Help: "Count of successful tunnel authenticate",
|
||||
},
|
||||
)
|
||||
prometheus.MustRegister(authSuccess)
|
||||
|
||||
authFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_authenticate_fail",
|
||||
Help: "Count of tunnel authenticate errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
prometheus.MustRegister(authFail)
|
||||
|
||||
return &TunnelMetrics{
|
||||
haConnections: haConnections,
|
||||
activeStreams: activeStreams,
|
||||
totalRequests: totalRequests,
|
||||
requestsPerTunnel: requestsPerTunnel,
|
||||
concurrentRequestsPerTunnel: concurrentRequestsPerTunnel,
|
||||
@@ -400,6 +497,8 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
regFail: registerFail,
|
||||
rpcFail: rpcFail,
|
||||
userHostnamesCounts: userHostnamesCounts,
|
||||
authSuccess: authSuccess,
|
||||
authFail: authFail,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+268
-84
@@ -2,16 +2,21 @@ package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
|
||||
"github.com/google/uuid"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,13 +26,27 @@ const (
|
||||
resolveTTL = time.Hour
|
||||
// Interval between registering new tunnels
|
||||
registrationInterval = time.Second
|
||||
|
||||
subsystemRefreshAuth = "refresh_auth"
|
||||
// Maximum exponent for 'Authenticate' exponential backoff
|
||||
refreshAuthMaxBackoff = 10
|
||||
// Waiting time before retrying a failed 'Authenticate' connection
|
||||
refreshAuthRetryDuration = time.Second * 10
|
||||
// Maximum time to make an Authenticate RPC
|
||||
authTokenTimeout = time.Second * 30
|
||||
)
|
||||
|
||||
var (
|
||||
errJWTUnset = errors.New("JWT unset")
|
||||
errEventDigestUnset = errors.New("event digest unset")
|
||||
)
|
||||
|
||||
// Supervisor manages non-declarative tunnels. Establishes TCP connections with the edge, and
|
||||
// reconnects them if they disconnect.
|
||||
type Supervisor struct {
|
||||
config *TunnelConfig
|
||||
edgeIPs []*net.TCPAddr
|
||||
// nextUnusedEdgeIP is the index of the next addr k edgeIPs to try
|
||||
nextUnusedEdgeIP int
|
||||
cloudflaredUUID uuid.UUID
|
||||
config *TunnelConfig
|
||||
edgeIPs *edgediscovery.Edge
|
||||
lastResolve time.Time
|
||||
resolverC chan resolveResult
|
||||
tunnelErrors chan tunnelError
|
||||
@@ -38,36 +57,76 @@ type Supervisor struct {
|
||||
nextConnectedSignal chan struct{}
|
||||
|
||||
logger *logrus.Entry
|
||||
|
||||
jwtLock sync.RWMutex
|
||||
jwt []byte
|
||||
|
||||
eventDigestLock sync.RWMutex
|
||||
eventDigest []byte
|
||||
|
||||
connDigestLock sync.RWMutex
|
||||
connDigest map[uint8][]byte
|
||||
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
type resolveResult struct {
|
||||
edgeIPs []*net.TCPAddr
|
||||
err error
|
||||
err error
|
||||
}
|
||||
|
||||
type tunnelError struct {
|
||||
index int
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
}
|
||||
|
||||
func NewSupervisor(config *TunnelConfig) *Supervisor {
|
||||
func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
|
||||
var (
|
||||
edgeIPs *edgediscovery.Edge
|
||||
err error
|
||||
)
|
||||
if len(config.EdgeAddrs) > 0 {
|
||||
edgeIPs, err = edgediscovery.StaticEdge(config.Logger, config.EdgeAddrs)
|
||||
} else {
|
||||
edgeIPs, err = edgediscovery.ResolveEdge(config.Logger)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Supervisor{
|
||||
cloudflaredUUID: u,
|
||||
config: config,
|
||||
edgeIPs: edgeIPs,
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
logger: config.Logger.WithField("subsystem", "supervisor"),
|
||||
}
|
||||
connDigest: make(map[uint8][]byte),
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(ctx, connectedSignal, u); err != nil {
|
||||
if err := s.initialize(ctx, connectedSignal, reconnectCh); err != nil {
|
||||
return err
|
||||
}
|
||||
var tunnelsWaiting []int
|
||||
tunnelsActive := s.config.HAConnections
|
||||
|
||||
backoff := BackoffHandler{MaxRetries: s.config.Retries, BaseTime: tunnelRetryDuration, RetryForever: true}
|
||||
var backoffTimer <-chan time.Time
|
||||
tunnelsActive := s.config.HAConnections
|
||||
|
||||
refreshAuthBackoff := &BackoffHandler{MaxRetries: refreshAuthMaxBackoff, BaseTime: refreshAuthRetryDuration, RetryForever: true}
|
||||
var refreshAuthBackoffTimer <-chan time.Time
|
||||
|
||||
if s.config.UseReconnectToken {
|
||||
if timer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
|
||||
refreshAuthBackoffTimer = timer
|
||||
} else {
|
||||
logger.WithError(err).Errorf("initial refreshAuth failed, retrying in %v", refreshAuthRetryDuration)
|
||||
refreshAuthBackoffTimer = time.After(refreshAuthRetryDuration)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -91,23 +150,27 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u
|
||||
backoffTimer = backoff.BackoffTimer()
|
||||
}
|
||||
|
||||
// If the error is a dial error, the problem is likely to be network related
|
||||
// try another addr before refreshing since we are likely to get back the
|
||||
// same IPs in the same order. Same problem with duplicate connection error.
|
||||
if s.unusedIPs() {
|
||||
s.replaceEdgeIP(tunnelError.index)
|
||||
} else {
|
||||
s.refreshEdgeIPs()
|
||||
}
|
||||
// Previously we'd mark the edge address as bad here, but now we'll just silently use
|
||||
// another.
|
||||
}
|
||||
// Backoff was set and its timer expired
|
||||
case <-backoffTimer:
|
||||
backoffTimer = nil
|
||||
for _, index := range tunnelsWaiting {
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), u)
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), reconnectCh)
|
||||
}
|
||||
tunnelsActive += len(tunnelsWaiting)
|
||||
tunnelsWaiting = nil
|
||||
// Time to call Authenticate
|
||||
case <-refreshAuthBackoffTimer:
|
||||
newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Authentication failed")
|
||||
// Permanent failure. Leave the `select` without setting the
|
||||
// channel to be non-null, so we'll never hit this case of the `select` again.
|
||||
continue
|
||||
}
|
||||
refreshAuthBackoffTimer = newTimer
|
||||
// Tunnel successfully connected
|
||||
case <-s.nextConnectedSignal:
|
||||
if !s.waitForNextTunnel(s.nextConnectedIndex) && len(tunnelsWaiting) == 0 {
|
||||
@@ -120,7 +183,6 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u
|
||||
s.resolverC = nil
|
||||
if result.err == nil {
|
||||
logger.Debug("Service discovery refresh complete")
|
||||
s.edgeIPs = result.edgeIPs
|
||||
} else {
|
||||
logger.WithError(result.err).Error("Service discovery error")
|
||||
}
|
||||
@@ -128,29 +190,22 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
// Returns nil if initialization succeeded, else the initialization error.
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) error {
|
||||
logger := s.logger
|
||||
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
|
||||
if err != nil {
|
||||
logger.Infof("ResolveEdgeIPs err")
|
||||
return err
|
||||
}
|
||||
s.edgeIPs = edgeIPs
|
||||
if s.config.HAConnections > len(edgeIPs) {
|
||||
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, len(edgeIPs))
|
||||
s.config.HAConnections = len(edgeIPs)
|
||||
}
|
||||
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, u)
|
||||
availableAddrs := int(s.edgeIPs.AvailableAddrs())
|
||||
if s.config.HAConnections > availableAddrs {
|
||||
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
|
||||
s.config.HAConnections = availableAddrs
|
||||
}
|
||||
|
||||
go s.startFirstTunnel(ctx, connectedSignal, reconnectCh)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-s.tunnelErrors
|
||||
// Error can't be nil. A nil error signals that initialization succeed
|
||||
return fmt.Errorf("context was canceled")
|
||||
return ctx.Err()
|
||||
case tunnelError := <-s.tunnelErrors:
|
||||
return tunnelError.err
|
||||
case <-connectedSignal.Wait():
|
||||
@@ -158,7 +213,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
// At least one successful connection, so start the rest
|
||||
for i := 1; i < s.config.HAConnections; i++ {
|
||||
ch := signal.New(make(chan struct{}))
|
||||
go s.startTunnel(ctx, i, ch, u)
|
||||
go s.startTunnel(ctx, i, ch, reconnectCh)
|
||||
time.Sleep(registrationInterval)
|
||||
}
|
||||
return nil
|
||||
@@ -166,37 +221,64 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
|
||||
|
||||
// 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 *signal.Signal, u uuid.UUID) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan struct{}) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
)
|
||||
const thisConnID = 0
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: 0, err: err}
|
||||
s.tunnelErrors <- tunnelError{index: thisConnID, addr: addr, err: err}
|
||||
}()
|
||||
|
||||
addr, err = s.edgeIPs.GetAddr(thisConnID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
// If the first tunnel disconnects, keep restarting it.
|
||||
edgeErrors := 0
|
||||
for s.unusedIPs() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
default:
|
||||
}
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
return
|
||||
// try the next address if it was a dialError(network problem) or
|
||||
// dupConnRegisterTunnelError
|
||||
case dialError, dupConnRegisterTunnelError:
|
||||
s.replaceEdgeIP(0)
|
||||
case connection.DialError, dupConnRegisterTunnelError:
|
||||
edgeErrors++
|
||||
default:
|
||||
return
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
if edgeErrors >= 2 {
|
||||
addr, err = s.edgeIPs.GetDifferentAddr(thisConnID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *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) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, reconnectCh chan struct{}) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: index, addr: addr, err: err}
|
||||
}()
|
||||
|
||||
addr, err = s.edgeIPs.GetAddr(index)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
|
||||
}
|
||||
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
|
||||
@@ -218,38 +300,140 @@ func (s *Supervisor) waitForNextTunnel(index int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if time.Since(s.lastResolve) < resolveTTL {
|
||||
return
|
||||
}
|
||||
s.resolverC = make(chan resolveResult)
|
||||
go func() {
|
||||
edgeIPs, err := s.resolveEdgeIPs()
|
||||
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Supervisor) unusedIPs() bool {
|
||||
return s.nextUnusedEdgeIP < len(s.edgeIPs)
|
||||
return s.edgeIPs.AvailableAddrs() > s.config.HAConnections
|
||||
}
|
||||
|
||||
func (s *Supervisor) replaceEdgeIP(badIPIndex int) {
|
||||
s.edgeIPs[badIPIndex] = s.edgeIPs[s.nextUnusedEdgeIP]
|
||||
s.nextUnusedEdgeIP++
|
||||
func (s *Supervisor) ReconnectToken() ([]byte, error) {
|
||||
s.jwtLock.RLock()
|
||||
defer s.jwtLock.RUnlock()
|
||||
if s.jwt == nil {
|
||||
return nil, errJWTUnset
|
||||
}
|
||||
return s.jwt, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) SetReconnectToken(jwt []byte) {
|
||||
s.jwtLock.Lock()
|
||||
defer s.jwtLock.Unlock()
|
||||
s.jwt = jwt
|
||||
}
|
||||
|
||||
func (s *Supervisor) EventDigest() ([]byte, error) {
|
||||
s.eventDigestLock.RLock()
|
||||
defer s.eventDigestLock.RUnlock()
|
||||
if s.eventDigest == nil {
|
||||
return nil, errEventDigestUnset
|
||||
}
|
||||
return s.eventDigest, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) SetEventDigest(eventDigest []byte) {
|
||||
s.eventDigestLock.Lock()
|
||||
defer s.eventDigestLock.Unlock()
|
||||
s.eventDigest = eventDigest
|
||||
}
|
||||
|
||||
func (s *Supervisor) ConnDigest(connID uint8) ([]byte, error) {
|
||||
s.connDigestLock.RLock()
|
||||
defer s.connDigestLock.RUnlock()
|
||||
digest, ok := s.connDigest[connID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no connection digest for connection %v", connID)
|
||||
}
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) SetConnDigest(connID uint8, connDigest []byte) {
|
||||
s.connDigestLock.Lock()
|
||||
defer s.connDigestLock.Unlock()
|
||||
s.connDigest[connID] = connDigest
|
||||
}
|
||||
|
||||
func (s *Supervisor) refreshAuth(
|
||||
ctx context.Context,
|
||||
backoff *BackoffHandler,
|
||||
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
|
||||
) (retryTimer <-chan time.Time, err error) {
|
||||
logger := s.config.Logger.WithField("subsystem", subsystemRefreshAuth)
|
||||
authOutcome, err := authenticate(ctx, backoff.Retries())
|
||||
if err != nil {
|
||||
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
logger.WithError(err).Warnf("Retrying in %v", duration)
|
||||
return backoff.BackoffTimer(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// clear backoff timer
|
||||
backoff.SetGracePeriod()
|
||||
|
||||
switch outcome := authOutcome.(type) {
|
||||
case tunnelpogs.AuthSuccess:
|
||||
s.SetReconnectToken(outcome.JWT())
|
||||
s.config.Metrics.authSuccess.Inc()
|
||||
return timeAfter(outcome.RefreshAfter()), nil
|
||||
case tunnelpogs.AuthUnknown:
|
||||
duration := outcome.RefreshAfter()
|
||||
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
|
||||
logger.WithError(outcome).Warnf("Retrying in %v", duration)
|
||||
return timeAfter(duration), nil
|
||||
case tunnelpogs.AuthFail:
|
||||
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
|
||||
return nil, outcome
|
||||
default:
|
||||
err := fmt.Errorf("Unexpected outcome type %T", authOutcome)
|
||||
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error) {
|
||||
arbitraryEdgeIP, err := s.edgeIPs.GetAddrForRPC()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeConn, err := connection.DialEdge(ctx, dialTimeout, s.config.TlsConfig, arbitraryEdgeIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer edgeConn.Close()
|
||||
|
||||
handler := h2mux.MuxedStreamFunc(func(*h2mux.MuxedStream) error {
|
||||
// This callback is invoked by h2mux when the edge initiates a stream.
|
||||
return nil // noop
|
||||
})
|
||||
muxerConfig := s.config.muxerConfig(handler)
|
||||
muxerConfig.Logger = muxerConfig.Logger.WithField("subsystem", subsystemRefreshAuth)
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, muxerConfig, s.config.Metrics.activeStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go muxer.Serve(ctx)
|
||||
defer func() {
|
||||
// If we don't wait for the muxer shutdown here, edgeConn.Close() runs before the muxer connections are done,
|
||||
// and the user sees log noise: "error writing data", "connection closed unexpectedly"
|
||||
<-muxer.Shutdown()
|
||||
}()
|
||||
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger.WithField("subsystem", subsystemRefreshAuth), openStreamTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tunnelServer.Close()
|
||||
|
||||
const arbitraryConnectionID = uint8(0)
|
||||
registrationOptions := s.config.RegistrationOptions(arbitraryConnectionID, edgeConn.LocalAddr().String(), s.cloudflaredUUID)
|
||||
registrationOptions.NumPreviousAttempts = uint8(numPreviousAttempts)
|
||||
authResponse, err := tunnelServer.Authenticate(
|
||||
ctx,
|
||||
s.config.OriginCert,
|
||||
s.config.Hostname,
|
||||
registrationOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authResponse.Outcome(), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
func testConfig(logger *logrus.Logger) *TunnelConfig {
|
||||
metrics := TunnelMetrics{}
|
||||
|
||||
metrics.authSuccess = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_authenticate_success",
|
||||
Help: "Count of successful tunnel authenticate",
|
||||
},
|
||||
)
|
||||
|
||||
metrics.authFail = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: tunnelSubsystem,
|
||||
Name: "tunnel_authenticate_fail",
|
||||
Help: "Count of tunnel authenticate errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
return &TunnelConfig{Logger: logger, Metrics: &metrics}
|
||||
}
|
||||
|
||||
func TestRefreshAuthBackoff(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
wait = d
|
||||
return time.After(d)
|
||||
}
|
||||
|
||||
s, err := NewSupervisor(testConfig(logger), uuid.New())
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
backoff := &BackoffHandler{MaxRetries: 3}
|
||||
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
|
||||
return nil, fmt.Errorf("authentication failure")
|
||||
}
|
||||
|
||||
// authentication failures should consume the backoff
|
||||
for i := uint(0); i < backoff.MaxRetries; i++ {
|
||||
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, retryChan)
|
||||
assert.Equal(t, (1<<i)*time.Second, wait)
|
||||
}
|
||||
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, retryChan)
|
||||
|
||||
// now we actually make contact with the remote server
|
||||
_, _ = s.refreshAuth(context.Background(), backoff, func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
|
||||
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
|
||||
})
|
||||
|
||||
// The backoff timer should have been reset. To confirm this, make timeNow
|
||||
// return a value after the backoff timer's grace period
|
||||
timeNow = func() time.Time {
|
||||
expectedGracePeriod := time.Duration(time.Second * 2 << backoff.MaxRetries)
|
||||
return time.Now().Add(expectedGracePeriod * 2)
|
||||
}
|
||||
_, ok := backoff.GetBackoffDuration(context.Background())
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestRefreshAuthSuccess(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
wait = d
|
||||
return time.After(d)
|
||||
}
|
||||
|
||||
s, err := NewSupervisor(testConfig(logger), uuid.New())
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
backoff := &BackoffHandler{MaxRetries: 3}
|
||||
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
|
||||
return tunnelpogs.NewAuthSuccess([]byte("jwt"), 19), nil
|
||||
}
|
||||
|
||||
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, retryChan)
|
||||
assert.Equal(t, 19*time.Hour, wait)
|
||||
|
||||
token, err := s.ReconnectToken()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("jwt"), token)
|
||||
}
|
||||
|
||||
func TestRefreshAuthUnknown(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
|
||||
var wait time.Duration
|
||||
timeAfter = func(d time.Duration) <-chan time.Time {
|
||||
wait = d
|
||||
return time.After(d)
|
||||
}
|
||||
|
||||
s, err := NewSupervisor(testConfig(logger), uuid.New())
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
backoff := &BackoffHandler{MaxRetries: 3}
|
||||
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
|
||||
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
|
||||
}
|
||||
|
||||
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, retryChan)
|
||||
assert.Equal(t, 19*time.Hour, wait)
|
||||
|
||||
token, err := s.ReconnectToken()
|
||||
assert.Equal(t, errJWTUnset, err)
|
||||
assert.Nil(t, token)
|
||||
}
|
||||
|
||||
func TestRefreshAuthFail(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.Level = logrus.ErrorLevel
|
||||
|
||||
s, err := NewSupervisor(testConfig(logger), uuid.New())
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
backoff := &BackoffHandler{MaxRetries: 3}
|
||||
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
|
||||
return tunnelpogs.NewAuthFail(errors.New("auth fail")), nil
|
||||
}
|
||||
|
||||
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, retryChan)
|
||||
|
||||
token, err := s.ReconnectToken()
|
||||
assert.Equal(t, errJWTUnset, err)
|
||||
assert.Nil(t, token)
|
||||
}
|
||||
+201
-136
@@ -14,7 +14,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
@@ -22,25 +30,25 @@ import (
|
||||
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
|
||||
muxerTimeout = 5 * 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 registerRPCName string
|
||||
|
||||
const (
|
||||
register registerRPCName = "register"
|
||||
reconnect registerRPCName = "reconnect"
|
||||
unknown registerRPCName = "unknown"
|
||||
)
|
||||
|
||||
type TunnelConfig struct {
|
||||
BuildInfo *buildinfo.BuildInfo
|
||||
ClientID string
|
||||
@@ -74,14 +82,21 @@ type TunnelConfig struct {
|
||||
WSGI bool
|
||||
// OriginUrl may not be used if a user specifies a unix socket.
|
||||
OriginUrl string
|
||||
|
||||
// feature-flag to use new edge reconnect tokens
|
||||
UseReconnectToken bool
|
||||
// feature-flag for using ConnectionDigest
|
||||
UseQuickReconnects bool
|
||||
}
|
||||
|
||||
type dialError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e dialError) Error() string {
|
||||
return e.cause.Error()
|
||||
// ReconnectTunnelCredentialManager is invoked by functions in this file to
|
||||
// get/set parameters for ReconnectTunnel RPC calls.
|
||||
type ReconnectTunnelCredentialManager interface {
|
||||
ReconnectToken() ([]byte, error)
|
||||
EventDigest() ([]byte, error)
|
||||
SetEventDigest(eventDigest []byte)
|
||||
ConnDigest(connID uint8) ([]byte, error)
|
||||
SetConnDigest(connID uint8, connDigest []byte)
|
||||
}
|
||||
|
||||
type dupConnRegisterTunnelError struct{}
|
||||
@@ -111,8 +126,8 @@ type clientRegisterTunnelError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec) clientRegisterTunnelError {
|
||||
counter.WithLabelValues(cause.Error()).Inc()
|
||||
func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec, name registerRPCName) clientRegisterTunnelError {
|
||||
counter.WithLabelValues(cause.Error(), string(name)).Inc()
|
||||
return clientRegisterTunnelError{cause: cause}
|
||||
}
|
||||
|
||||
@@ -120,6 +135,18 @@ func (e clientRegisterTunnelError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) muxerConfig(handler h2mux.MuxedStreamHandler) h2mux.MuxerConfig {
|
||||
return h2mux.MuxerConfig{
|
||||
Timeout: muxerTimeout,
|
||||
Handler: handler,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: c.HeartbeatInterval,
|
||||
MaxHeartbeats: c.MaxHeartbeats,
|
||||
Logger: c.TransportLogger.WithFields(log.Fields{}),
|
||||
CompressionQuality: h2mux.CompressionSetting(c.CompressionQuality),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions {
|
||||
policy := tunnelrpc.ExistingTunnelPolicy_balance
|
||||
if c.HAConnections <= 1 && c.LBPool == "" {
|
||||
@@ -138,19 +165,27 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
RunFromTerminal: c.RunFromTerminal,
|
||||
CompressionQuality: c.CompressionQuality,
|
||||
UUID: uuid.String(),
|
||||
Features: connection.SupportedFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal, cloudflaredID)
|
||||
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID, reconnectCh chan struct{}) error {
|
||||
s, err := NewSupervisor(config, cloudflaredID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Run(ctx, connectedSignal, reconnectCh)
|
||||
}
|
||||
|
||||
func ServeTunnelLoop(ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectedSignal *signal.Signal,
|
||||
u uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan struct{},
|
||||
) error {
|
||||
connectionLogger := config.Logger.WithField("connectionID", connectionID)
|
||||
config.Metrics.incrementHaConnections()
|
||||
@@ -165,7 +200,18 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
// Ensure the above goroutine will terminate if we return without connecting
|
||||
defer connectedFuse.Fuse(false)
|
||||
for {
|
||||
err, recoverable := ServeTunnel(ctx, config, connectionLogger, addr, connectionID, connectedFuse, &backoff, u)
|
||||
err, recoverable := ServeTunnel(
|
||||
ctx,
|
||||
credentialManager,
|
||||
config,
|
||||
connectionLogger,
|
||||
addr, connectionID,
|
||||
connectedFuse,
|
||||
&backoff,
|
||||
u,
|
||||
bufferPool,
|
||||
reconnectCh,
|
||||
)
|
||||
if recoverable {
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
connectionLogger.Infof("Retrying in %s seconds", duration)
|
||||
@@ -179,6 +225,7 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
|
||||
func ServeTunnel(
|
||||
ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
addr *net.TCPAddr,
|
||||
@@ -186,6 +233,8 @@ func ServeTunnel(
|
||||
connectedFuse *h2mux.BooleanFuse,
|
||||
backoff *BackoffHandler,
|
||||
u uuid.UUID,
|
||||
bufferPool *buffer.Pool,
|
||||
reconnectCh chan struct{},
|
||||
) (err error, recoverable bool) {
|
||||
// Treat panics as recoverable errors
|
||||
defer func() {
|
||||
@@ -206,11 +255,11 @@ func ServeTunnel(
|
||||
tags["ha"] = connectionTag
|
||||
|
||||
// Returns error from parsing the origin URL or handshake errors
|
||||
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr.String(), connectionID)
|
||||
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID, bufferPool)
|
||||
if err != nil {
|
||||
errLog := logger.WithError(err)
|
||||
switch err.(type) {
|
||||
case dialError:
|
||||
case connection.DialError:
|
||||
errLog.Error("Unable to dial edge")
|
||||
case h2mux.MuxerHandshakeError:
|
||||
errLog.Error("Handshake failed with edge server")
|
||||
@@ -223,13 +272,38 @@ func ServeTunnel(
|
||||
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := RegisterTunnel(serveCtx, handler.muxer, config, logger, connectionID, originLocalIP, u)
|
||||
if err == nil {
|
||||
connectedFuse.Fuse(true)
|
||||
backoff.SetGracePeriod()
|
||||
errGroup.Go(func() (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
connectedFuse.Fuse(true)
|
||||
backoff.SetGracePeriod()
|
||||
}
|
||||
}()
|
||||
|
||||
if config.UseReconnectToken && connectedFuse.Value() {
|
||||
token, tokenErr := credentialManager.ReconnectToken()
|
||||
eventDigest, eventDigestErr := credentialManager.EventDigest()
|
||||
// if we have both credentials, we can reconnect
|
||||
if tokenErr == nil && eventDigestErr == nil {
|
||||
var connDigest []byte
|
||||
|
||||
// check if we can use Quick Reconnects
|
||||
if config.UseQuickReconnects {
|
||||
if digest, connDigestErr := credentialManager.ConnDigest(connectionID); connDigestErr == nil {
|
||||
connDigest = digest
|
||||
}
|
||||
}
|
||||
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionID, originLocalIP, u, credentialManager)
|
||||
}
|
||||
// log errors and proceed to RegisterTunnel
|
||||
if tokenErr != nil {
|
||||
logger.WithError(tokenErr).Error("Couldn't get reconnect token")
|
||||
}
|
||||
if eventDigestErr != nil {
|
||||
logger.WithError(eventDigestErr).Error("Couldn't get event digest")
|
||||
}
|
||||
}
|
||||
return err
|
||||
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionID, originLocalIP, u)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
@@ -247,6 +321,16 @@ func ServeTunnel(
|
||||
}
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
select {
|
||||
case <-reconnectCh:
|
||||
return fmt.Errorf("received disconnect signal")
|
||||
case <-serveCtx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
// All routines should stop when muxer finish serving. When muxer is shutdown
|
||||
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
|
||||
@@ -260,6 +344,8 @@ func ServeTunnel(
|
||||
|
||||
err = errGroup.Wait()
|
||||
if err != nil {
|
||||
_ = newClientRegisterTunnelError(err, config.Metrics.regFail, unknown)
|
||||
|
||||
switch castedErr := err.(type) {
|
||||
case dupConnRegisterTunnelError:
|
||||
logger.Info("Already connected to this server, selecting a different one")
|
||||
@@ -274,32 +360,21 @@ func ServeTunnel(
|
||||
return castedErr.cause, !castedErr.permanent
|
||||
case clientRegisterTunnelError:
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
|
||||
raven.CaptureError(castedErr.cause, tags)
|
||||
return err, true
|
||||
case muxerShutdownError:
|
||||
logger.Infof("Muxer shutdown")
|
||||
return err, true
|
||||
default:
|
||||
logger.WithError(err).Error("Serve tunnel error")
|
||||
raven.CaptureError(err, tags)
|
||||
return err, true
|
||||
}
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func IsRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
if len(headers) != 1 {
|
||||
return false
|
||||
}
|
||||
if headers[0].Name != ":status" || headers[0].Value != "200" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RegisterTunnel(
|
||||
ctx context.Context,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
@@ -308,45 +383,82 @@ func RegisterTunnel(
|
||||
uuid uuid.UUID,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to register")
|
||||
stream, err := openStream(ctx, muxer)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-register"), openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
|
||||
}
|
||||
if !IsRPCStreamResponse(stream.Headers) {
|
||||
// stream response error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
}
|
||||
conn := rpc.NewConn(
|
||||
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)}
|
||||
defer tunnelServer.Close()
|
||||
// Request server info without blocking tunnel registration; must use capnp library directly.
|
||||
tsClient := tunnelrpc.TunnelServer{Client: ts.Client}
|
||||
serverInfoPromise := tsClient.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
return nil
|
||||
})
|
||||
registration, err := ts.RegisterTunnel(
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
|
||||
registration := tunnelServer.RegisterTunnel(
|
||||
ctx,
|
||||
config.OriginCert,
|
||||
config.Hostname,
|
||||
config.RegistrationOptions(connectionID, originLocalIP, uuid),
|
||||
)
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
|
||||
if err != nil {
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// RegisterTunnel RPC failure
|
||||
return newClientRegisterTunnelError(err, config.Metrics.regFail)
|
||||
return processRegisterTunnelError(registrationErr, config.Metrics, register)
|
||||
}
|
||||
credentialManager.SetEventDigest(registration.EventDigest)
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
|
||||
}
|
||||
|
||||
func ReconnectTunnel(
|
||||
ctx context.Context,
|
||||
token []byte,
|
||||
eventDigest, connDigest []byte,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
uuid uuid.UUID,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to reconnect")
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-reconnect"), openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
|
||||
}
|
||||
defer tunnelServer.Close()
|
||||
// Request server info without blocking tunnel registration; must use capnp library directly.
|
||||
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
return nil
|
||||
})
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
|
||||
registration := tunnelServer.ReconnectTunnel(
|
||||
ctx,
|
||||
token,
|
||||
eventDigest,
|
||||
connDigest,
|
||||
config.Hostname,
|
||||
config.RegistrationOptions(connectionID, originLocalIP, uuid),
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// ReconnectTunnel RPC failure
|
||||
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
|
||||
}
|
||||
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
|
||||
}
|
||||
|
||||
func processRegistrationSuccess(
|
||||
config *TunnelConfig,
|
||||
logger *log.Entry,
|
||||
connectionID uint8,
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name registerRPCName,
|
||||
credentialManager ReconnectTunnelCredentialManager,
|
||||
) error {
|
||||
for _, logLine := range registration.LogLines {
|
||||
logger.Info(logLine)
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -364,58 +476,36 @@ func RegisterTunnel(
|
||||
}
|
||||
}
|
||||
|
||||
credentialManager.SetConnDigest(connectionID, registration.ConnDigest)
|
||||
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")
|
||||
config.Metrics.regSuccess.WithLabelValues(string(name)).Inc()
|
||||
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 {
|
||||
func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics, name registerRPCName) error {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
|
||||
return dupConnRegisterTunnelError{}
|
||||
}
|
||||
metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", err),
|
||||
permanent: permanentFailure,
|
||||
cause: fmt.Errorf("Server error: %s", err.Error()),
|
||||
permanent: err.IsPermanent(),
|
||||
}
|
||||
}
|
||||
|
||||
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
|
||||
logger.Debug("initiating RPC stream to unregister")
|
||||
ctx := context.Background()
|
||||
stream, err := openStream(ctx, muxer)
|
||||
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger.WithField("subsystem", "rpc-unregister"), openStreamTimeout)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return err
|
||||
}
|
||||
if !IsRPCStreamResponse(stream.Headers) {
|
||||
// stream response error
|
||||
return err
|
||||
}
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(logger.WithField("subsystem", "rpc-unregister"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(logger.WithField("subsystem", "rpc-transport")),
|
||||
)
|
||||
defer conn.Close()
|
||||
ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
|
||||
// gracePeriod is encoded in int64 using capnproto
|
||||
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)
|
||||
return tunnelServer.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
|
||||
}
|
||||
|
||||
func LogServerInfo(
|
||||
@@ -438,16 +528,6 @@ func LogServerInfo(
|
||||
metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type TunnelHandler struct {
|
||||
originUrl string
|
||||
httpHostHeader string
|
||||
@@ -460,15 +540,16 @@ type TunnelHandler struct {
|
||||
connectionID string
|
||||
logger *log.Logger
|
||||
noChunkedEncoding bool
|
||||
}
|
||||
|
||||
var dialer = net.Dialer{DualStack: true}
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
|
||||
func NewTunnelHandler(ctx context.Context,
|
||||
config *TunnelConfig,
|
||||
addr string,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
bufferPool *buffer.Pool,
|
||||
) (*TunnelHandler, string, error) {
|
||||
originURL, err := validation.ValidateUrl(config.OriginUrl)
|
||||
if err != nil {
|
||||
@@ -484,41 +565,23 @@ func NewTunnelHandler(ctx context.Context,
|
||||
connectionID: uint8ToString(connectionID),
|
||||
logger: config.Logger,
|
||||
noChunkedEncoding: config.NoChunkedEncoding,
|
||||
bufferPool: bufferPool,
|
||||
}
|
||||
if h.httpClient == nil {
|
||||
h.httpClient = http.DefaultTransport
|
||||
}
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout)
|
||||
// TUN-92: enforce a timeout on dial and handshake (as tls.Dial does not support one)
|
||||
plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", addr)
|
||||
dialCancel()
|
||||
|
||||
edgeConn, err := connection.DialEdge(ctx, dialTimeout, config.TlsConfig, addr)
|
||||
if err != nil {
|
||||
return nil, "", dialError{cause: errors.Wrap(err, "DialContext error")}
|
||||
return nil, "", err
|
||||
}
|
||||
edgeConn := tls.Client(plaintextEdgeConn, config.TlsConfig)
|
||||
edgeConn.SetDeadline(time.Now().Add(dialTimeout))
|
||||
err = edgeConn.Handshake()
|
||||
if err != nil {
|
||||
return nil, "", dialError{cause: errors.Wrap(err, "Handshake with edge error")}
|
||||
}
|
||||
// clear the deadline on the conn; h2mux has its own timeouts
|
||||
edgeConn.SetDeadline(time.Time{})
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
h.muxer, err = h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
|
||||
Timeout: 5 * time.Second,
|
||||
Handler: h,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.TransportLogger.WithFields(log.Fields{}),
|
||||
CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality),
|
||||
})
|
||||
h.muxer, err = h2mux.Handshake(edgeConn, edgeConn, config.muxerConfig(h), h.metrics.activeStreams)
|
||||
if err != nil {
|
||||
return h, "", errors.New("TLS handshake error")
|
||||
return nil, "", errors.Wrap(err, "h2mux handshake with edge error")
|
||||
}
|
||||
return h, edgeConn.LocalAddr().String(), err
|
||||
return h, edgeConn.LocalAddr().String(), nil
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) AppendTagHeaders(r *http.Request) {
|
||||
@@ -561,7 +624,7 @@ func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request,
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = streamhandler.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
@@ -580,7 +643,7 @@ func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Requ
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
err = stream.WriteHeaders(h2mux.H1ResponseToH2ResponseHeaders(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
@@ -614,7 +677,7 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
err = stream.WriteHeaders(h2mux.H1ResponseToH2ResponseHeaders(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
@@ -623,7 +686,9 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
|
||||
} 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))
|
||||
buf := h.bufferPool.Get()
|
||||
defer h.bufferPool.Put(buf)
|
||||
io.CopyBuffer(stream, response.Body, buf)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/buffer"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
@@ -33,6 +34,7 @@ type HTTPService struct {
|
||||
client http.RoundTripper
|
||||
originURL *url.URL
|
||||
chunkedEncoding bool
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
|
||||
@@ -40,6 +42,7 @@ func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding b
|
||||
client: transport,
|
||||
originURL: url,
|
||||
chunkedEncoding: chunkedEncoding,
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +74,9 @@ func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*htt
|
||||
} 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))
|
||||
buf := hc.bufferPool.Get()
|
||||
defer hc.bufferPool.Put(buf)
|
||||
io.CopyBuffer(stream, resp.Body, buf)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -142,10 +147,11 @@ func (wsc *WebsocketService) Shutdown() {
|
||||
|
||||
// HelloWorldService talks to the hello world example origin
|
||||
type HelloWorldService struct {
|
||||
client http.RoundTripper
|
||||
listener net.Listener
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
client http.RoundTripper
|
||||
listener net.Listener
|
||||
originURL *url.URL
|
||||
shutdownC chan struct{}
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
|
||||
@@ -164,7 +170,8 @@ func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
|
||||
Scheme: "https",
|
||||
Host: listener.Addr().String(),
|
||||
},
|
||||
shutdownC: shutdownC,
|
||||
shutdownC: shutdownC,
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -184,7 +191,9 @@ func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request
|
||||
|
||||
// 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))
|
||||
buf := hwc.bufferPool.Get()
|
||||
defer hwc.bufferPool.Put(buf)
|
||||
io.CopyBuffer(stream, resp.Body, buf)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -221,6 +221,21 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
// stderr streams are used non-pty sessions since they have distinct IO streams.
|
||||
remoteStderr := remoteChan.Stderr()
|
||||
localStderr := localChan.Stderr()
|
||||
go func() {
|
||||
if _, err := io.Copy(remoteStderr, localStderr); err != nil {
|
||||
s.logger.WithError(err).Error("stderr local to remote copy error")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if _, err := io.Copy(localStderr, remoteStderr); err != nil {
|
||||
s.logger.WithError(err).Error("stderr remote to local copy error")
|
||||
}
|
||||
}()
|
||||
|
||||
s.logAuditEvent(conn, "", auditEventStart, ctx)
|
||||
defer s.logAuditEvent(conn, "", auditEventStop, ctx)
|
||||
|
||||
@@ -231,7 +246,6 @@ func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanRe
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.forwardChannelRequest(remoteChan, req); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to forward request")
|
||||
return
|
||||
@@ -329,6 +343,7 @@ func (s *SSHProxy) dialDestination(ctx ssh.Context) (*gossh.Client, error) {
|
||||
s.logger.WithError(err).Error("Failed to generate signed short lived cert")
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Debugf("Short lived certificate for %s connecting to %s:\n\n%s", ctx.User(), preamble.Destination, gossh.MarshalAuthorizedKey(signer.PublicKey()))
|
||||
|
||||
clientConfig := &gossh.ClientConfig{
|
||||
User: ctx.User(),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package streamhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
@@ -28,42 +26,9 @@ func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, erro
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unexpected error from http.NewRequest")
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
switch header.Name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
u, err := url.Parse(header.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable path")
|
||||
}
|
||||
resolved := h1.URL.ResolveReference(u)
|
||||
// prevent escaping base URL
|
||||
if !strings.HasPrefix(resolved.String(), h1.URL.String()) {
|
||||
return fmt.Errorf("invalid path")
|
||||
}
|
||||
h1.URL = resolved
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
default:
|
||||
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ var (
|
||||
{Name: ":scheme", Value: "http"},
|
||||
{Name: ":authority", Value: "example.com"},
|
||||
{Name: ":path", Value: "/"},
|
||||
|
||||
// Regular headers must always come after the pseudoheaders
|
||||
{Name: h2mux.RequestUserHeadersField, Value: ""},
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{
|
||||
Name: h2mux.CloudflaredProxyTunnelHostnameHeader,
|
||||
Value: testTunnelHostname.String(),
|
||||
}
|
||||
tunnelHostnameHeader = h2mux.Header{Name: h2mux.CloudflaredProxyTunnelHostnameHeader, Value: testTunnelHostname.String()}
|
||||
)
|
||||
|
||||
func TestServeRequest(t *testing.T) {
|
||||
@@ -69,29 +69,73 @@ func TestServeRequest(t *testing.T) {
|
||||
assertRespBody(t, message, stream)
|
||||
}
|
||||
|
||||
func TestServeBadRequest(t *testing.T) {
|
||||
func createStreamHandler() *StreamHandler {
|
||||
configChan := make(chan *pogs.ClientConfig)
|
||||
useConfigResultChan := make(chan *pogs.UseConfigurationResult)
|
||||
streamHandler := NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
|
||||
return NewStreamHandler(configChan, useConfigResultChan, logrus.New())
|
||||
}
|
||||
|
||||
func createRequestMuxPair(t *testing.T, streamHandler *StreamHandler) *DefaultMuxerPair {
|
||||
muxPair := NewDefaultMuxerPair(t, streamHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
return muxPair
|
||||
}
|
||||
|
||||
func TestServeStatusBadRequest(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No tunnel hostname header, expect to get 400 Bad Request
|
||||
stream, err := muxPair.EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, baseHeaders, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeInvalidContentLength(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Invalid content-length, wouldn't be able to create a request
|
||||
// Expect to get 400 Bad Request
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig([]*pogs.ReverseProxyConfig{
|
||||
{
|
||||
TunnelHostname: testTunnelHostname,
|
||||
OriginConfig: &pogs.HTTPOriginConfig{
|
||||
URLString: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
mux := createRequestMuxPair(t, streamHandler).EdgeMux
|
||||
stream, err := mux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusNotFound(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// No mapping for the tunnel hostname, expect to get 404 Not Found
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
stream, err := createRequestMuxPair(t, createStreamHandler()).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusNotFound, stream.Headers)
|
||||
assertRespBody(t, statusNotFound.text, stream)
|
||||
}
|
||||
|
||||
func TestServeStatusBadGateway(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Nothing listening on empty url, so proxy would fail. Expect to get 502 Bad Gateway
|
||||
reverseProxyConfigs := []*pogs.ReverseProxyConfig{
|
||||
@@ -102,21 +146,13 @@ func TestServeBadRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
streamHandler := createStreamHandler()
|
||||
streamHandler.UpdateConfig(reverseProxyConfigs)
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
headers := append(baseHeaders, tunnelHostnameHeader)
|
||||
stream, err := createRequestMuxPair(t, streamHandler).EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadGateway, stream.Headers)
|
||||
assertRespBody(t, statusBadGateway.text, stream)
|
||||
|
||||
// Invalid content-length, wouldn't not be able to create a request. Expect to get 400 Bad Request
|
||||
headers = append(headers, h2mux.Header{
|
||||
Name: "content-length",
|
||||
Value: "x",
|
||||
})
|
||||
stream, err = muxPair.EdgeMux.OpenStream(ctx, headers, nil)
|
||||
assert.NoError(t, err)
|
||||
assertStatusHeader(t, http.StatusBadRequest, stream.Headers)
|
||||
assertRespBody(t, statusBadRequest.text, stream)
|
||||
}
|
||||
|
||||
func assertStatusHeader(t *testing.T, expectedStatus int, headers []h2mux.Header) {
|
||||
@@ -141,7 +177,7 @@ type DefaultMuxerPair struct {
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewDefaultMuxerPair(t assert.TestingT, h h2mux.MuxedStreamHandler) *DefaultMuxerPair {
|
||||
func NewDefaultMuxerPair(t *testing.T, h h2mux.MuxedStreamHandler) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: h2mux.MuxerConfig{
|
||||
@@ -167,20 +203,20 @@ func NewDefaultMuxerPair(t assert.TestingT, h h2mux.MuxedStreamHandler) *Default
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
assert.NoError(t, p.Handshake(t.Name()))
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Handshake() error {
|
||||
func (p *DefaultMuxerPair) Handshake(testName string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
|
||||
defer cancel()
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() (err error) {
|
||||
p.EdgeMux, err = h2mux.Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
|
||||
p.EdgeMux, err = h2mux.Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, h2mux.NewActiveStreamsMetrics(testName, "edge"))
|
||||
return errors.Wrap(err, "edge handshake failure")
|
||||
})
|
||||
errGroup.Go(func() (err error) {
|
||||
p.OriginMux, err = h2mux.Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
|
||||
p.OriginMux, err = h2mux.Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, h2mux.NewActiveStreamsMetrics(testName, "origin"))
|
||||
return errors.Wrap(err, "origin handshake failure")
|
||||
})
|
||||
|
||||
@@ -218,6 +254,6 @@ type mockHTTPHandler struct {
|
||||
message []byte
|
||||
}
|
||||
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(mth.message)
|
||||
func (mth *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(mth.message)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package supervisor is used by declarative tunnels to get/apply new config from the edge.
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
@@ -13,9 +14,11 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/streamhandler"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -28,13 +31,34 @@ type Supervisor struct {
|
||||
useConfigResultChan chan<- *pogs.UseConfigurationResult
|
||||
state *state
|
||||
logger *logrus.Entry
|
||||
metrics metrics
|
||||
}
|
||||
|
||||
type metrics struct {
|
||||
configVersion prometheus.Gauge
|
||||
}
|
||||
|
||||
func newMetrics() metrics {
|
||||
configVersion := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "supervisor",
|
||||
Subsystem: "supervisor",
|
||||
Name: "config_version",
|
||||
Help: "Latest configuration version received from Cloudflare",
|
||||
},
|
||||
)
|
||||
prometheus.MustRegister(
|
||||
configVersion,
|
||||
)
|
||||
return metrics{
|
||||
configVersion: configVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSupervisor(
|
||||
defaultClientConfig *pogs.ClientConfig,
|
||||
userCredential []byte,
|
||||
tlsConfig *tls.Config,
|
||||
serviceDiscoverer connection.EdgeServiceDiscoverer,
|
||||
serviceDiscoverer *edgediscovery.Edge,
|
||||
cloudflaredConfig *connection.CloudflaredConfig,
|
||||
autoupdater *updater.AutoUpdater,
|
||||
supportAutoupdate bool,
|
||||
@@ -70,6 +94,7 @@ func NewSupervisor(
|
||||
useConfigResultChan: useConfigResultChan,
|
||||
state: newState(defaultClientConfig),
|
||||
logger: logger.WithField("subsystem", "supervisor"),
|
||||
metrics: newMetrics(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -131,6 +156,7 @@ func (s *Supervisor) notifySubsystemsNewConfig(newConfig *pogs.ClientConfig) *po
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
s.metrics.configVersion.Set(float64(newConfig.Version))
|
||||
|
||||
s.state.updateConfig(newConfig)
|
||||
var tunnelHostnames []h2mux.TunnelHostname
|
||||
|
||||
+22
-22
@@ -26,28 +26,28 @@ mcifak4CQsr+DH4pn5SJD7JxtCG3YGswW8QZsw==
|
||||
-----END CERTIFICATE-----
|
||||
Issuer: C=US, O=CloudFlare, Inc., OU=CloudFlare Origin SSL Certificate Authority, L=San Francisco, ST=California
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID/DCCAuagAwIBAgIID+rOSdTGfGcwCwYJKoZIhvcNAQELMIGLMQswCQYDVQQG
|
||||
EwJVUzEZMBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE0MDIGA1UECxMrQ2xvdWRG
|
||||
bGFyZSBPcmlnaW4gU1NMIENlcnRpZmljYXRlIEF1dGhvcml0eTEWMBQGA1UEBxMN
|
||||
U2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xNDExMTMyMDM4
|
||||
NTBaFw0xOTExMTQwMTQzNTBaMIGLMQswCQYDVQQGEwJVUzEZMBcGA1UEChMQQ2xv
|
||||
dWRGbGFyZSwgSW5jLjE0MDIGA1UECxMrQ2xvdWRGbGFyZSBPcmlnaW4gU1NMIENl
|
||||
cnRpZmljYXRlIEF1dGhvcml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEG
|
||||
A1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AMBIlWf1KEKR5hbB75OYrAcUXobpD/AxvSYRXr91mbRu+lqE7YbyyRUShQh15lem
|
||||
ef+umeEtPZoLFLhcLyczJxOhI+siLGDQm/a/UDkWvAXYa5DZ+pHU5ct5nZ8pGzqJ
|
||||
p8G1Hy5RMVYDXZT9F6EaHjMG0OOffH6Ih25TtgfyyrjXycwDH0u6GXt+G/rywcqz
|
||||
/9W4Aki3XNQMUHNQAtBLEEIYHMkyTYJxuL2tXO6ID5cCsoWw8meHufTeZW2DyUpl
|
||||
yP3AHt4149RQSyWZMJ6AyntL9d8Xhfpxd9rJkh9Kge2iV9rQTFuE1rRT5s7OSJcK
|
||||
xUsklgHcGHYMcNfNMilNHb8CAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgAGMBIGA1Ud
|
||||
EwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFCToU1ddfDRAh6nrlNu64RZ4/CmkMB8G
|
||||
A1UdIwQYMBaAFCToU1ddfDRAh6nrlNu64RZ4/CmkMAsGCSqGSIb3DQEBCwOCAQEA
|
||||
cQDBVAoRrhhsGegsSFsv1w8v27zzHKaJNv6ffLGIRvXK8VKKK0gKXh2zQtN9SnaD
|
||||
gYNe7Pr4C3I8ooYKRJJWLsmEHdGdnYYmj0OJfGrfQf6MLIc/11bQhLepZTxdhFYh
|
||||
QGgDl6gRmb8aDwk7Q92BPvek5nMzaWlP82ixavvYI+okoSY8pwdcVKobx6rWzMWz
|
||||
ZEC9M6H3F0dDYE23XcCFIdgNSAmmGyXPBstOe0aAJXwJTxOEPn36VWr0PKIQJy5Y
|
||||
4o1wpMpqCOIwWc8J9REV/REzN6Z1LXImdUgXIXOwrz56gKUJzPejtBQyIGj0mveX
|
||||
Fu6q54beR89jDc+oABmOgg==
|
||||
MIIEADCCAuigAwIBAgIID+rOSdTGfGcwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNV
|
||||
BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91
|
||||
ZEZsYXJlIE9yaWdpbiBTU0wgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQH
|
||||
Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMB4XDTE5MDgyMzIx
|
||||
MDgwMFoXDTI5MDgxNTE3MDAwMFowgYsxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBD
|
||||
bG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91ZEZsYXJlIE9yaWdpbiBTU0wg
|
||||
Q2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMw
|
||||
EQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAwEiVZ/UoQpHmFsHvk5isBxRehukP8DG9JhFev3WZtG76WoTthvLJFRKFCHXm
|
||||
V6Z5/66Z4S09mgsUuFwvJzMnE6Ej6yIsYNCb9r9QORa8BdhrkNn6kdTly3mdnykb
|
||||
OomnwbUfLlExVgNdlP0XoRoeMwbQ4598foiHblO2B/LKuNfJzAMfS7oZe34b+vLB
|
||||
yrP/1bgCSLdc1AxQc1AC0EsQQhgcyTJNgnG4va1c7ogPlwKyhbDyZ4e59N5lbYPJ
|
||||
SmXI/cAe3jXj1FBLJZkwnoDKe0v13xeF+nF32smSH0qB7aJX2tBMW4TWtFPmzs5I
|
||||
lwrFSySWAdwYdgxw180yKU0dvwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYD
|
||||
VR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUJOhTV118NECHqeuU27rhFnj8KaQw
|
||||
HwYDVR0jBBgwFoAUJOhTV118NECHqeuU27rhFnj8KaQwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBAHwOf9Ur1l0Ar5vFE6PNrZWrDfQIMyEfdgSKofCdTckbqXNTiXdgbHs+TWoQ
|
||||
wAB0pfJDAHJDXOTCWRyTeXOseeOi5Btj5CnEuw3P0oXqdqevM1/+uWp0CM35zgZ8
|
||||
VD4aITxity0djzE6Qnx3Syzz+ZkoBgTnNum7d9A66/V636x4vTeqbZFBr9erJzgz
|
||||
hhurjcoacvRNhnjtDRM0dPeiCJ50CP3wEYuvUzDHUaowOsnLCjQIkWbR7Ni6KEIk
|
||||
MOz2U0OBSif3FTkhCgZWQKOOLo1P42jHC3ssUZAtVNXrCk3fw9/E15k8NPkBazZ6
|
||||
0iykLhH1trywrKRMVw67F44IE8Y=
|
||||
-----END CERTIFICATE-----
|
||||
Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net
|
||||
-----BEGIN CERTIFICATE-----
|
||||
|
||||
@@ -83,7 +83,7 @@ func (u *UpstreamHTTPS) exchangeWireformat(msg []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/dns-message")
|
||||
req.Host = u.endpoint.Hostname()
|
||||
req.Host = u.endpoint.Host
|
||||
|
||||
resp, err := u.client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuthenticateResponse is the serialized response from the Authenticate RPC.
|
||||
// It's a 1:1 representation of the capnp message, so it's not very useful for programmers.
|
||||
// Instead, you should call the `Outcome()` method to get a programmer-friendly sum type, with one
|
||||
// case for each possible outcome.
|
||||
type AuthenticateResponse struct {
|
||||
PermanentErr string
|
||||
RetryableErr string
|
||||
Jwt []byte
|
||||
HoursUntilRefresh uint8
|
||||
}
|
||||
|
||||
// Outcome turns the deserialized response of Authenticate into a programmer-friendly sum type.
|
||||
func (ar AuthenticateResponse) Outcome() AuthOutcome {
|
||||
// If the user's authentication was unsuccessful, the server will return an error explaining why.
|
||||
// cloudflared should fatal with this error.
|
||||
if ar.PermanentErr != "" {
|
||||
return NewAuthFail(errors.New(ar.PermanentErr))
|
||||
}
|
||||
|
||||
// If there was a network error, then cloudflared should retry later,
|
||||
// because origintunneld couldn't prove whether auth was correct or not.
|
||||
if ar.RetryableErr != "" {
|
||||
return NewAuthUnknown(errors.New(ar.RetryableErr), ar.HoursUntilRefresh)
|
||||
}
|
||||
|
||||
// If auth succeeded, return the token and refresh it when instructed.
|
||||
if len(ar.Jwt) > 0 {
|
||||
return NewAuthSuccess(ar.Jwt, ar.HoursUntilRefresh)
|
||||
}
|
||||
|
||||
// Otherwise the state got messed up.
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthOutcome is a programmer-friendly sum type denoting the possible outcomes of Authenticate.
|
||||
//go-sumtype:decl AuthOutcome
|
||||
type AuthOutcome interface {
|
||||
isAuthOutcome()
|
||||
// Serialize into an AuthenticateResponse which can be sent via Capnp
|
||||
Serialize() AuthenticateResponse
|
||||
}
|
||||
|
||||
// AuthSuccess means the backend successfully authenticated this cloudflared.
|
||||
type AuthSuccess struct {
|
||||
jwt []byte
|
||||
hoursUntilRefresh uint8
|
||||
}
|
||||
|
||||
func NewAuthSuccess(jwt []byte, hoursUntilRefresh uint8) AuthSuccess {
|
||||
return AuthSuccess{jwt: jwt, hoursUntilRefresh: hoursUntilRefresh}
|
||||
}
|
||||
|
||||
func (ao AuthSuccess) JWT() []byte {
|
||||
return ao.jwt
|
||||
}
|
||||
|
||||
// RefreshAfter is how long cloudflared should wait before rerunning Authenticate.
|
||||
func (ao AuthSuccess) RefreshAfter() time.Duration {
|
||||
return hoursToTime(ao.hoursUntilRefresh)
|
||||
}
|
||||
|
||||
// Serialize into an AuthenticateResponse which can be sent via Capnp
|
||||
func (ao AuthSuccess) Serialize() AuthenticateResponse {
|
||||
return AuthenticateResponse{
|
||||
Jwt: ao.jwt,
|
||||
HoursUntilRefresh: ao.hoursUntilRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
func (ao AuthSuccess) isAuthOutcome() {}
|
||||
|
||||
// AuthFail means this cloudflared has the wrong auth and should exit.
|
||||
type AuthFail struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewAuthFail(err error) AuthFail {
|
||||
return AuthFail{err: err}
|
||||
}
|
||||
|
||||
func (ao AuthFail) Error() string {
|
||||
return ao.err.Error()
|
||||
}
|
||||
|
||||
// Serialize into an AuthenticateResponse which can be sent via Capnp
|
||||
func (ao AuthFail) Serialize() AuthenticateResponse {
|
||||
return AuthenticateResponse{
|
||||
PermanentErr: ao.err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ao AuthFail) isAuthOutcome() {}
|
||||
|
||||
// AuthUnknown means the backend couldn't finish checking authentication. Try again later.
|
||||
type AuthUnknown struct {
|
||||
err error
|
||||
hoursUntilRefresh uint8
|
||||
}
|
||||
|
||||
func NewAuthUnknown(err error, hoursUntilRefresh uint8) AuthUnknown {
|
||||
return AuthUnknown{err: err, hoursUntilRefresh: hoursUntilRefresh}
|
||||
}
|
||||
|
||||
func (ao AuthUnknown) Error() string {
|
||||
return ao.err.Error()
|
||||
}
|
||||
|
||||
// RefreshAfter is how long cloudflared should wait before rerunning Authenticate.
|
||||
func (ao AuthUnknown) RefreshAfter() time.Duration {
|
||||
return hoursToTime(ao.hoursUntilRefresh)
|
||||
}
|
||||
|
||||
// Serialize into an AuthenticateResponse which can be sent via Capnp
|
||||
func (ao AuthUnknown) Serialize() AuthenticateResponse {
|
||||
return AuthenticateResponse{
|
||||
RetryableErr: ao.err.Error(),
|
||||
HoursUntilRefresh: ao.hoursUntilRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
func (ao AuthUnknown) isAuthOutcome() {}
|
||||
|
||||
func hoursToTime(hours uint8) time.Duration {
|
||||
return time.Duration(hours) * time.Hour
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
"zombiezen.com/go/capnproto2/server"
|
||||
)
|
||||
|
||||
func (i TunnelServer_PogsImpl) Authenticate(p tunnelrpc.TunnelServer_authenticate) error {
|
||||
originCert, err := p.Params.OriginCert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostname, err := p.Params.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options, err := p.Params.Options()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pogsOptions, err := UnmarshalRegistrationOptions(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server.Ack(p.Options)
|
||||
resp, err := i.impl.Authenticate(p.Ctx, originCert, hostname, pogsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := p.Results.NewResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalAuthenticateResponse(result, resp)
|
||||
}
|
||||
|
||||
func MarshalAuthenticateResponse(s tunnelrpc.AuthenticateResponse, p *AuthenticateResponse) error {
|
||||
return pogs.Insert(tunnelrpc.AuthenticateResponse_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func (c TunnelServer_PogsClient) Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error) {
|
||||
client := tunnelrpc.TunnelServer{Client: c.Client}
|
||||
promise := client.Authenticate(ctx, func(p tunnelrpc.TunnelServer_authenticate_Params) error {
|
||||
err := p.SetOriginCert(originCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetHostname(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registrationOptions, err := p.NewOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalRegistrationOptions(registrationOptions, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalAuthenticateResponse(retval)
|
||||
}
|
||||
|
||||
func UnmarshalAuthenticateResponse(s tunnelrpc.AuthenticateResponse) (*AuthenticateResponse, error) {
|
||||
p := new(AuthenticateResponse)
|
||||
err := pogs.Extract(p, tunnelrpc.AuthenticateResponse_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
// Ensure the AuthOutcome sum is correct
|
||||
var _ AuthOutcome = &AuthSuccess{}
|
||||
var _ AuthOutcome = &AuthFail{}
|
||||
var _ AuthOutcome = &AuthUnknown{}
|
||||
|
||||
// Unit tests for AuthenticateResponse.Outcome()
|
||||
func TestAuthenticateResponseOutcome(t *testing.T) {
|
||||
type fields struct {
|
||||
PermanentErr string
|
||||
RetryableErr string
|
||||
Jwt []byte
|
||||
HoursUntilRefresh uint8
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want AuthOutcome
|
||||
}{
|
||||
{"success",
|
||||
fields{Jwt: []byte("asdf"), HoursUntilRefresh: 6},
|
||||
AuthSuccess{jwt: []byte("asdf"), hoursUntilRefresh: 6},
|
||||
},
|
||||
{"fail",
|
||||
fields{PermanentErr: "bad creds"},
|
||||
AuthFail{err: fmt.Errorf("bad creds")},
|
||||
},
|
||||
{"error",
|
||||
fields{RetryableErr: "bad conn", HoursUntilRefresh: 6},
|
||||
AuthUnknown{err: fmt.Errorf("bad conn"), hoursUntilRefresh: 6},
|
||||
},
|
||||
{"nil (no fields are set)",
|
||||
fields{},
|
||||
nil,
|
||||
},
|
||||
{"nil (too few fields are set)",
|
||||
fields{HoursUntilRefresh: 6},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ar := AuthenticateResponse{
|
||||
PermanentErr: tt.fields.PermanentErr,
|
||||
RetryableErr: tt.fields.RetryableErr,
|
||||
Jwt: tt.fields.Jwt,
|
||||
HoursUntilRefresh: tt.fields.HoursUntilRefresh,
|
||||
}
|
||||
got := ar.Outcome()
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AuthenticateResponse.Outcome() = %T, want %v", got, tt.want)
|
||||
}
|
||||
if got != nil && !reflect.DeepEqual(got.Serialize(), ar) {
|
||||
t.Errorf(".Outcome() and .Serialize() should be inverses but weren't. Expected %v, got %v", ar, got.Serialize())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthSuccess(t *testing.T) {
|
||||
input := NewAuthSuccess([]byte("asdf"), 6)
|
||||
output, ok := input.Serialize().Outcome().(AuthSuccess)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, input, output)
|
||||
}
|
||||
|
||||
func TestAuthUnknown(t *testing.T) {
|
||||
input := NewAuthUnknown(fmt.Errorf("pdx unreachable"), 6)
|
||||
output, ok := input.Serialize().Outcome().(AuthUnknown)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, input, output)
|
||||
}
|
||||
|
||||
func TestAuthFail(t *testing.T) {
|
||||
input := NewAuthFail(fmt.Errorf("wrong creds"))
|
||||
output, ok := input.Serialize().Outcome().(AuthFail)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, input, output)
|
||||
}
|
||||
|
||||
func TestWhenToRefresh(t *testing.T) {
|
||||
expected := 4 * time.Hour
|
||||
actual := hoursToTime(4)
|
||||
if expected != actual {
|
||||
t.Fatalf("expected %v hours, got %v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that serializing and deserializing AuthenticationResponse undo each other.
|
||||
func TestSerializeAuthenticationResponse(t *testing.T) {
|
||||
|
||||
tests := []*AuthenticateResponse{
|
||||
&AuthenticateResponse{
|
||||
Jwt: []byte("\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"),
|
||||
HoursUntilRefresh: 24,
|
||||
},
|
||||
&AuthenticateResponse{
|
||||
PermanentErr: "bad auth",
|
||||
},
|
||||
&AuthenticateResponse{
|
||||
RetryableErr: "bad connection",
|
||||
HoursUntilRefresh: 24,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range tests {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewAuthenticateResponse(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalAuthenticateResponse(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalAuthenticateResponse(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
@@ -197,11 +197,14 @@ func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialContext := (&net.Dialer{
|
||||
dialer := &net.Dialer{
|
||||
Timeout: hc.ProxyConnectionTimeout,
|
||||
KeepAlive: hc.TCPKeepAlive,
|
||||
DualStack: hc.DialDualStack,
|
||||
}).DialContext
|
||||
}
|
||||
if !hc.DialDualStack {
|
||||
dialer.FallbackDelay = -1
|
||||
}
|
||||
dialContext := dialer.DialContext
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialContext,
|
||||
@@ -270,7 +273,6 @@ func (*HelloWorldOriginConfig) Service() (originservice.OriginService, error) {
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).DialContext,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"zombiezen.com/go/capnproto2/server"
|
||||
)
|
||||
|
||||
func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnectTunnel) error {
|
||||
jwt, err := p.Params.Jwt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventDigest, err := p.Params.EventDigest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connDigest, err := p.Params.ConnDigest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostname, err := p.Params.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options, err := p.Params.Options()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pogsOptions, err := UnmarshalRegistrationOptions(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server.Ack(p.Options)
|
||||
registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, eventDigest, connDigest, hostname, pogsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := p.Results.NewResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalTunnelRegistration(result, registration)
|
||||
}
|
||||
|
||||
func (c TunnelServer_PogsClient) ReconnectTunnel(
|
||||
ctx context.Context,
|
||||
jwt,
|
||||
eventDigest []byte,
|
||||
connDigest []byte,
|
||||
hostname string,
|
||||
options *RegistrationOptions,
|
||||
) *TunnelRegistration {
|
||||
client := tunnelrpc.TunnelServer{Client: c.Client}
|
||||
promise := client.ReconnectTunnel(ctx, func(p tunnelrpc.TunnelServer_reconnectTunnel_Params) error {
|
||||
err := p.SetJwt(jwt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetEventDigest(eventDigest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetConnDigest(connDigest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.SetHostname(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registrationOptions, err := p.NewOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalRegistrationOptions(registrationOptions, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize()
|
||||
}
|
||||
registration, err := UnmarshalTunnelRegistration(retval)
|
||||
if err != nil {
|
||||
return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize()
|
||||
}
|
||||
return registration
|
||||
}
|
||||
+127
-15
@@ -9,13 +9,16 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
"zombiezen.com/go/capnproto2/server"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRetryAfterSeconds = 15
|
||||
)
|
||||
|
||||
type Authentication struct {
|
||||
Key string
|
||||
Email string
|
||||
@@ -33,11 +36,115 @@ func UnmarshalAuthentication(s tunnelrpc.Authentication) (*Authentication, error
|
||||
}
|
||||
|
||||
type TunnelRegistration struct {
|
||||
Err string
|
||||
Url string
|
||||
LogLines []string
|
||||
PermanentFailure bool
|
||||
TunnelID string `capnp:"tunnelID"`
|
||||
SuccessfulTunnelRegistration
|
||||
Err string
|
||||
PermanentFailure bool
|
||||
RetryAfterSeconds uint16
|
||||
}
|
||||
|
||||
type SuccessfulTunnelRegistration struct {
|
||||
Url string
|
||||
LogLines []string
|
||||
TunnelID string `capnp:"tunnelID"`
|
||||
EventDigest []byte
|
||||
ConnDigest []byte
|
||||
}
|
||||
|
||||
func NewSuccessfulTunnelRegistration(
|
||||
url string,
|
||||
logLines []string,
|
||||
tunnelID string,
|
||||
eventDigest []byte,
|
||||
connDigest []byte,
|
||||
) *TunnelRegistration {
|
||||
// Marshal nil will result in an error
|
||||
if logLines == nil {
|
||||
logLines = []string{}
|
||||
}
|
||||
return &TunnelRegistration{
|
||||
SuccessfulTunnelRegistration: SuccessfulTunnelRegistration{
|
||||
Url: url,
|
||||
LogLines: logLines,
|
||||
TunnelID: tunnelID,
|
||||
EventDigest: eventDigest,
|
||||
ConnDigest: connDigest,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Not calling this function Error() to avoid confusion with implementing error interface
|
||||
func (tr TunnelRegistration) DeserializeError() TunnelRegistrationError {
|
||||
if tr.Err != "" {
|
||||
err := fmt.Errorf(tr.Err)
|
||||
if tr.PermanentFailure {
|
||||
return NewPermanentRegistrationError(err)
|
||||
}
|
||||
retryAfterSeconds := tr.RetryAfterSeconds
|
||||
if retryAfterSeconds < defaultRetryAfterSeconds {
|
||||
retryAfterSeconds = defaultRetryAfterSeconds
|
||||
}
|
||||
return NewRetryableRegistrationError(err, retryAfterSeconds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TunnelRegistrationError interface {
|
||||
error
|
||||
Serialize() *TunnelRegistration
|
||||
IsPermanent() bool
|
||||
}
|
||||
|
||||
type PermanentRegistrationError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func NewPermanentRegistrationError(err error) TunnelRegistrationError {
|
||||
return &PermanentRegistrationError{
|
||||
err: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func (pre *PermanentRegistrationError) Error() string {
|
||||
return pre.err
|
||||
}
|
||||
|
||||
func (pre *PermanentRegistrationError) Serialize() *TunnelRegistration {
|
||||
return &TunnelRegistration{
|
||||
Err: pre.err,
|
||||
PermanentFailure: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (*PermanentRegistrationError) IsPermanent() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type RetryableRegistrationError struct {
|
||||
err string
|
||||
retryAfterSeconds uint16
|
||||
}
|
||||
|
||||
func NewRetryableRegistrationError(err error, retryAfterSeconds uint16) TunnelRegistrationError {
|
||||
return &RetryableRegistrationError{
|
||||
err: err.Error(),
|
||||
retryAfterSeconds: retryAfterSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
func (rre *RetryableRegistrationError) Error() string {
|
||||
return rre.err
|
||||
}
|
||||
|
||||
func (rre *RetryableRegistrationError) Serialize() *TunnelRegistration {
|
||||
return &TunnelRegistration{
|
||||
Err: rre.err,
|
||||
PermanentFailure: false,
|
||||
RetryAfterSeconds: rre.retryAfterSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
func (*RetryableRegistrationError) IsPermanent() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func MarshalTunnelRegistration(s tunnelrpc.TunnelRegistration, p *TunnelRegistration) error {
|
||||
@@ -63,6 +170,8 @@ type RegistrationOptions struct {
|
||||
RunFromTerminal bool `capnp:"runFromTerminal"`
|
||||
CompressionQuality uint64 `capnp:"compressionQuality"`
|
||||
UUID string `capnp:"uuid"`
|
||||
NumPreviousAttempts uint8
|
||||
Features []string
|
||||
}
|
||||
|
||||
func MarshalRegistrationOptions(s tunnelrpc.RegistrationOptions, p *RegistrationOptions) error {
|
||||
@@ -323,10 +432,12 @@ func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectPar
|
||||
}
|
||||
|
||||
type TunnelServer interface {
|
||||
RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) *TunnelRegistration
|
||||
GetServerInfo(ctx context.Context) (*ServerInfo, error)
|
||||
UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error
|
||||
Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error)
|
||||
Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error)
|
||||
ReconnectTunnel(ctx context.Context, jwt, eventDigest, connDigest []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
}
|
||||
|
||||
func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer {
|
||||
@@ -355,15 +466,12 @@ func (i TunnelServer_PogsImpl) RegisterTunnel(p tunnelrpc.TunnelServer_registerT
|
||||
return err
|
||||
}
|
||||
server.Ack(p.Options)
|
||||
registration, err := i.impl.RegisterTunnel(p.Ctx, originCert, hostname, pogsOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registration := i.impl.RegisterTunnel(p.Ctx, originCert, hostname, pogsOptions)
|
||||
|
||||
result, err := p.Results.NewResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(registration.TunnelID)
|
||||
return MarshalTunnelRegistration(result, registration)
|
||||
}
|
||||
|
||||
@@ -416,7 +524,7 @@ func (c TunnelServer_PogsClient) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) {
|
||||
func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) *TunnelRegistration {
|
||||
client := tunnelrpc.TunnelServer{Client: c.Client}
|
||||
promise := client.RegisterTunnel(ctx, func(p tunnelrpc.TunnelServer_registerTunnel_Params) error {
|
||||
err := p.SetOriginCert(originCert)
|
||||
@@ -439,9 +547,13 @@ func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize()
|
||||
}
|
||||
return UnmarshalTunnelRegistration(retval)
|
||||
registration, err := UnmarshalTunnelRegistration(retval)
|
||||
if err != nil {
|
||||
return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize()
|
||||
}
|
||||
return registration
|
||||
}
|
||||
|
||||
func (c TunnelServer_PogsClient) GetServerInfo(ctx context.Context) (*ServerInfo, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -11,6 +12,51 @@ import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
const (
|
||||
testURL = "tunnel.example.com"
|
||||
testTunnelID = "asdfghjkl;"
|
||||
testRetryAfterSeconds = 19
|
||||
)
|
||||
|
||||
var (
|
||||
testErr = fmt.Errorf("Invalid credential")
|
||||
testLogLines = []string{"all", "working"}
|
||||
testEventDigest = []byte("asdf")
|
||||
testConnDigest = []byte("lkjh")
|
||||
)
|
||||
|
||||
// *PermanentRegistrationError implements TunnelRegistrationError
|
||||
var _ TunnelRegistrationError = (*PermanentRegistrationError)(nil)
|
||||
|
||||
// *RetryableRegistrationError implements TunnelRegistrationError
|
||||
var _ TunnelRegistrationError = (*RetryableRegistrationError)(nil)
|
||||
|
||||
func TestTunnelRegistration(t *testing.T) {
|
||||
testCases := []*TunnelRegistration{
|
||||
NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID, testEventDigest, testConnDigest),
|
||||
NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID, testEventDigest, testConnDigest),
|
||||
NewPermanentRegistrationError(testErr).Serialize(),
|
||||
NewRetryableRegistrationError(testErr, testRetryAfterSeconds).Serialize(),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewTunnelRegistration(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalTunnelRegistration(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase #%v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalTunnelRegistration(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase #%v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConnectResult(t *testing.T) {
|
||||
testCases := []ConnectResult{
|
||||
&ConnectError{
|
||||
|
||||
@@ -19,6 +19,12 @@ struct TunnelRegistration {
|
||||
permanentFailure @3 :Bool;
|
||||
# Displayed to user
|
||||
tunnelID @4 :Text;
|
||||
# How long should this connection wait to retry in seconds, if the error wasn't permanent
|
||||
retryAfterSeconds @5 :UInt16;
|
||||
# A unique ID used to reconnect this tunnel.
|
||||
eventDigest @6 :Data;
|
||||
# A unique ID used to prove this tunnel was previously connected to a given metal.
|
||||
connDigest @7 :Data;
|
||||
}
|
||||
|
||||
struct RegistrationOptions {
|
||||
@@ -44,6 +50,10 @@ struct RegistrationOptions {
|
||||
# cross stream compression setting, 0 - off, 3 - high
|
||||
compressionQuality @10 :UInt64;
|
||||
uuid @11 :Text;
|
||||
# number of previous attempts to send RegisterTunnel/ReconnectTunnel
|
||||
numPreviousAttempts @12 :UInt8;
|
||||
# Set of features this cloudflared knows it supports
|
||||
features @13 :List(Text);
|
||||
}
|
||||
|
||||
struct CapnpConnectParameters {
|
||||
@@ -274,11 +284,20 @@ struct FailedConfig {
|
||||
reason @4 :Text;
|
||||
}
|
||||
|
||||
struct AuthenticateResponse {
|
||||
permanentErr @0 :Text;
|
||||
retryableErr @1 :Text;
|
||||
jwt @2 :Data;
|
||||
hoursUntilRefresh @3 :UInt8;
|
||||
}
|
||||
|
||||
interface TunnelServer {
|
||||
registerTunnel @0 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
|
||||
getServerInfo @1 () -> (result :ServerInfo);
|
||||
unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> ();
|
||||
connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult);
|
||||
authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse);
|
||||
reconnectTunnel @5 (jwt :Data, eventDigest :Data, connDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration);
|
||||
}
|
||||
|
||||
interface ClientService {
|
||||
|
||||
+965
-229
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -11,13 +12,19 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
"gopkg.in/coreos/go-oidc.v2"
|
||||
)
|
||||
|
||||
const defaultScheme = "http"
|
||||
const (
|
||||
defaultScheme = "http"
|
||||
accessDomain = "cloudflareaccess.com"
|
||||
accessCertPath = "/cdn-cgi/access/certs"
|
||||
accessJwtHeader = "Cf-access-jwt-assertion"
|
||||
)
|
||||
|
||||
var (
|
||||
supportedProtocols = []string{"http", "https", "rdp"}
|
||||
validationTimeout = time.Duration(30 * time.Second)
|
||||
validationTimeout = time.Duration(30 * time.Second)
|
||||
)
|
||||
|
||||
func ValidateHostname(hostname string) (string, error) {
|
||||
@@ -53,6 +60,12 @@ func ValidateHostname(hostname string) (string, error) {
|
||||
|
||||
}
|
||||
|
||||
// ValidateUrl returns a validated version of `originUrl` with a scheme prepended (by default http://).
|
||||
// Note: when originUrl contains a scheme, the path is removed:
|
||||
// ValidateUrl("https://localhost:8080/api/") => "https://localhost:8080"
|
||||
// but when it does not, the path is preserved:
|
||||
// ValidateUrl("localhost:8080/api/") => "http://localhost:8080/api/"
|
||||
// This is arguably a bug, but changing it might break some cloudflared users.
|
||||
func ValidateUrl(originUrl string) (string, error) {
|
||||
if originUrl == "" {
|
||||
return "", fmt.Errorf("URL should not be empty")
|
||||
@@ -114,6 +127,8 @@ func ValidateUrl(originUrl string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("URL %s has invalid format", originUrl)
|
||||
}
|
||||
// This is why the path is preserved when `originUrl` doesn't have a schema.
|
||||
// Using `parsedUrl.Port()` here, instead of `port`, would remove the path
|
||||
return fmt.Sprintf("%s://%s", defaultScheme, net.JoinHostPort(hostname, port)), nil
|
||||
}
|
||||
}
|
||||
@@ -175,10 +190,11 @@ func ValidateHTTPService(originURL string, hostname string, transport http.Round
|
||||
_, secondErr := client.Do(secondRequest)
|
||||
if secondErr == nil { // Worked this time--advise the user to switch protocols
|
||||
return errors.Errorf(
|
||||
"%s doesn't seem to work over %s, but does seem to work over %s. Consider changing the origin URL to %s",
|
||||
"%s doesn't seem to work over %s, but does seem to work over %s. Reason: %v. Consider changing the origin URL to %s",
|
||||
parsedURL.Host,
|
||||
oldScheme,
|
||||
parsedURL.Scheme,
|
||||
initialErr,
|
||||
parsedURL,
|
||||
)
|
||||
}
|
||||
@@ -197,3 +213,50 @@ func toggleProtocol(httpProtocol string) string {
|
||||
return httpProtocol
|
||||
}
|
||||
}
|
||||
|
||||
// Access checks if a JWT from Cloudflare Access is valid.
|
||||
type Access struct {
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
func NewAccessValidator(ctx context.Context, domain, issuer, applicationAUD string) (*Access, error) {
|
||||
domainURL, err := ValidateUrl(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issuerURL, err := ValidateUrl(issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An issuerURL from Cloudflare Access will always use HTTPS.
|
||||
issuerURL = strings.Replace(issuerURL, "http:", "https:", 1)
|
||||
|
||||
keySet := oidc.NewRemoteKeySet(ctx, domainURL+accessCertPath)
|
||||
return &Access{oidc.NewVerifier(issuerURL, keySet, &oidc.Config{ClientID: applicationAUD})}, nil
|
||||
}
|
||||
|
||||
func (a *Access) Validate(ctx context.Context, jwt string) error {
|
||||
token, err := a.verifier.Verify(ctx, jwt)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "token is invalid: %s", jwt)
|
||||
}
|
||||
|
||||
// Perform extra sanity checks, just to be safe.
|
||||
|
||||
if token == nil {
|
||||
return fmt.Errorf("token is nil: %s", jwt)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(token.Issuer, accessDomain) {
|
||||
return fmt.Errorf("token has non-cloudflare issuer of %s: %s", token.Issuer, jwt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Access) ValidateRequest(ctx context.Context, r *http.Request) error {
|
||||
return a.Validate(ctx, r.Header.Get(accessJwtHeader))
|
||||
}
|
||||
|
||||
@@ -53,98 +53,65 @@ func TestValidateHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateUrl(t *testing.T) {
|
||||
type testCase struct {
|
||||
input string
|
||||
expectedOutput string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{"http://localhost", "http://localhost"},
|
||||
{"http://localhost/", "http://localhost"},
|
||||
{"http://localhost/api", "http://localhost"},
|
||||
{"http://localhost/api/", "http://localhost"},
|
||||
{"https://localhost", "https://localhost"},
|
||||
{"https://localhost/", "https://localhost"},
|
||||
{"https://localhost/api", "https://localhost"},
|
||||
{"https://localhost/api/", "https://localhost"},
|
||||
{"https://localhost:8080", "https://localhost:8080"},
|
||||
{"https://localhost:8080/", "https://localhost:8080"},
|
||||
{"https://localhost:8080/api", "https://localhost:8080"},
|
||||
{"https://localhost:8080/api/", "https://localhost:8080"},
|
||||
{"localhost", "http://localhost"},
|
||||
{"localhost/", "http://localhost/"},
|
||||
{"localhost/api", "http://localhost/api"},
|
||||
{"localhost/api/", "http://localhost/api/"},
|
||||
{"localhost:8080", "http://localhost:8080"},
|
||||
{"localhost:8080/", "http://localhost:8080/"},
|
||||
{"localhost:8080/api", "http://localhost:8080/api"},
|
||||
{"localhost:8080/api/", "http://localhost:8080/api/"},
|
||||
{"localhost:8080/api/?asdf", "http://localhost:8080/api/?asdf"},
|
||||
{"http://127.0.0.1:8080", "http://127.0.0.1:8080"},
|
||||
{"127.0.0.1:8080", "http://127.0.0.1:8080"},
|
||||
{"127.0.0.1", "http://127.0.0.1"},
|
||||
{"https://127.0.0.1:8080", "https://127.0.0.1:8080"},
|
||||
{"[::1]:8080", "http://[::1]:8080"},
|
||||
{"http://[::1]", "http://[::1]"},
|
||||
{"http://[::1]:8080", "http://[::1]:8080"},
|
||||
{"[::1]", "http://[::1]"},
|
||||
{"https://example.com", "https://example.com"},
|
||||
{"example.com", "http://example.com"},
|
||||
{"http://hello.example.com", "http://hello.example.com"},
|
||||
{"hello.example.com", "http://hello.example.com"},
|
||||
{"hello.example.com:8080", "http://hello.example.com:8080"},
|
||||
{"https://hello.example.com:8080", "https://hello.example.com:8080"},
|
||||
{"https://bücher.example.com", "https://xn--bcher-kva.example.com"},
|
||||
{"bücher.example.com", "http://xn--bcher-kva.example.com"},
|
||||
{"https%3A%2F%2Fhello.example.com", "https://hello.example.com"},
|
||||
{"https://alex:12345@hello.example.com:8080", "https://hello.example.com:8080"},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
validUrl, err := ValidateUrl(testCase.input)
|
||||
assert.NoError(t, err, "test case %v", i)
|
||||
assert.Equal(t, testCase.expectedOutput, validUrl, "test case %v", i)
|
||||
}
|
||||
|
||||
validUrl, err := ValidateUrl("")
|
||||
assert.Equal(t, fmt.Errorf("URL should not be empty"), err)
|
||||
assert.Empty(t, validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://localhost:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://localhost:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("localhost:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://localhost:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("http://localhost")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://localhost", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("http://127.0.0.1:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://127.0.0.1:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("127.0.0.1:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://127.0.0.1:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("127.0.0.1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://127.0.0.1", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://127.0.0.1:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://127.0.0.1:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("[::1]:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://[::1]:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("http://[::1]")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://[::1]", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("http://[::1]:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://[::1]:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("[::1]")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://[::1]", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("http://hello.example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://hello.example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("hello.example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://hello.example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("hello.example.com:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://hello.example.com:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://hello.example.com:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://hello.example.com:8080", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://bücher.example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://xn--bcher-kva.example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("bücher.example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "http://xn--bcher-kva.example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https%3A%2F%2Fhello.example.com")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://hello.example.com", validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("ftp://alex:12345@hello.example.com:8080/robot.txt")
|
||||
assert.Equal(t, "Currently Argo Tunnel does not support ftp protocol.", err.Error())
|
||||
assert.Empty(t, validUrl)
|
||||
|
||||
validUrl, err = ValidateUrl("https://alex:12345@hello.example.com:8080")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "https://hello.example.com:8080", validUrl)
|
||||
|
||||
}
|
||||
|
||||
func TestToggleProtocol(t *testing.T) {
|
||||
@@ -320,6 +287,39 @@ func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccessValidatorOk(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
url := "test.cloudflareaccess.com"
|
||||
access, err := NewAccessValidator(ctx, url, url, "")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, access)
|
||||
|
||||
assert.Error(t, access.Validate(ctx, ""))
|
||||
assert.Error(t, access.Validate(ctx, "invalid"))
|
||||
|
||||
req := httptest.NewRequest("GET", "https://test.cloudflareaccess.com", nil)
|
||||
req.Header.Set(accessJwtHeader, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
|
||||
assert.Error(t, access.ValidateRequest(ctx, req))
|
||||
}
|
||||
|
||||
func TestNewAccessValidatorErr(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
urls := []string{
|
||||
"",
|
||||
"tcp://test.cloudflareaccess.com",
|
||||
"wss://cloudflarenone.com",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
access, err := NewAccessValidator(ctx, url, url, "")
|
||||
|
||||
assert.Error(t, err, url)
|
||||
assert.Nil(t, access)
|
||||
}
|
||||
}
|
||||
|
||||
type testRoundTripper func(req *http.Request) (*http.Response, error)
|
||||
|
||||
func (f testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
/examples/blog/blog
|
||||
/examples/orders/orders
|
||||
/examples/basic/basic
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
language: go
|
||||
|
||||
go_import_path: github.com/DATA-DOG/go-sqlmock
|
||||
|
||||
go:
|
||||
- 1.2.x
|
||||
- 1.3.x
|
||||
- 1.4 # has no cover tool for latest releases
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
|
||||
sudo: false
|
||||
|
||||
script:
|
||||
- go vet
|
||||
- test -z "$(go fmt ./...)" # fail if not formatted properly
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
The three clause BSD license (http://en.wikipedia.org/wiki/BSD_licenses)
|
||||
|
||||
Copyright (c) 2013-2019, DATA-DOG team
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* The name DataDog.lt may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
[](https://travis-ci.org/DATA-DOG/go-sqlmock)
|
||||
[](https://godoc.org/github.com/DATA-DOG/go-sqlmock)
|
||||
[](https://goreportcard.com/report/github.com/DATA-DOG/go-sqlmock)
|
||||
[](https://codecov.io/github/DATA-DOG/go-sqlmock)
|
||||
|
||||
# Sql driver mock for Golang
|
||||
|
||||
**sqlmock** is a mock library implementing [sql/driver](https://godoc.org/database/sql/driver). Which has one and only
|
||||
purpose - to simulate any **sql** driver behavior in tests, without needing a real database connection. It helps to
|
||||
maintain correct **TDD** workflow.
|
||||
|
||||
- this library is now complete and stable. (you may not find new changes for this reason)
|
||||
- supports concurrency and multiple connections.
|
||||
- supports **go1.8** Context related feature mocking and Named sql parameters.
|
||||
- does not require any modifications to your source code.
|
||||
- the driver allows to mock any sql driver method behavior.
|
||||
- has strict by default expectation order matching.
|
||||
- has no third party dependencies.
|
||||
|
||||
**NOTE:** in **v1.2.0** **sqlmock.Rows** has changed to struct from interface, if you were using any type references to that
|
||||
interface, you will need to switch it to a pointer struct type. Also, **sqlmock.Rows** were used to implement **driver.Rows**
|
||||
interface, which was not required or useful for mocking and was removed. Hope it will not cause issues.
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/DATA-DOG/go-sqlmock
|
||||
|
||||
## Documentation and Examples
|
||||
|
||||
Visit [godoc](http://godoc.org/github.com/DATA-DOG/go-sqlmock) for general examples and public api reference.
|
||||
See **.travis.yml** for supported **go** versions.
|
||||
Different use case, is to functionally test with a real database - [go-txdb](https://github.com/DATA-DOG/go-txdb)
|
||||
all database related actions are isolated within a single transaction so the database can remain in the same state.
|
||||
|
||||
See implementation examples:
|
||||
|
||||
- [blog API server](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/blog)
|
||||
- [the same orders example](https://github.com/DATA-DOG/go-sqlmock/tree/master/examples/orders)
|
||||
|
||||
### Something you may want to test, assuming you use the [go-mysql-driver](https://github.com/go-sql-driver/mysql)
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func recordStats(db *sql.DB, userID, productID int64) (err error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
switch err {
|
||||
case nil:
|
||||
err = tx.Commit()
|
||||
default:
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
// @NOTE: the real connection is not required for tests
|
||||
db, err := sql.Open("mysql", "root@/blog")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tests with sqlmock
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
// a successful case
|
||||
func TestShouldUpdateStats(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
// now we execute our method
|
||||
if err = recordStats(db, 2, 3); err != nil {
|
||||
t.Errorf("error was not expected while updating stats: %s", err)
|
||||
}
|
||||
|
||||
// we make sure that all expectations were met
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// a failing test case
|
||||
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("INSERT INTO product_viewers").
|
||||
WithArgs(2, 3).
|
||||
WillReturnError(fmt.Errorf("some error"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
// now we execute our method
|
||||
if err = recordStats(db, 2, 3); err == nil {
|
||||
t.Errorf("was expecting an error, but there was none")
|
||||
}
|
||||
|
||||
// we make sure that all expectations were met
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customize SQL query matching
|
||||
|
||||
There were plenty of requests from users regarding SQL query string validation or different matching option.
|
||||
We have now implemented the `QueryMatcher` interface, which can be passed through an option when calling
|
||||
`sqlmock.New` or `sqlmock.NewWithDSN`.
|
||||
|
||||
This now allows to include some library, which would allow for example to parse and validate `mysql` SQL AST.
|
||||
And create a custom QueryMatcher in order to validate SQL in sophisticated ways.
|
||||
|
||||
By default, **sqlmock** is preserving backward compatibility and default query matcher is `sqlmock.QueryMatcherRegexp`
|
||||
which uses expected SQL string as a regular expression to match incoming query string. There is an equality matcher:
|
||||
`QueryMatcherEqual` which will do a full case sensitive match.
|
||||
|
||||
In order to customize the QueryMatcher, use the following:
|
||||
|
||||
``` go
|
||||
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
||||
```
|
||||
|
||||
The query matcher can be fully customized based on user needs. **sqlmock** will not
|
||||
provide a standard sql parsing matchers, since various drivers may not follow the same SQL standard.
|
||||
|
||||
## Matching arguments like time.Time
|
||||
|
||||
There may be arguments which are of `struct` type and cannot be compared easily by value like `time.Time`. In this case
|
||||
**sqlmock** provides an [Argument](https://godoc.org/github.com/DATA-DOG/go-sqlmock#Argument) interface which
|
||||
can be used in more sophisticated matching. Here is a simple example of time argument matching:
|
||||
|
||||
``` go
|
||||
type AnyTime struct{}
|
||||
|
||||
// Match satisfies sqlmock.Argument interface
|
||||
func (a AnyTime) Match(v driver.Value) bool {
|
||||
_, ok := v.(time.Time)
|
||||
return ok
|
||||
}
|
||||
|
||||
func TestAnyTimeArgument(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, mock, err := New()
|
||||
if err != nil {
|
||||
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
mock.ExpectExec("INSERT INTO users").
|
||||
WithArgs("john", AnyTime{}).
|
||||
WillReturnResult(NewResult(1, 1))
|
||||
|
||||
_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("error '%s' was not expected, while inserting a row", err)
|
||||
}
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("there were unfulfilled expectations: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It only asserts that argument is of `time.Time` type.
|
||||
|
||||
## Run tests
|
||||
|
||||
go test -race
|
||||
|
||||
## Change Log
|
||||
|
||||
- **2019-02-13** - added `go.mod` removed the references and suggestions using `gopkg.in`.
|
||||
- **2018-12-11** - added expectation of Rows to be closed, while mocking expected query.
|
||||
- **2018-12-11** - introduced an option to provide **QueryMatcher** in order to customize SQL query matching.
|
||||
- **2017-09-01** - it is now possible to expect that prepared statement will be closed,
|
||||
using **ExpectedPrepare.WillBeClosed**.
|
||||
- **2017-02-09** - implemented support for **go1.8** features. **Rows** interface was changed to struct
|
||||
but contains all methods as before and should maintain backwards compatibility. **ExpectedQuery.WillReturnRows** may now
|
||||
accept multiple row sets.
|
||||
- **2016-11-02** - `db.Prepare()` was not validating expected prepare SQL
|
||||
query. It should still be validated even if Exec or Query is not
|
||||
executed on that prepared statement.
|
||||
- **2016-02-23** - added **sqlmock.AnyArg()** function to provide any kind
|
||||
of argument matcher.
|
||||
- **2016-02-23** - convert expected arguments to driver.Value as natural
|
||||
driver does, the change may affect time.Time comparison and will be
|
||||
stricter. See [issue](https://github.com/DATA-DOG/go-sqlmock/issues/31).
|
||||
- **2015-08-27** - **v1** api change, concurrency support, all known issues fixed.
|
||||
- **2014-08-16** instead of **panic** during reflect type mismatch when comparing query arguments - now return error
|
||||
- **2014-08-14** added **sqlmock.NewErrorResult** which gives an option to return driver.Result with errors for
|
||||
interface methods, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/5)
|
||||
- **2014-05-29** allow to match arguments in more sophisticated ways, by providing an **sqlmock.Argument** interface
|
||||
- **2014-04-21** introduce **sqlmock.New()** to open a mock database connection for tests. This method
|
||||
calls sql.DB.Ping to ensure that connection is open, see [issue](https://github.com/DATA-DOG/go-sqlmock/issues/4).
|
||||
This way on Close it will surely assert if all expectations are met, even if database was not triggered at all.
|
||||
The old way is still available, but it is advisable to call db.Ping manually before asserting with db.Close.
|
||||
- **2014-02-14** RowsFromCSVString is now a part of Rows interface named as FromCSVString.
|
||||
It has changed to allow more ways to construct rows and to easily extend this API in future.
|
||||
See [issue 1](https://github.com/DATA-DOG/go-sqlmock/issues/1)
|
||||
**RowsFromCSVString** is deprecated and will be removed in future
|
||||
|
||||
## Contributions
|
||||
|
||||
Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) -
|
||||
please open an issue before, to discuss whether these changes can be accepted. All backward incompatible changes are
|
||||
and will be treated cautiously
|
||||
|
||||
## License
|
||||
|
||||
The [three clause BSD license](http://en.wikipedia.org/wiki/BSD_licenses)
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package sqlmock
|
||||
|
||||
import "database/sql/driver"
|
||||
|
||||
// Argument interface allows to match
|
||||
// any argument in specific way when used with
|
||||
// ExpectedQuery and ExpectedExec expectations.
|
||||
type Argument interface {
|
||||
Match(driver.Value) bool
|
||||
}
|
||||
|
||||
// AnyArg will return an Argument which can
|
||||
// match any kind of arguments.
|
||||
//
|
||||
// Useful for time.Time or similar kinds of arguments.
|
||||
func AnyArg() Argument {
|
||||
return anyArgument{}
|
||||
}
|
||||
|
||||
type anyArgument struct{}
|
||||
|
||||
func (a anyArgument) Match(_ driver.Value) bool {
|
||||
return true
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package sqlmock
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var pool *mockDriver
|
||||
|
||||
func init() {
|
||||
pool = &mockDriver{
|
||||
conns: make(map[string]*sqlmock),
|
||||
}
|
||||
sql.Register("sqlmock", pool)
|
||||
}
|
||||
|
||||
type mockDriver struct {
|
||||
sync.Mutex
|
||||
counter int
|
||||
conns map[string]*sqlmock
|
||||
}
|
||||
|
||||
func (d *mockDriver) Open(dsn string) (driver.Conn, error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
c, ok := d.conns[dsn]
|
||||
if !ok {
|
||||
return c, fmt.Errorf("expected a connection to be available, but it is not")
|
||||
}
|
||||
|
||||
c.opened++
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New creates sqlmock database connection and a mock to manage expectations.
|
||||
// Accepts options, like ValueConverterOption, to use a ValueConverter from
|
||||
// a specific driver.
|
||||
// Pings db so that all expectations could be
|
||||
// asserted.
|
||||
func New(options ...func(*sqlmock) error) (*sql.DB, Sqlmock, error) {
|
||||
pool.Lock()
|
||||
dsn := fmt.Sprintf("sqlmock_db_%d", pool.counter)
|
||||
pool.counter++
|
||||
|
||||
smock := &sqlmock{dsn: dsn, drv: pool, ordered: true}
|
||||
pool.conns[dsn] = smock
|
||||
pool.Unlock()
|
||||
|
||||
return smock.open(options)
|
||||
}
|
||||
|
||||
// NewWithDSN creates sqlmock database connection with a specific DSN
|
||||
// and a mock to manage expectations.
|
||||
// Accepts options, like ValueConverterOption, to use a ValueConverter from
|
||||
// a specific driver.
|
||||
// Pings db so that all expectations could be asserted.
|
||||
//
|
||||
// This method is introduced because of sql abstraction
|
||||
// libraries, which do not provide a way to initialize
|
||||
// with sql.DB instance. For example GORM library.
|
||||
//
|
||||
// Note, it will error if attempted to create with an
|
||||
// already used dsn
|
||||
//
|
||||
// It is not recommended to use this method, unless you
|
||||
// really need it and there is no other way around.
|
||||
func NewWithDSN(dsn string, options ...func(*sqlmock) error) (*sql.DB, Sqlmock, error) {
|
||||
pool.Lock()
|
||||
if _, ok := pool.conns[dsn]; ok {
|
||||
pool.Unlock()
|
||||
return nil, nil, fmt.Errorf("cannot create a new mock database with the same dsn: %s", dsn)
|
||||
}
|
||||
smock := &sqlmock{dsn: dsn, drv: pool, ordered: true}
|
||||
pool.conns[dsn] = smock
|
||||
pool.Unlock()
|
||||
|
||||
return smock.open(options)
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
package sqlmock
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// an expectation interface
|
||||
type expectation interface {
|
||||
fulfilled() bool
|
||||
Lock()
|
||||
Unlock()
|
||||
String() string
|
||||
}
|
||||
|
||||
// common expectation struct
|
||||
// satisfies the expectation interface
|
||||
type commonExpectation struct {
|
||||
sync.Mutex
|
||||
triggered bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *commonExpectation) fulfilled() bool {
|
||||
return e.triggered
|
||||
}
|
||||
|
||||
// ExpectedClose is used to manage *sql.DB.Close expectation
|
||||
// returned by *Sqlmock.ExpectClose.
|
||||
type ExpectedClose struct {
|
||||
commonExpectation
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for *sql.DB.Close action
|
||||
func (e *ExpectedClose) WillReturnError(err error) *ExpectedClose {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedClose) String() string {
|
||||
msg := "ExpectedClose => expecting database Close"
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf(", which should return error: %s", e.err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// ExpectedBegin is used to manage *sql.DB.Begin expectation
|
||||
// returned by *Sqlmock.ExpectBegin.
|
||||
type ExpectedBegin struct {
|
||||
commonExpectation
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for *sql.DB.Begin action
|
||||
func (e *ExpectedBegin) WillReturnError(err error) *ExpectedBegin {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedBegin) String() string {
|
||||
msg := "ExpectedBegin => expecting database transaction Begin"
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf(", which should return error: %s", e.err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// WillDelayFor allows to specify duration for which it will delay
|
||||
// result. May be used together with Context
|
||||
func (e *ExpectedBegin) WillDelayFor(duration time.Duration) *ExpectedBegin {
|
||||
e.delay = duration
|
||||
return e
|
||||
}
|
||||
|
||||
// ExpectedCommit is used to manage *sql.Tx.Commit expectation
|
||||
// returned by *Sqlmock.ExpectCommit.
|
||||
type ExpectedCommit struct {
|
||||
commonExpectation
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for *sql.Tx.Close action
|
||||
func (e *ExpectedCommit) WillReturnError(err error) *ExpectedCommit {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedCommit) String() string {
|
||||
msg := "ExpectedCommit => expecting transaction Commit"
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf(", which should return error: %s", e.err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// ExpectedRollback is used to manage *sql.Tx.Rollback expectation
|
||||
// returned by *Sqlmock.ExpectRollback.
|
||||
type ExpectedRollback struct {
|
||||
commonExpectation
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for *sql.Tx.Rollback action
|
||||
func (e *ExpectedRollback) WillReturnError(err error) *ExpectedRollback {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedRollback) String() string {
|
||||
msg := "ExpectedRollback => expecting transaction Rollback"
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf(", which should return error: %s", e.err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// ExpectedQuery is used to manage *sql.DB.Query, *dql.DB.QueryRow, *sql.Tx.Query,
|
||||
// *sql.Tx.QueryRow, *sql.Stmt.Query or *sql.Stmt.QueryRow expectations.
|
||||
// Returned by *Sqlmock.ExpectQuery.
|
||||
type ExpectedQuery struct {
|
||||
queryBasedExpectation
|
||||
rows driver.Rows
|
||||
delay time.Duration
|
||||
rowsMustBeClosed bool
|
||||
rowsWereClosed bool
|
||||
}
|
||||
|
||||
// WithArgs will match given expected args to actual database query arguments.
|
||||
// if at least one argument does not match, it will return an error. For specific
|
||||
// arguments an sqlmock.Argument interface can be used to match an argument.
|
||||
func (e *ExpectedQuery) WithArgs(args ...driver.Value) *ExpectedQuery {
|
||||
e.args = args
|
||||
return e
|
||||
}
|
||||
|
||||
// RowsWillBeClosed expects this query rows to be closed.
|
||||
func (e *ExpectedQuery) RowsWillBeClosed() *ExpectedQuery {
|
||||
e.rowsMustBeClosed = true
|
||||
return e
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for expected database query
|
||||
func (e *ExpectedQuery) WillReturnError(err error) *ExpectedQuery {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// WillDelayFor allows to specify duration for which it will delay
|
||||
// result. May be used together with Context
|
||||
func (e *ExpectedQuery) WillDelayFor(duration time.Duration) *ExpectedQuery {
|
||||
e.delay = duration
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedQuery) String() string {
|
||||
msg := "ExpectedQuery => expecting Query, QueryContext or QueryRow which:"
|
||||
msg += "\n - matches sql: '" + e.expectSQL + "'"
|
||||
|
||||
if len(e.args) == 0 {
|
||||
msg += "\n - is without arguments"
|
||||
} else {
|
||||
msg += "\n - is with arguments:\n"
|
||||
for i, arg := range e.args {
|
||||
msg += fmt.Sprintf(" %d - %+v\n", i, arg)
|
||||
}
|
||||
msg = strings.TrimSpace(msg)
|
||||
}
|
||||
|
||||
if e.rows != nil {
|
||||
msg += fmt.Sprintf("\n - %s", e.rows)
|
||||
}
|
||||
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf("\n - should return error: %s", e.err)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// ExpectedExec is used to manage *sql.DB.Exec, *sql.Tx.Exec or *sql.Stmt.Exec expectations.
|
||||
// Returned by *Sqlmock.ExpectExec.
|
||||
type ExpectedExec struct {
|
||||
queryBasedExpectation
|
||||
result driver.Result
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// WithArgs will match given expected args to actual database exec operation arguments.
|
||||
// if at least one argument does not match, it will return an error. For specific
|
||||
// arguments an sqlmock.Argument interface can be used to match an argument.
|
||||
func (e *ExpectedExec) WithArgs(args ...driver.Value) *ExpectedExec {
|
||||
e.args = args
|
||||
return e
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for expected database exec action
|
||||
func (e *ExpectedExec) WillReturnError(err error) *ExpectedExec {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// WillDelayFor allows to specify duration for which it will delay
|
||||
// result. May be used together with Context
|
||||
func (e *ExpectedExec) WillDelayFor(duration time.Duration) *ExpectedExec {
|
||||
e.delay = duration
|
||||
return e
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedExec) String() string {
|
||||
msg := "ExpectedExec => expecting Exec or ExecContext which:"
|
||||
msg += "\n - matches sql: '" + e.expectSQL + "'"
|
||||
|
||||
if len(e.args) == 0 {
|
||||
msg += "\n - is without arguments"
|
||||
} else {
|
||||
msg += "\n - is with arguments:\n"
|
||||
var margs []string
|
||||
for i, arg := range e.args {
|
||||
margs = append(margs, fmt.Sprintf(" %d - %+v", i, arg))
|
||||
}
|
||||
msg += strings.Join(margs, "\n")
|
||||
}
|
||||
|
||||
if e.result != nil {
|
||||
res, _ := e.result.(*result)
|
||||
msg += "\n - should return Result having:"
|
||||
msg += fmt.Sprintf("\n LastInsertId: %d", res.insertID)
|
||||
msg += fmt.Sprintf("\n RowsAffected: %d", res.rowsAffected)
|
||||
if res.err != nil {
|
||||
msg += fmt.Sprintf("\n Error: %s", res.err)
|
||||
}
|
||||
}
|
||||
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf("\n - should return error: %s", e.err)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// WillReturnResult arranges for an expected Exec() to return a particular
|
||||
// result, there is sqlmock.NewResult(lastInsertID int64, affectedRows int64) method
|
||||
// to build a corresponding result. Or if actions needs to be tested against errors
|
||||
// sqlmock.NewErrorResult(err error) to return a given error.
|
||||
func (e *ExpectedExec) WillReturnResult(result driver.Result) *ExpectedExec {
|
||||
e.result = result
|
||||
return e
|
||||
}
|
||||
|
||||
// ExpectedPrepare is used to manage *sql.DB.Prepare or *sql.Tx.Prepare expectations.
|
||||
// Returned by *Sqlmock.ExpectPrepare.
|
||||
type ExpectedPrepare struct {
|
||||
commonExpectation
|
||||
mock *sqlmock
|
||||
expectSQL string
|
||||
statement driver.Stmt
|
||||
closeErr error
|
||||
mustBeClosed bool
|
||||
wasClosed bool
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// WillReturnError allows to set an error for the expected *sql.DB.Prepare or *sql.Tx.Prepare action.
|
||||
func (e *ExpectedPrepare) WillReturnError(err error) *ExpectedPrepare {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
// WillReturnCloseError allows to set an error for this prepared statement Close action
|
||||
func (e *ExpectedPrepare) WillReturnCloseError(err error) *ExpectedPrepare {
|
||||
e.closeErr = err
|
||||
return e
|
||||
}
|
||||
|
||||
// WillDelayFor allows to specify duration for which it will delay
|
||||
// result. May be used together with Context
|
||||
func (e *ExpectedPrepare) WillDelayFor(duration time.Duration) *ExpectedPrepare {
|
||||
e.delay = duration
|
||||
return e
|
||||
}
|
||||
|
||||
// WillBeClosed expects this prepared statement to
|
||||
// be closed.
|
||||
func (e *ExpectedPrepare) WillBeClosed() *ExpectedPrepare {
|
||||
e.mustBeClosed = true
|
||||
return e
|
||||
}
|
||||
|
||||
// ExpectQuery allows to expect Query() or QueryRow() on this prepared statement.
|
||||
// This method is convenient in order to prevent duplicating sql query string matching.
|
||||
func (e *ExpectedPrepare) ExpectQuery() *ExpectedQuery {
|
||||
eq := &ExpectedQuery{}
|
||||
eq.expectSQL = e.expectSQL
|
||||
eq.converter = e.mock.converter
|
||||
e.mock.expected = append(e.mock.expected, eq)
|
||||
return eq
|
||||
}
|
||||
|
||||
// ExpectExec allows to expect Exec() on this prepared statement.
|
||||
// This method is convenient in order to prevent duplicating sql query string matching.
|
||||
func (e *ExpectedPrepare) ExpectExec() *ExpectedExec {
|
||||
eq := &ExpectedExec{}
|
||||
eq.expectSQL = e.expectSQL
|
||||
eq.converter = e.mock.converter
|
||||
e.mock.expected = append(e.mock.expected, eq)
|
||||
return eq
|
||||
}
|
||||
|
||||
// String returns string representation
|
||||
func (e *ExpectedPrepare) String() string {
|
||||
msg := "ExpectedPrepare => expecting Prepare statement which:"
|
||||
msg += "\n - matches sql: '" + e.expectSQL + "'"
|
||||
|
||||
if e.err != nil {
|
||||
msg += fmt.Sprintf("\n - should return error: %s", e.err)
|
||||
}
|
||||
|
||||
if e.closeErr != nil {
|
||||
msg += fmt.Sprintf("\n - should return error on Close: %s", e.closeErr)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// query based expectation
|
||||
// adds a query matching logic
|
||||
type queryBasedExpectation struct {
|
||||
commonExpectation
|
||||
expectSQL string
|
||||
converter driver.ValueConverter
|
||||
args []driver.Value
|
||||
}
|
||||
|
||||
func (e *queryBasedExpectation) attemptArgMatch(args []namedValue) (err error) {
|
||||
// catch panic
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
_, ok := e.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf(e.(string))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = e.argsMatches(args)
|
||||
return
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// +build !go1.8
|
||||
|
||||
package sqlmock
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// WillReturnRows specifies the set of resulting rows that will be returned
|
||||
// by the triggered query
|
||||
func (e *ExpectedQuery) WillReturnRows(rows *Rows) *ExpectedQuery {
|
||||
e.rows = &rowSets{sets: []*Rows{rows}, ex: e}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
|
||||
if nil == e.args {
|
||||
return nil
|
||||
}
|
||||
if len(args) != len(e.args) {
|
||||
return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args))
|
||||
}
|
||||
for k, v := range args {
|
||||
// custom argument matcher
|
||||
matcher, ok := e.args[k].(Argument)
|
||||
if ok {
|
||||
// @TODO: does it make sense to pass value instead of named value?
|
||||
if !matcher.Match(v.Value) {
|
||||
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dval := e.args[k]
|
||||
// convert to driver converter
|
||||
darg, err := e.converter.ConvertValue(dval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err)
|
||||
}
|
||||
|
||||
if !driver.IsValue(darg) {
|
||||
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(darg, v.Value) {
|
||||
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// +build go1.8
|
||||
|
||||
package sqlmock
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// WillReturnRows specifies the set of resulting rows that will be returned
|
||||
// by the triggered query
|
||||
func (e *ExpectedQuery) WillReturnRows(rows ...*Rows) *ExpectedQuery {
|
||||
sets := make([]*Rows, len(rows))
|
||||
for i, r := range rows {
|
||||
sets[i] = r
|
||||
}
|
||||
e.rows = &rowSets{sets: sets, ex: e}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *queryBasedExpectation) argsMatches(args []namedValue) error {
|
||||
if nil == e.args {
|
||||
return nil
|
||||
}
|
||||
if len(args) != len(e.args) {
|
||||
return fmt.Errorf("expected %d, but got %d arguments", len(e.args), len(args))
|
||||
}
|
||||
// @TODO should we assert either all args are named or ordinal?
|
||||
for k, v := range args {
|
||||
// custom argument matcher
|
||||
matcher, ok := e.args[k].(Argument)
|
||||
if ok {
|
||||
if !matcher.Match(v.Value) {
|
||||
return fmt.Errorf("matcher %T could not match %d argument %T - %+v", matcher, k, args[k], args[k])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dval := e.args[k]
|
||||
if named, isNamed := dval.(sql.NamedArg); isNamed {
|
||||
dval = named.Value
|
||||
if v.Name != named.Name {
|
||||
return fmt.Errorf("named argument %d: name: \"%s\" does not match expected: \"%s\"", k, v.Name, named.Name)
|
||||
}
|
||||
} else if k+1 != v.Ordinal {
|
||||
return fmt.Errorf("argument %d: ordinal position: %d does not match expected: %d", k, k+1, v.Ordinal)
|
||||
}
|
||||
|
||||
// convert to driver converter
|
||||
darg, err := e.converter.ConvertValue(dval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not convert %d argument %T - %+v to driver value: %s", k, e.args[k], e.args[k], err)
|
||||
}
|
||||
|
||||
if !driver.IsValue(darg) {
|
||||
return fmt.Errorf("argument %d: non-subset type %T returned from Value", k, darg)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(darg, v.Value) {
|
||||
return fmt.Errorf("argument %d expected [%T - %+v] does not match actual [%T - %+v]", k, darg, darg, v.Value, v.Value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user