mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bff1ef9df | |||
| 25cfffd0d1 | |||
| 4662e40068 | |||
| 38d78f9844 | |||
| 6f8708d33c | |||
| fa17b0200f | |||
| 14f50d0922 | |||
| 945320880a | |||
| 809d2f3f28 | |||
| 137928ecaf | |||
| 8183af31e2 | |||
| 28f890a701 | |||
| bab7583a97 | |||
| 2e2fa29637 | |||
| 2bef5dbe72 | |||
| 13d25a52a9 | |||
| 9a43a92b1c | |||
| 102b364cc9 | |||
| c18702f297 | |||
| 980bee22a5 | |||
| 80d3d989eb | |||
| 6804a5ff9d | |||
| b5dab1f5da | |||
| 8c6cfa34aa | |||
| 8560436487 | |||
| 619bc95501 | |||
| 781e1fef7b | |||
| 541cf68608 | |||
| eafc89bf73 | |||
| fd451ca158 | |||
| d22e214000 | |||
| 92d6d73f9c | |||
| b02718f86b | |||
| 5a0b7ecb62 | |||
| 07a409ffef | |||
| 58e5e9c9a4 | |||
| 6ca642e572 | |||
| 073c5bfdaa | |||
| fea3569956 | |||
| abdbc76a46 | |||
| f22202b31b | |||
| 4586ed3e51 | |||
| 7475e3e487 | |||
| e025a4cd7b | |||
| 850f804c47 | |||
| 1558280185 | |||
| 15d68a0eed | |||
| e3aed15c08 | |||
| 99713e3439 | |||
| 27c6977746 | |||
| 200f9a3786 | |||
| 92defa26d4 | |||
| c9f1c1ec70 | |||
| 10e4a2940c | |||
| 77f12c7c34 | |||
| ca33ed9f6a | |||
| 6a331b13ba | |||
| 47c878b9c4 | |||
| 61cd4a918d | |||
| 9db4b7fdfb | |||
| 2f59b8ac22 | |||
| f94699e07b | |||
| f7cf597f54 | |||
| aeb8dd7e51 | |||
| 5bf6dd8f85 | |||
| 62b1ab8c98 | |||
| 8de19dc647 | |||
| 8a099d8f6e | |||
| ef400afe00 | |||
| bf596c035e | |||
| 36276e957a | |||
| c69437ba83 | |||
| b2d0c612a5 | |||
| 8e2908f889 | |||
| 3e8d886c25 | |||
| 446c5cf60c | |||
| 13f88b3739 | |||
| 69ee6c1d88 | |||
| 192ae35728 | |||
| 10d547f528 | |||
| b59fd4b7d8 | |||
| c85c8526e8 | |||
| f49d9dcb67 | |||
| c2ac282aca | |||
| 58daf6bfed | |||
| 611b284e20 | |||
| 236a0a164d | |||
| 148eea5899 | |||
| 83c6c8713b | |||
| 2b820d790c | |||
| 36286301f7 | |||
| 9a48fe959d | |||
| f6014cb2b4 | |||
| 9fe21fa906 | |||
| 80a75e91d2 | |||
| 6acc95f756 | |||
| 72412db4c2 | |||
| fa92441415 |
@@ -7,3 +7,6 @@ guide/public
|
||||
.idea
|
||||
.vscode
|
||||
\#*\#
|
||||
cscope.*
|
||||
cloudflared
|
||||
!cmd/cloudflared/
|
||||
Generated
+234
-83
@@ -2,31 +2,45 @@
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9f3b30d9f8e0d7040f729b82dcbc8f0dead820a133b3147ce355fc451f32d761"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
pruneopts = "UT"
|
||||
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
pruneopts = "UT"
|
||||
revision = "4b2b341e8d7715fae06375aa633dbb6e91b3fb46"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fed1f537c2f1269fe475a8556c393fe466641682d73ef8fd0491cd3aa1e47bad"
|
||||
name = "github.com/certifi/gocertifi"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "deb3ae2ef2610fde3330947281941c562861188b"
|
||||
version = "2018.01.18"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e5003c19d396d8b3cf1324ea0bf49b00f13e9466d0297d1268b641f1c617c3a2"
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
packages = ["."]
|
||||
pruneopts = "T"
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6dbb2bbc7e6333e691c4d82fd86485f0695a35902fbb9b2df5f72e22ab0040f3"
|
||||
name = "github.com/cloudflare/golibs"
|
||||
packages = ["lrucache"]
|
||||
pruneopts = "UT"
|
||||
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3f9506ee991cdee1f05bf0cd3e34b5cd922dc00d6a950fb4beb4e07ab1c4d3d1"
|
||||
name = "github.com/coredns/coredns"
|
||||
packages = [
|
||||
"core/dnsserver",
|
||||
@@ -52,52 +66,63 @@
|
||||
"plugin/pkg/uniq",
|
||||
"plugin/pkg/watch",
|
||||
"plugin/test",
|
||||
"request"
|
||||
"request",
|
||||
]
|
||||
revision = "992e7928c7c258628d2b13b769acc86781b9faea"
|
||||
pruneopts = "UT"
|
||||
revision = "2e322f6e8a54f18c6aef9c25a7c432c291a3d9f7"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6a503e232df389d94ebb97dfb22d4ae463b6e2f351660613e11d9e42f57ab6df"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
packages = [
|
||||
"http",
|
||||
"jose",
|
||||
"key",
|
||||
"oauth2",
|
||||
"oidc"
|
||||
"oidc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1da3a221f0bc090792d3a2a080ff09008427c0e0f0533a4ed6abd8994421da73"
|
||||
name = "github.com/coreos/go-systemd"
|
||||
packages = ["daemon"]
|
||||
revision = "39ca1b05acc7ad1220e09f133283b8859a8b71ab"
|
||||
version = "v17"
|
||||
pruneopts = "UT"
|
||||
revision = "95778dfbb74eb7e4dbaf43bf7d71809650ef8076"
|
||||
version = "v19"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6fda0d7f5e52b081e075775b1ecebf1ea0c923e7be33604ed0225ae078e701b5"
|
||||
name = "github.com/coreos/pkg"
|
||||
packages = [
|
||||
"health",
|
||||
"httputil",
|
||||
"timeutil"
|
||||
"timeutil",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "97fdf19511ea361ae1c100dd393cc47f8dcfa1e1"
|
||||
version = "v4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
pruneopts = "UT"
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c013ffc6e15f9f898078f9d38441c68b228aa7b899659452170250ccb27f5f1e"
|
||||
name = "github.com/elgs/gosqljson"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "027aa4915315a0b2825c0f025cea347829b974fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d4268b2a09b1f736633577c4ac93f2a5356c73742fff5344e2451aeec60a7ad0"
|
||||
name = "github.com/equinox-io/equinox"
|
||||
packages = [
|
||||
".",
|
||||
@@ -105,225 +130,273 @@
|
||||
"internal/go-update/internal/binarydist",
|
||||
"internal/go-update/internal/osext",
|
||||
"internal/osext",
|
||||
"proto"
|
||||
"proto",
|
||||
]
|
||||
revision = "f24972fa72facf59d05c91c848b65eac38815915"
|
||||
pruneopts = "UT"
|
||||
revision = "5205c98a6c11dc72747ce12fff6cd620a99fde05"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:433763f10d88dba9b533a7ea2fe9f5ee11e57e00306eb97a1f6090fd978e8fa1"
|
||||
name = "github.com/facebookgo/grace"
|
||||
packages = ["gracenet"]
|
||||
pruneopts = "UT"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:50a46ab1d5edbbdd55125b4d37f1bf503d0807c26461f9ad7b358d6006641d09"
|
||||
name = "github.com/flynn/go-shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "3f9db97f856818214da2e1057f8ad84803971cff"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d4623fc7bf7e281d9107367cc4a9e76ed3e86b1eec1a4e30630c870bef1fedd0"
|
||||
name = "github.com/getsentry/raven-go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3e6afc3ed8a72949aa735c00fddc23427dc9384ccfd51cf0d91a412e668da632"
|
||||
name = "github.com/golang-collections/collections"
|
||||
packages = ["queue"]
|
||||
pruneopts = "UT"
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
pruneopts = "UT"
|
||||
revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b"
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
|
||||
version = "0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
|
||||
pruneopts = "UT"
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d5f97fc268267ec1b61c3453058c738246fc3e746f14b1ae25161513b7367b0c"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
|
||||
version = "v1.6.2"
|
||||
pruneopts = "UT"
|
||||
revision = "c5c6c98bc25355028a63748a498942a6398ccd22"
|
||||
version = "v1.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1a1206efd03a54d336dce7bb8719e74f2f8932f661cb9f57d5813a1d99c083d8"
|
||||
name = "github.com/grpc-ecosystem/grpc-opentracing"
|
||||
packages = ["go/otgrpc"]
|
||||
pruneopts = "UT"
|
||||
revision = "8e809c8a86450a29b90dcc9efbf062d0fe6d9746"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:75ab90ae3f5d876167e60f493beadfe66f0ed861a710f283fb06c86437a09538"
|
||||
name = "github.com/jonboulle/clockwork"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bc1c0be40c67b6b4aee09d7508d5a2a52c1c116b1fa43806dad2b0d6b4d4003b"
|
||||
name = "github.com/lib/pq"
|
||||
packages = [
|
||||
".",
|
||||
"oid"
|
||||
"oid",
|
||||
"scram",
|
||||
]
|
||||
revision = "90697d60dd844d5ef6ff15135d0203f65d2f53b8"
|
||||
pruneopts = "UT"
|
||||
revision = "51e2106eed1cea199c802d2a49e91e2491b02056"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2fa7b0155cd54479a755c629de26f888a918e13f8857a2c442205d825368e084"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
pruneopts = "UT"
|
||||
revision = "3a70a971f94a22f2fa562ffcc7a0eb45f5daf045"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e150b5fafbd7607e2d638e4e5cf43aa4100124e5593385147b0a74e2733d8b0d"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
pruneopts = "UT"
|
||||
revision = "c2a7a6ca930a4cd0bc33a3f298eb71960732a3a7"
|
||||
version = "v0.0.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
pruneopts = "UT"
|
||||
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:75fa16a231ef40da3e462d651c20b9df20bde0777bdc1ac0982242c79057ee71"
|
||||
name = "github.com/mholt/caddy"
|
||||
packages = [
|
||||
".",
|
||||
"caddyfile",
|
||||
"telemetry"
|
||||
"telemetry",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2b4b4b2e5544c2a11a486c1b631357aa2ddf766e50c1b2483cf809da2c511234"
|
||||
name = "github.com/miekg/dns"
|
||||
packages = ["."]
|
||||
revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1"
|
||||
version = "v1.0.8"
|
||||
pruneopts = "UT"
|
||||
revision = "73601d4aed9d844322611759d7f3619110b7c88e"
|
||||
version = "v1.1.8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5d231480e1c64a726869bc4142d270184c419749d34f167646baa21008eb0a79"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
|
||||
pruneopts = "UT"
|
||||
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:11e62d6050198055e6cd87ed57e5d8c669e84f839c16e16f192374d913d1a70d"
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
".",
|
||||
"ext",
|
||||
"log"
|
||||
"log",
|
||||
]
|
||||
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
|
||||
version = "v1.0.2"
|
||||
pruneopts = "UT"
|
||||
revision = "659c90643e714681897ec2521c60567dd21da733"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "UT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c968b29db5d68ec97de404b6d058d5937fa015a141b3b4f7a0d87d5f8226f04c"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = [
|
||||
"prometheus",
|
||||
"prometheus/promhttp"
|
||||
"prometheus/promhttp",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||
version = "v0.9.0-pre1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
pruneopts = "UT"
|
||||
revision = "fd36f4220a901265f90734c3183c5f0c91daa0b8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:35cf6bdf68db765988baa9c4f10cc5d7dda1126a54bd62e252dbcd0b1fc8da90"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
"model",
|
||||
]
|
||||
revision = "7600349dcfe1abd18d72d3a1770870d9800a7801"
|
||||
pruneopts = "UT"
|
||||
revision = "a82f4c12f983cc2649298185f296632953e50d3e"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:49b09905e781d7775c086604cc00083e1832d0783f1f421b79f42657c457d029"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "ae68e2d4c00fed4943b5f6698d504a5fe083da8a"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "8368d24ba045f26503eb745b624d930cbe214c79"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1a23fdd843129ef761ffe7651bc5fe7c5b09fbe933e92783ab06cc11c37b7b37"
|
||||
name = "github.com/rifflock/lfshook"
|
||||
packages = ["."]
|
||||
revision = "bf539943797a1f34c1f502d07de419b5238ae6c6"
|
||||
version = "v2.3"
|
||||
pruneopts = "UT"
|
||||
revision = "b9218ef580f59a2e72dad1aa33d660150445d05a"
|
||||
version = "v2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5f2aaa360f48d1711795bd88c7e45a38f86cf81e4bc01453d20983baa67e2d51"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
pruneopts = "UT"
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
pruneopts = "UT"
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a84d5ec8b40a827962ea250f2cf03434138ccae9d83fcac12fb49b70c70b80cc"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"nacl/box",
|
||||
"nacl/secretbox",
|
||||
"poly1305",
|
||||
"salsa20/salsa",
|
||||
"ssh/terminal"
|
||||
"ssh",
|
||||
"ssh/terminal",
|
||||
]
|
||||
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
|
||||
pruneopts = "UT"
|
||||
revision = "f416ebab96af27ca70b6e5c23d6a0747530da626"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:52d140f7ab52e491cc1cbc93e6637aa5e9a7f3beae7545d675b02e52ca9d7290"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
@@ -338,30 +411,36 @@
|
||||
"ipv4",
|
||||
"ipv6",
|
||||
"trace",
|
||||
"websocket"
|
||||
"websocket",
|
||||
]
|
||||
revision = "32a936f46389aa10549d60bd7833e54b01685d09"
|
||||
pruneopts = "UT"
|
||||
revision = "1da14a5a36f220ea3f03470682b737b1dfd5de22"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = "UT"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:77751d02e939d7078faedaeec10c09af575a09c528d84d18f2cb45a84bd1889a"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"cpu",
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
"windows/svc",
|
||||
"windows/svc/eventlog",
|
||||
"windows/svc/mgr"
|
||||
"windows/svc/mgr",
|
||||
]
|
||||
revision = "ce36f3865eeb42541ce3f87f32f8462c5687befa"
|
||||
pruneopts = "UT"
|
||||
revision = "12500544f89f9420afe9529ba8940bf72d294972"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
@@ -377,34 +456,46 @@
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable"
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c3076e7defee87de1236f1814beb588f40a75544c60121e6eb38b3b3721783e2"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "ff3583edef7de132f219f0efc00e097cabcc0ec0"
|
||||
pruneopts = "UT"
|
||||
revision = "d1146b9035b912113a38af3b138eb2af567b2c67"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31d87f39886fb38a2b6c097ff3b9f985d6960772170d64a68246f7790e955746"
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"binarylog/grpc_binarylog_v1",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"credentials/internal",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/balancerload",
|
||||
"internal/binarylog",
|
||||
"internal/channelz",
|
||||
"internal/envconfig",
|
||||
"internal/grpcrand",
|
||||
"internal/grpcsync",
|
||||
"internal/syscall",
|
||||
"internal/transport",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
@@ -415,28 +506,33 @@
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8"
|
||||
version = "v1.13.0"
|
||||
pruneopts = "UT"
|
||||
revision = "236199dd5f8031d698fb64091194aecd1c3895b2"
|
||||
version = "v1.20.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "altsrc-parse-durations"
|
||||
digest = "1:0370b1bceda03dbfade3abbde639a43f1113bab711ec760452e5c0dcc0c14787"
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
packages = [
|
||||
".",
|
||||
"altsrc"
|
||||
"altsrc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "d604b6ffeee878fbf084fd2761466b6649989cee"
|
||||
source = "https://github.com/cbranch/cli"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
pruneopts = "UT"
|
||||
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
|
||||
version = "v2.2.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8ffc3ddc31414c0a71220957bb723b16510d7fcb5b3880dc0da4cf6d39c31642"
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
packages = [
|
||||
".",
|
||||
@@ -452,8 +548,9 @@
|
||||
"rpc/internal/refcount",
|
||||
"schemas",
|
||||
"server",
|
||||
"std/capnp/rpc"
|
||||
"std/capnp/rpc",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "7cfd211c19c7f5783c695f3654efa46f0df259c3"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "v2.17.1"
|
||||
@@ -461,6 +558,60 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "ee681bef3527e49801c841e313f98b40116eafe8b60be21273956eeb96487486"
|
||||
input-imports = [
|
||||
"github.com/cloudflare/brotli-go",
|
||||
"github.com/cloudflare/golibs/lrucache",
|
||||
"github.com/coredns/coredns/core/dnsserver",
|
||||
"github.com/coredns/coredns/plugin",
|
||||
"github.com/coredns/coredns/plugin/cache",
|
||||
"github.com/coredns/coredns/plugin/metrics/vars",
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest",
|
||||
"github.com/coredns/coredns/plugin/pkg/rcode",
|
||||
"github.com/coredns/coredns/request",
|
||||
"github.com/coreos/go-oidc/jose",
|
||||
"github.com/coreos/go-oidc/oidc",
|
||||
"github.com/coreos/go-systemd/daemon",
|
||||
"github.com/elgs/gosqljson",
|
||||
"github.com/equinox-io/equinox",
|
||||
"github.com/facebookgo/grace/gracenet",
|
||||
"github.com/getsentry/raven-go",
|
||||
"github.com/golang-collections/collections/queue",
|
||||
"github.com/google/uuid",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/lib/pq",
|
||||
"github.com/mattn/go-colorable",
|
||||
"github.com/miekg/dns",
|
||||
"github.com/mitchellh/go-homedir",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/rifflock/lfshook",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"golang.org/x/crypto/nacl/box",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"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
|
||||
|
||||
+27
-15
@@ -10,35 +10,35 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/facebookgo/grace"
|
||||
branch = "master"
|
||||
revision = "75cf19382434e82df4dd84953f566b8ad23d6e9e"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/getsentry/raven-go"
|
||||
branch = "master"
|
||||
revision = "ed7bcb39ff10f39ab08e317ce16df282845852fa"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
version = "=0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
version = "0.9.0-pre1"
|
||||
version = "=0.9.0-pre1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.3"
|
||||
version = "=1.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
version = "=1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
branch = "master"
|
||||
branch = "master" # master required by github.com/miekg/dns
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/sync"
|
||||
branch = "master"
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/urfave/cli.v2"
|
||||
@@ -48,28 +48,40 @@
|
||||
[[constraint]]
|
||||
name = "zombiezen.com/go/capnproto2"
|
||||
source = "https://github.com/zombiezen/go-capnproto2"
|
||||
version = "2.17.1"
|
||||
version = "=2.17.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coredns/coredns"
|
||||
branch = "master"
|
||||
version = "=1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/dns"
|
||||
version = "=1.1.8"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/brotli-go"
|
||||
branch = "master"
|
||||
revision = "18c9f6c67e3dfc12e0ddaca748d2887f97a7ac28"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/mholt/caddy"
|
||||
branch = "master"
|
||||
revision = "d3b731e9255b72d4571a5aac125634cf1b6031dc"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/coreos/go-oidc"
|
||||
revision = "a93f71fdfe73d2c0f5413c0565eea0af6523a6df"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master" # master required by github.com/miekg/dns
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cloudflare/golibs"
|
||||
revision = "333127dbecfcc23a8db7d9a4f52785d23aff44a1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "=1.1.1"
|
||||
|
||||
@@ -16,15 +16,23 @@ ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
endif
|
||||
|
||||
ifeq ($(GOARCH),)
|
||||
GOARCH := amd64
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared:
|
||||
cloudflared: tunnel-deps
|
||||
go build -v $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
test: vet
|
||||
go test -v -race $(VERSION_FLAGS) ./...
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
@@ -32,7 +40,7 @@ cloudflared-deb: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared
|
||||
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
@@ -43,8 +51,8 @@ cloudflared-darwin-amd64.tgz: cloudflared
|
||||
homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
|
||||
|
||||
.PHONY: homebrew-release
|
||||
|
||||
.PHONY: homebrew-release
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
|
||||
@@ -57,5 +65,15 @@ bin/equinox:
|
||||
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps:
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
capnp compile -ogo -I ./tunnelrpc tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype
|
||||
go-sumtype $$(go list ./...)
|
||||
|
||||
+105
@@ -1,3 +1,108 @@
|
||||
2019.5.0
|
||||
- 2019-04-25 TUN-1781: ServeStream should return early on error
|
||||
- 2019-04-30 TUN-1786: Remove low-level Windows service logging
|
||||
- 2019-05-03 TUN-1807: Send cloudflared version in Connect RPC
|
||||
- 2019-01-23 AUTH-1557: Short Lived Certs
|
||||
- 2019-05-13 TUN-1847: Log a distinct message when OpenStream fails while waiting for response headers
|
||||
- 2019-05-13 AUTH-1706: fixes and testing
|
||||
- 2019-05-22 TUN-1880: Save debug and warn level log to logfile
|
||||
- 2019-05-22 AUTH-1781: fixed race condition for short lived certs, doc required config
|
||||
|
||||
2019.4.1
|
||||
- 2019-03-18 TUN-1626: Create new supervisor to establish connection with origintunneld
|
||||
- 2019-04-04 TUN-1619: Add flag to test declarative tunnels.
|
||||
- 2019-04-05 TUN-1577: decompose carrier.StartServer to make TestStartServer less flappy
|
||||
- 2019-03-29 TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server
|
||||
- 2019-04-02 TUN-1682: Add context to OpenStream to prevent it from blocking indefinitely.
|
||||
- 2019-04-16 TUN-1732: cloudflared metrics should track userHostnames
|
||||
- 2019-04-17 TUN-1734: Pin packages at exact versions
|
||||
- 2019-04-18 TUN-1669: Update license message in help text. Also fix test
|
||||
|
||||
2019.4.0
|
||||
- 2019-03-28 TUN-1648: ConnectionID is now a UUID
|
||||
- 2019-04-01 TUN-1673: Conflate Hello and Connect RPCs
|
||||
|
||||
2019.3.2
|
||||
- 2019-03-22 TUN-1637: Free tunnels shouldn't require cert.pem
|
||||
- 2019-03-18 TUN-1604: Define Connect RPC call
|
||||
|
||||
2019.3.1
|
||||
- 2019-03-09 Add rdp as a supported protocol in URL validation (#76)
|
||||
- 2019-03-15 TUN-1613: improved cloudflared RegisterTunnel fail metrics
|
||||
- 2019-03-17 TUN-1615: revert miekg/dns to last known working revision
|
||||
|
||||
2019.3.0
|
||||
- 2018-12-28 make http transport aware of proxy from envvar
|
||||
- 2019-02-28 TUN-1559: fix nil dereference in TunnelConfig.CloseConnOnce
|
||||
- 2019-03-04 TUN-1451: Make non-interactive, non-service execution possible on Windows
|
||||
- 2019-03-04 TUN-1562: Refactor connectedSignal to be safe to close multiple times
|
||||
- 2019-02-27 TUN-1550: Add validation timeout for non-responsive origins
|
||||
- 2019-03-06 AUTH-1531: Named flags for ssh service tokens
|
||||
- 2019-02-14 Support unix sockets.
|
||||
- 2019-03-08 TUN-1389: Non-scalar flags in a cloudflared config.yml don't get logged
|
||||
- 2019-03-07 TUN-1522: If we can't get SRV from default resolver, get them from 1.1.1.1 DoT
|
||||
|
||||
2019.2.1
|
||||
- 2019-02-14 TUN-1381: should tell you if you're on the latest version rather than just exiting silently
|
||||
- 2019-02-15 TUN-1467: build with Go 1.11
|
||||
- 2019-02-15 AUTH-1519: Added logging
|
||||
- 2019-02-19 TUN-1525: cloudflared metrics for registration success/fail
|
||||
- 2019-02-19 TUN-1510: Wrap the close() in sync.Once.Do
|
||||
|
||||
2019.2.0
|
||||
- 2019-01-24 AUTH-1462: better curl arg parsing
|
||||
- 2019-02-01 TUN-1456: Only make one UUID
|
||||
- 2019-01-30 cloudflared/linux_service: Add missing /etc/init.d shebang
|
||||
- 2019-02-07 AUTH-1511: Add custom headers for ssh command
|
||||
- 2019-02-01 AUTH-1503: Added RDP support
|
||||
- 2019-02-01 AUTH-1403: Print the paths in the ssh-config instructions
|
||||
|
||||
2019.1.0
|
||||
- 2018-12-10 TUN-1231: Horizontal overflow wrapping on the Hello page
|
||||
- 2018-12-17 TUN-1140: Show usage if invoked with no args or config
|
||||
- 2018-11-06 TUN-632 Filter out common network exceptions from going to Sentry on StartServer
|
||||
- 2019-01-07 TUN-1138: Install cloudflared service directory with 755 permissions
|
||||
- 2019-01-07 TUN-1265: Silent exit when failing to parse config
|
||||
- 2019-01-10 TUN-1350: Enhance error messages with cloudflarestatus.com link, if relevant
|
||||
- 2019-01-16 TUN-1358: Close readyList after Muxer.Serve() has stopped running
|
||||
- 2019-01-24 AUTH-1423: move from stdout to stderr
|
||||
- 2019-01-24 AUTH-1404: reauth if the token is about to expire within 15 minutes
|
||||
- 2019-01-24 AUTH-1459: improved ssh streaming error message
|
||||
- 2019-01-24 AUTH-1211: print all the versions
|
||||
- 2019-01-24 AUTH-1337: fix url path
|
||||
- 2019-01-28 TUN-1418: Rename ProtocolLogger to TransportLogger, and use TransportLogger to log RPC events.
|
||||
- 2019-01-28 TUN-1419: Identify request/response headers/content length with ray ID
|
||||
|
||||
2018.12.1
|
||||
- 2018-12-11 TUN-1270: cloudflared panic (HA metrics missing label)
|
||||
|
||||
2018.12.0
|
||||
- 2018-11-15 TUN-1196: Allow TLS config client CA and root CA to be constructed from multiple certificates
|
||||
- 2018-11-20 TUN-1209: TLS Config Certificates and GetCertificate can both be set
|
||||
- 2018-11-26 TUN-1212: Expose tunnel_id in metrics
|
||||
- 2018-11-30 TUN-1204: remove 'cloudflared hello' command
|
||||
- 2018-12-04 Fix license URL typo
|
||||
- 2018-12-07 TUN-1250: ValidateHTTPService shouldn't follow 302s
|
||||
|
||||
2018.11.0
|
||||
- 2018-10-31 AUTH-1282: Fixed an issue where we were receiving as opposed sending on the channel.
|
||||
- 2018-11-06 TUN-1179: Fix log message in cmd/cloudflared/transfer.Run
|
||||
- 2018-11-13 AUTH-1308: get jwt even when you are already logged in
|
||||
- 2018-11-12 TUN-1190: check URL parse error when starting SSH proxy server
|
||||
- 2018-11-15 AUTH-1320: Fixed request issue and unhide the ssh command
|
||||
|
||||
2018.10.5
|
||||
- 2018-10-18 TUN-968: Flow control for large requests/responses
|
||||
- 2018-10-26 TUN-1158: Windows: use process arguments rather than trivial service arguments
|
||||
- 2018-10-20 #30: Fix the Content-Length header for HTTP2->HTTP1
|
||||
- 2018-10-29 TUN-1160: pass Host header during origin url validation
|
||||
|
||||
2018.10.4
|
||||
- 2018-09-21 AUTH-1070: added SSH/protocol forwarding
|
||||
- 2018-10-19 AUTH-1235: fixed packaging of deb dev file
|
||||
- 2018-10-19 TUN-1097: Host missing from WebSocket request
|
||||
- 2018-10-19 AUTH-1188: UX Review and Changes for CLI SSH Access
|
||||
|
||||
2018.10.3
|
||||
- 2018-10-08 TUN-1099: Bring back changes in 2018.10.1
|
||||
- 2018-10-08 TUN-1098: removed deprecation error
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//Package carrier provides a WebSocket proxy to carry or proxy a connection
|
||||
//from the local client to the edge. See it as a wrapper around any protocol
|
||||
//that it packages up in a WebSocket connection to the edge.
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
// StdinoutStream is empty struct for wrapping stdin/stdout
|
||||
// into a single ReadWriter
|
||||
type StdinoutStream struct {
|
||||
}
|
||||
|
||||
// Read will read from Stdin
|
||||
func (c *StdinoutStream) Read(p []byte) (int, error) {
|
||||
return os.Stdin.Read(p)
|
||||
|
||||
}
|
||||
|
||||
// Write will write to Stdout
|
||||
func (c *StdinoutStream) Write(p []byte) (int, error) {
|
||||
return os.Stdout.Write(p)
|
||||
}
|
||||
|
||||
// StartClient will copy the data from stdin/stdout over a WebSocket connection
|
||||
// to the edge (originURL)
|
||||
func StartClient(logger *logrus.Logger, stream io.ReadWriter, options *StartOptions) error {
|
||||
return serveStream(logger, stream, options)
|
||||
}
|
||||
|
||||
// StartServer will setup a listener on a specified address/port and then
|
||||
// forward connections to the origin by calling `Serve()`.
|
||||
func StartServer(logger *logrus.Logger, address string, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("failed to start forwarding server")
|
||||
return err
|
||||
}
|
||||
logger.Info("Started listening on ", address)
|
||||
return Serve(logger, listener, shutdownC, options)
|
||||
}
|
||||
|
||||
// Serve accepts incoming connections on the specified net.Listener.
|
||||
// Each connection is handled in a new goroutine: its data is copied over a
|
||||
// WebSocket connection to the edge (originURL).
|
||||
// `Serve` always closes `listener`.
|
||||
func Serve(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
|
||||
defer listener.Close()
|
||||
for {
|
||||
select {
|
||||
case <-shutdownC:
|
||||
return nil
|
||||
default:
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go serveConnection(logger, conn, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveConnection handles connections for the Serve() call
|
||||
func serveConnection(logger *logrus.Logger, c net.Conn, options *StartOptions) {
|
||||
defer c.Close()
|
||||
serveStream(logger, c, options)
|
||||
}
|
||||
|
||||
// serveStream will serve the data over the WebSocket stream
|
||||
func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOptions) error {
|
||||
wsConn, err := createWebsocketStream(options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("failed to connect to %s\n", options.OriginURL)
|
||||
return err
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
websocket.Stream(wsConn, conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions) (*websocket.Conn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
|
||||
wsConn, resp, err := websocket.ClientConnect(req, nil)
|
||||
if err != nil && resp != nil && resp.StatusCode > 300 {
|
||||
location, err := resp.Location()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.Contains(location.String(), "cdn-cgi/access/login") {
|
||||
return nil, errors.New("not an Access redirect")
|
||||
}
|
||||
req, err := buildAccessRequest(options.OriginURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wsConn, _, err = websocket.ClientConnect(req, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &websocket.Conn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
// buildAccessRequest builds an HTTP request with the Access token set
|
||||
func buildAccessRequest(originURL string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, originURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := token.FetchToken(req.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to create a new request as FetchToken will modify req (boo mutable)
|
||||
// as it has to follow redirect on the API and such, so here we init a new one
|
||||
originRequest, err := http.NewRequest(http.MethodGet, originURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originRequest.Header.Set("cf-access-token", token)
|
||||
|
||||
return originRequest, nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
ws "github.com/gorilla/websocket"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
// example in Sec-Websocket-Key in rfc6455
|
||||
testSecWebsocketKey = "dGhlIHNhbXBsZSBub25jZQ=="
|
||||
)
|
||||
|
||||
type testStreamer struct {
|
||||
buf *bytes.Buffer
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
func newTestStream() *testStreamer {
|
||||
return &testStreamer{buf: new(bytes.Buffer)}
|
||||
}
|
||||
|
||||
func (s *testStreamer) Read(p []byte) (int, error) {
|
||||
s.l.RLock()
|
||||
defer s.l.RUnlock()
|
||||
return s.buf.Read(p)
|
||||
|
||||
}
|
||||
|
||||
func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
s.l.Lock()
|
||||
defer s.l.Unlock()
|
||||
return s.buf.Write(p)
|
||||
}
|
||||
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
buf := newTestStream()
|
||||
options := &StartOptions{
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
err := StartClient(logger, buf, options)
|
||||
assert.NoError(t, err)
|
||||
buf.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
buf.Read(readBuffer)
|
||||
assert.Equal(t, message, string(readBuffer))
|
||||
}
|
||||
|
||||
func TestStartServer(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:")
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting listener: %v", err)
|
||||
}
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
logger := logrus.New()
|
||||
shutdownC := make(chan struct{})
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
OriginURL: "http://" + ts.Listener.Addr().String(),
|
||||
Headers: nil,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := Serve(logger, listener, shutdownC, options)
|
||||
if err != nil {
|
||||
t.Fatalf("Error running server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
conn.Write([]byte(message))
|
||||
|
||||
readBuffer := make([]byte, len(message))
|
||||
conn.Read(readBuffer)
|
||||
assert.Equal(t, string(readBuffer), message)
|
||||
}
|
||||
|
||||
func newTestWebSocketServer() *httptest.Server {
|
||||
upgrader := ws.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, _ := upgrader.Upgrade(w, r, nil)
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(mt, []byte(message)); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
|
||||
req, err := http.NewRequest("GET", url, stream)
|
||||
if err != nil {
|
||||
t.Fatalf("testRequestHeader error")
|
||||
}
|
||||
|
||||
req.Header.Add("Connection", "Upgrade")
|
||||
req.Header.Add("Upgrade", "WebSocket")
|
||||
req.Header.Add("Sec-Websocket-Key", testSecWebsocketKey)
|
||||
req.Header.Add("Sec-Websocket-Protocol", "tunnel-protocol")
|
||||
req.Header.Add("Sec-Websocket-Version", "13")
|
||||
req.Header.Add("User-Agent", "curl/7.59.0")
|
||||
|
||||
return req
|
||||
}
|
||||
+17
-11
@@ -1,6 +1,7 @@
|
||||
pinned_go: &pinned_go go=1.9.3-1
|
||||
pinned_go: &pinned_go go=1.11.5-1
|
||||
build_dir: &build_dir /cfsetup_build/src/github.com/cloudflare/cloudflared/
|
||||
stretch:
|
||||
default-flavor: stretch
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -46,15 +47,15 @@ stretch:
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -86,4 +87,9 @@ stretch:
|
||||
- export GOPATH=/cfsetup_build/
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- sudo chown -R $(whoami) /cfsetup_build/
|
||||
- go get github.com/BurntSushi/go-sumtype
|
||||
- export PATH="$GOPATH/bin:$PATH"
|
||||
- make test
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
// ssh will start a WS proxy server for server mode
|
||||
// or copy from stdin/stdout for client mode
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh")
|
||||
}
|
||||
originURL := "https://" + hostname
|
||||
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
|
||||
}
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
localForwarder, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
forwarder, err := url.Parse(localForwarder)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
return carrier.StartServer(logger, forwarder.Host, shutdownC, options)
|
||||
}
|
||||
|
||||
return carrier.StartClient(logger, &carrier.StdinoutStream{}, options)
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
split := strings.Split(valuePair, ":")
|
||||
if len(split) > 1 {
|
||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildRequestHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("client", "value")
|
||||
headers.Add("secret", "safe-value")
|
||||
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
||||
}
|
||||
+214
-39
@@ -3,10 +3,15 @@ package access
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/sshgen"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
@@ -14,8 +19,44 @@ import (
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
sshHostnameFlag = "hostname"
|
||||
sshURLFlag = "url"
|
||||
sshHeaderFlag = "header"
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConfigTemplate = `
|
||||
Add this configuration block to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
{{- if .ShortLivedCerts}}
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt cfpipe >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{.Hostname}}.me-cf_key
|
||||
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
|
||||
{{- else}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
{{end}}
|
||||
`
|
||||
)
|
||||
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
logger = log.CreateLogger()
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
func Init(s, g chan struct{}) {
|
||||
shutdownC, graceShutdownC = s, g
|
||||
}
|
||||
|
||||
// Flags return the global flags for Access related commands (hopefully none)
|
||||
func Flags() []cli.Flag {
|
||||
return []cli.Flag{} // no flags yet.
|
||||
@@ -53,7 +94,7 @@ func Commands() []*cli.Command {
|
||||
{
|
||||
Name: "curl",
|
||||
Action: curl,
|
||||
Usage: "curl <args>",
|
||||
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
|
||||
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
|
||||
header when using curl to reach an application behind Access.`,
|
||||
ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.",
|
||||
@@ -61,7 +102,7 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "token",
|
||||
Action: token,
|
||||
Action: generateToken,
|
||||
Usage: "token -app=<url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
@@ -71,6 +112,67 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh",
|
||||
Action: ssh,
|
||||
Aliases: []string{"rdp"},
|
||||
Usage: "",
|
||||
ArgsUsage: "",
|
||||
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
Aliases: []string{"H"},
|
||||
Usage: "specify additional headers you wish to send.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-config",
|
||||
Action: sshConfig,
|
||||
Usage: "",
|
||||
Description: `Prints an example configuration ~/.ssh/config`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: sshGenCertFlag,
|
||||
Usage: "specify if you wish to generate short lived certs.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ssh-gen",
|
||||
Action: sshGen,
|
||||
Usage: "",
|
||||
Description: `Generates a short lived certificate for given hostname`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: sshHostnameFlag,
|
||||
Usage: "specify the hostname of your application.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -86,10 +188,12 @@ func login(c *cli.Context) error {
|
||||
logger.Errorf("Please provide the url of the Access application\n")
|
||||
return err
|
||||
}
|
||||
if _, err := fetchToken(c, appURL); err != nil {
|
||||
token, err := token.FetchToken(appURL)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to fetch token: %s\n", err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -104,18 +208,19 @@ func curl(c *cli.Context) error {
|
||||
return errors.New("incorrect args")
|
||||
}
|
||||
|
||||
cmdArgs, appURL, allowRequest, err := buildCurlCmdArgs(args.Slice())
|
||||
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
|
||||
appURL, err := getAppURL(cmdArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := getTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
token, err = fetchToken(c, appURL)
|
||||
tok, err = token.FetchToken(appURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to refresh token: ", err)
|
||||
return err
|
||||
@@ -123,37 +228,118 @@ func curl(c *cli.Context) error {
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, "-H")
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", token))
|
||||
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
|
||||
return shell.Run("curl", cmdArgs...)
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func token(c *cli.Context) error {
|
||||
func generateToken(c *cli.Context) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
appURL, err := url.Parse(c.String("app"))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
token, err := getTokenIfExists(appURL)
|
||||
if err != nil || token == "" {
|
||||
tok, err := token.GetTokenIfExists(appURL)
|
||||
if err != nil || tok == "" {
|
||||
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(os.Stdout, token); err != nil {
|
||||
if _, err := fmt.Fprint(os.Stdout, tok); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshConfig prints an example SSH config to stdout
|
||||
func sshConfig(c *cli.Context) error {
|
||||
genCertBool := c.Bool(sshGenCertFlag)
|
||||
hostname := c.String(sshHostnameFlag)
|
||||
if hostname == "" {
|
||||
hostname = "[your hostname]"
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Home string
|
||||
ShortLivedCerts bool
|
||||
Hostname string
|
||||
Cloudflared string
|
||||
}
|
||||
|
||||
t := template.Must(template.New("sshConfig").Parse(sshConfigTemplate))
|
||||
return t.Execute(os.Stdout, config{Home: os.Getenv("HOME"), ShortLivedCerts: genCertBool, Hostname: hostname, Cloudflared: cloudflaredPath()})
|
||||
}
|
||||
|
||||
// sshGen generates a short lived certificate for provided hostname
|
||||
func sshGen(c *cli.Context) error {
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
return cli.ShowCommandHelp(c, "ssh-gen")
|
||||
}
|
||||
|
||||
originURL, err := url.Parse("https://" + hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this fetchToken function mutates the appURL param. We should refactor that
|
||||
fetchTokenURL := &url.URL{}
|
||||
*fetchTokenURL = *originURL
|
||||
token, err := token.FetchToken(fetchTokenURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sshgen.GenerateShortLivedCertificate(originURL, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAppURL will pull the appURL needed for fetching a user's Access token
|
||||
func getAppURL(cmdArgs []string) (*url.URL, error) {
|
||||
if len(cmdArgs) < 1 {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, errors.New("not a valid url")
|
||||
}
|
||||
|
||||
u, err := processURL(cmdArgs[0])
|
||||
if err != nil {
|
||||
logger.Error("Please provide a valid URL as the first argument to curl.")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
// parseAllowRequest will parse cmdArgs and return a copy of the args and result
|
||||
// of the allow request was present
|
||||
func parseAllowRequest(cmdArgs []string) ([]string, bool) {
|
||||
if len(cmdArgs) > 1 {
|
||||
if cmdArgs[0] == "--allow-request" || cmdArgs[0] == "-ar" {
|
||||
return cmdArgs[1:], true
|
||||
}
|
||||
}
|
||||
|
||||
return cmdArgs, false
|
||||
}
|
||||
|
||||
// processURL will preprocess the string (parse to a url, convert to punycode, etc).
|
||||
func processURL(s string) (*url.URL, error) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Host == "" {
|
||||
return nil, errors.New("not a valid host")
|
||||
}
|
||||
|
||||
host, err := idna.ToASCII(u.Hostname())
|
||||
if err != nil { // we fail to convert to punycode, just return the url we parsed.
|
||||
return u, nil
|
||||
@@ -167,33 +353,22 @@ func processURL(s string) (*url.URL, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// buildCurlCmdArgs will build the curl cmd args
|
||||
func buildCurlCmdArgs(cmdArgs []string) ([]string, *url.URL, bool, error) {
|
||||
allowRequest, iAllowRequest := false, 0
|
||||
var appURL *url.URL
|
||||
for i, arg := range cmdArgs {
|
||||
if arg == "-allow-request" || arg == "-ar" {
|
||||
iAllowRequest = i
|
||||
allowRequest = true
|
||||
}
|
||||
|
||||
u, err := processURL(arg)
|
||||
if err == nil {
|
||||
appURL = u
|
||||
cmdArgs[i] = appURL.String()
|
||||
// cloudflaredPath pulls the full path of cloudflared on disk
|
||||
func cloudflaredPath() string {
|
||||
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
||||
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
||||
if isFileThere(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
if appURL == nil {
|
||||
logger.Error("Please provide a valid URL.")
|
||||
return cmdArgs, appURL, allowRequest, errors.New("invalid url")
|
||||
}
|
||||
|
||||
if allowRequest {
|
||||
// remove from cmdArgs
|
||||
cmdArgs[iAllowRequest] = cmdArgs[len(cmdArgs)-1]
|
||||
cmdArgs = cmdArgs[:len(cmdArgs)-1]
|
||||
}
|
||||
|
||||
return cmdArgs, appURL, allowRequest, nil
|
||||
return "cloudflared"
|
||||
}
|
||||
|
||||
// isFileThere will check for the presence of candidate path
|
||||
func isFileThere(candidate string) bool {
|
||||
fi, err := os.Stat(candidate)
|
||||
if err != nil || fi.IsDir() || !fi.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// fetchToken will either load a stored token or generate a new one
|
||||
func fetchToken(c *cli.Context, appURL *url.URL) (string, error) {
|
||||
if token, err := getTokenIfExists(appURL); token != "" && err == nil {
|
||||
fmt.Fprintf(os.Stdout, "You have an existing token:\n\n%s\n\n", token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := generateFilePathForTokenURL(appURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
const resourceName, key, value = "token", "token", ""
|
||||
token, err := transfer.Run(c, appURL, resourceName, key, value, path, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// getTokenIfExists will return the token from local storage if it exists
|
||||
func getTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := generateFilePathForTokenURL(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := token.Claims()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ident, err := oidc.IdentityFromClaims(claims)
|
||||
if err == nil && ident.ExpiresAt.After(time.Now()) {
|
||||
return token.Encode(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// generateFilePathForTokenURL will return a filepath for given access application url
|
||||
func generateFilePathForTokenURL(url *url.URL) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-token", url.Hostname(), url.EscapedPath()), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
@@ -60,3 +62,26 @@ func FindDefaultConfigPath() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ValidateUnixSocket ensures --unix-socket param is used exclusively
|
||||
// i.e. it fails if a user specifies both --url and --unix-socket
|
||||
func ValidateUnixSocket(c *cli.Context) (string, error) {
|
||||
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
|
||||
return "", errors.New("--unix-socket must be used exclusivly.")
|
||||
}
|
||||
return c.String("unix-socket"), nil
|
||||
}
|
||||
|
||||
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
|
||||
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
|
||||
func ValidateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
|
||||
@@ -84,7 +84,8 @@ WantedBy=timers.target
|
||||
var sysvTemplate = ServiceTemplate{
|
||||
Path: "/etc/init.d/cloudflared",
|
||||
FileMode: 0755,
|
||||
Content: `# For RedHat and cousins:
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# processname: {{.Path}}
|
||||
|
||||
+61
-11
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
@@ -10,22 +11,32 @@ import (
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
raven "github.com/getsentry/raven-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
licenseUrl = developerPortal + "/licence/"
|
||||
versionText = "Print the version"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
logger = log.CreateLogger()
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
"An existing connection was forcibly closed by the remote host.",
|
||||
"use of closed connection",
|
||||
"You need to enable Argo Smart Routing",
|
||||
"3001 connection closed",
|
||||
"3002 connection dropped",
|
||||
"rpc exception: dial tcp",
|
||||
"rpc exception: EOF",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -39,26 +50,38 @@ func main() {
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
graceShutdownC := make(chan struct{})
|
||||
|
||||
cli.VersionFlag = &cli.BoolFlag{
|
||||
Name: "version",
|
||||
Aliases: []string{"v", "V"},
|
||||
Usage: versionText,
|
||||
}
|
||||
|
||||
app := &cli.App{}
|
||||
app.Name = "cloudflared"
|
||||
app.Usage = "Cloudflare's command-line tool and agent"
|
||||
app.ArgsUsage = "origin-url"
|
||||
app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
|
||||
Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/argo-tunnel/license/),
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(Version, shutdownC, graceShutdownC)
|
||||
app.Before = tunnel.Before
|
||||
app.Commands = commands()
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(shutdownC, graceShutdownC)
|
||||
runApp(app, shutdownC, graceShutdownC)
|
||||
}
|
||||
|
||||
func commands() []*cli.Command {
|
||||
func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
@@ -71,6 +94,15 @@ Otherwise, does nothing.
|
||||
|
||||
To determine if an update happened in a script, check for error code 64.`,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Action: func(c *cli.Context) (err error) {
|
||||
version(c)
|
||||
return nil
|
||||
},
|
||||
Usage: versionText,
|
||||
Description: versionText,
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
@@ -82,14 +114,21 @@ func flags() []cli.Flag {
|
||||
return append(flags, access.Flags()...)
|
||||
}
|
||||
|
||||
func isEmptyInvocation(c *cli.Context) bool {
|
||||
return c.NArg() == 0 && c.NumFlags() == 0
|
||||
}
|
||||
|
||||
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
return func(c *cli.Context) (err error) {
|
||||
if isEmptyInvocation(c) {
|
||||
cli.ShowAppHelpAndExit(c, 1)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
handleError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -107,3 +146,14 @@ func userHomeDir() (string, error) {
|
||||
}
|
||||
return homeDir, nil
|
||||
}
|
||||
|
||||
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
|
||||
func handleError(err error) {
|
||||
errorMessage := err.Error()
|
||||
for _, ignoredErrorMessage := range ignoredErrors {
|
||||
if strings.Contains(errorMessage, ignoredErrorMessage) {
|
||||
return
|
||||
}
|
||||
}
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// GenerateFilePathFromURL will return a filepath for given access application url
|
||||
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
|
||||
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ok, err := config.FileExists(configPath)
|
||||
if !ok && err == nil {
|
||||
// create config directory if doesn't already exist
|
||||
err = os.Mkdir(configPath, 0700)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := strings.Replace(fmt.Sprintf("%s%s-%s", url.Hostname(), url.EscapedPath(), suffix), "/", "-", -1)
|
||||
return filepath.Join(configPath, name), nil
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func runCommand(command string, args ...string) error {
|
||||
func ensureConfigDirExists(configDir string) error {
|
||||
ok, err := config.FileExists(configDir)
|
||||
if !ok && err == nil {
|
||||
err = os.Mkdir(configDir, 0700)
|
||||
err = os.Mkdir(configDir, 0755)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
keyName = "token"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
func FetchToken(appURL *url.URL) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
path, err := path.GenerateFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// this weird parameter is the resource name (token) and the key/value
|
||||
// we want to send to the transfer service. the key is token and the value
|
||||
// is blank (basically just the id generated in the transfer service)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// GetTokenIfExists will return the token from local storage if it exists
|
||||
func GetTokenIfExists(url *url.URL) (string, error) {
|
||||
path, err := path.GenerateFilePathFromURL(url, keyName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token, err := jose.ParseJWT(string(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := token.Claims()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ident, err := oidc.IdentityFromClaims(claims)
|
||||
// AUTH-1404, reauth if the token is about to expire within 15 minutes
|
||||
if err == nil && ident.ExpiresAt.After(time.Now().Add(time.Minute*15)) {
|
||||
return token.Encode(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -10,13 +10,11 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,7 +30,7 @@ var logger = log.CreateLogger()
|
||||
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
|
||||
// the user to complete an action, while it long polls in the background waiting for an
|
||||
// action to be completed to download the resource.
|
||||
func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
|
||||
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -42,23 +40,18 @@ func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See AUTH-1423 for why we use stderr (the way git wraps ssh)
|
||||
err = shell.OpenBrowser(requestURL)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stdout, "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", resourceName, requestURL)
|
||||
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.Stdout, "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)
|
||||
}
|
||||
|
||||
// for local debugging
|
||||
baseURL := baseStoreURL
|
||||
if c.IsSet("url") {
|
||||
baseURL = c.String("url")
|
||||
fmt.Fprintf(os.Stderr, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, open it yourself and visit the URL above.\n", requestURL)
|
||||
}
|
||||
|
||||
var resourceData []byte
|
||||
|
||||
if shouldEncrypt {
|
||||
buf, key, err := transferRequest(baseURL + filepath.Join("transfer", encrypterClient.PublicKey()))
|
||||
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +67,7 @@ func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path st
|
||||
|
||||
resourceData = decrypted
|
||||
} else {
|
||||
buf, _, err := transferRequest(baseURL + filepath.Join(encrypterClient.PublicKey()))
|
||||
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -90,21 +83,19 @@ func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path st
|
||||
|
||||
// BuildRequestURL creates a request suitable for a resource transfer.
|
||||
// it will return a constructed url based off the base url and query key/value provided.
|
||||
// follow will follow redirects.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, follow bool) (string, error) {
|
||||
// cli will build a url for cli transfer request.
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if !follow {
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
response, err := http.Get(baseURL.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return response.Request.URL.String(), nil
|
||||
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
// transferRequest downloads the requested resource from the request URL
|
||||
|
||||
@@ -3,6 +3,8 @@ package tunnel
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"sync"
|
||||
@@ -18,11 +20,13 @@ import (
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/pkg/errors"
|
||||
cli "gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"gopkg.in/urfave/cli.v2/altsrc"
|
||||
)
|
||||
|
||||
@@ -53,20 +57,6 @@ func Commands() []*cli.Command {
|
||||
},
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "hello",
|
||||
Action: helloWorld,
|
||||
Usage: "Run a simple \"Hello World\" server for testing Argo Tunnel.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "Listen on the selected port.",
|
||||
Value: 8080,
|
||||
},
|
||||
},
|
||||
ArgsUsage: " ", // can't be the empty string or we get the default output
|
||||
Hidden: true,
|
||||
},
|
||||
{
|
||||
Name: "proxy-dns",
|
||||
Action: tunneldns.Run,
|
||||
@@ -187,11 +177,11 @@ func Init(v string, s, g chan struct{}) {
|
||||
}
|
||||
|
||||
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
|
||||
raven.SetDSN(sentryDSN)
|
||||
_ = raven.SetDSN(sentryDSN)
|
||||
var wg sync.WaitGroup
|
||||
listeners := gracenet.Net{}
|
||||
errC := make(chan error)
|
||||
connectedSignal := make(chan struct{})
|
||||
connectedSignal := signal.New(make(chan struct{}))
|
||||
dnsReadySignal := make(chan struct{})
|
||||
|
||||
if c.String("config") == "" {
|
||||
@@ -202,9 +192,9 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
return errors.Wrap(err, "Error configuring logger")
|
||||
}
|
||||
|
||||
protoLogger, err := configProtoLogger(c)
|
||||
transportLogger, err := configTransportLogger(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error configuring protocol logger")
|
||||
return errors.Wrap(err, "Error configuring transport logger")
|
||||
}
|
||||
|
||||
if c.IsSet("trace-output") {
|
||||
@@ -220,7 +210,10 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
|
||||
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
|
||||
} else {
|
||||
os.Remove(tmpTraceFile.Name())
|
||||
err := os.Remove(tmpTraceFile.Name())
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Failed to remove the temporary trace file %s", tmpTraceFile.Name())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -232,7 +225,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
}
|
||||
|
||||
if c.String("logfile") != "" {
|
||||
if err := initLogFile(c, logger, protoLogger); err != nil {
|
||||
if err := initLogFile(c, logger, transportLogger); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -288,7 +281,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
if dnsProxyStandAlone(c) {
|
||||
close(connectedSignal)
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
|
||||
}
|
||||
@@ -308,7 +301,21 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
c.Set("url", "https://"+helloListener.Addr().String())
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, protoLogger)
|
||||
if host := hostnameFromURI(c.String("url")); host != "" {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Cannot start Websocket Proxy Server")
|
||||
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errC <- websocket.StartProxyServer(logger, listener, host, shutdownC)
|
||||
}()
|
||||
c.Set("url", "http://"+listener.Addr().String())
|
||||
}
|
||||
|
||||
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -324,19 +331,19 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
|
||||
|
||||
func Before(c *cli.Context) error {
|
||||
if c.String("config") == "" {
|
||||
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
|
||||
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
|
||||
}
|
||||
inputSource, err := config.FindInputSourceContext(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot load configuration from %s", c.String("config"))
|
||||
logger.WithError(err).Errorf("Cannot load configuration from %s", c.String("config"))
|
||||
return err
|
||||
} else if inputSource != nil {
|
||||
err := altsrc.ApplyInputSourceValues(c, inputSource, c.App.Flags)
|
||||
if err != nil {
|
||||
logger.WithError(err).Infof("Cannot apply configuration from %s", c.String("config"))
|
||||
logger.WithError(err).Errorf("Cannot apply configuration from %s", c.String("config"))
|
||||
return err
|
||||
}
|
||||
logger.Infof("Applied configuration from %s", c.String("config"))
|
||||
logger.Debugf("Applied configuration from %s", c.String("config"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -368,13 +375,13 @@ func waitToShutdown(wg *sync.WaitGroup,
|
||||
return err
|
||||
}
|
||||
|
||||
func notifySystemd(waitForSignal chan struct{}) {
|
||||
<-waitForSignal
|
||||
func notifySystemd(waitForSignal *signal.Signal) {
|
||||
<-waitForSignal.Wait()
|
||||
daemon.SdNotify(false, "READY=1")
|
||||
}
|
||||
|
||||
func writePidFile(waitForSignal chan struct{}, pidFile string) {
|
||||
<-waitForSignal
|
||||
func writePidFile(waitForSignal *signal.Signal, pidFile string) {
|
||||
<-waitForSignal.Wait()
|
||||
file, err := os.Create(pidFile)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
|
||||
@@ -383,6 +390,27 @@ func writePidFile(waitForSignal chan struct{}, pidFile string) {
|
||||
fmt.Fprintf(file, "%d", os.Getpid())
|
||||
}
|
||||
|
||||
func hostnameFromURI(uri string) string {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "ssh":
|
||||
return addPortIfMissing(u, 22)
|
||||
case "rdp":
|
||||
return addPortIfMissing(u, 3389)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func addPortIfMissing(uri *url.URL, port int) string {
|
||||
if uri.Port() != "" {
|
||||
return uri.Host
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
}
|
||||
|
||||
func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -417,7 +445,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "cacert",
|
||||
Usage: "Certificate Authority authenticating the Cloudflare tunnel connection.",
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
EnvVars: []string{"TUNNEL_CACERT"},
|
||||
Hidden: true,
|
||||
}),
|
||||
@@ -447,6 +475,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_URL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "unix-socket",
|
||||
Usage: "Path to unix socket to use instead of --url",
|
||||
EnvVars: []string{"TUNNEL_UNIX_SOCKET"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "hostname",
|
||||
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
|
||||
@@ -524,15 +558,16 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "loglevel",
|
||||
Value: "info",
|
||||
Usage: "Application logging level {panic, fatal, error, warn, info, debug}",
|
||||
Usage: "Application logging level {panic, fatal, error, warn, info, debug}. " + debugLevelWarning,
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "proto-loglevel",
|
||||
Name: "transport-loglevel",
|
||||
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
|
||||
Value: "warn",
|
||||
Usage: "Protocol logging level {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL"},
|
||||
Usage: "Transport logging level(previously called protocol logging level) {panic, fatal, error, warn, info, debug}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewUintFlag(&cli.UintFlag{
|
||||
@@ -654,5 +689,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "use-declarative-tunnels",
|
||||
Usage: "Test establishing connections with declarative tunnel methods.",
|
||||
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHostnameFromURI(t *testing.T) {
|
||||
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse:22"))
|
||||
assert.Equal(t, "awesome.warptunnels.horse:22", hostnameFromURI("ssh://awesome.warptunnels.horse"))
|
||||
assert.Equal(t, "awesome.warptunnels.horse:2222", hostnameFromURI("ssh://awesome.warptunnels.horse:2222"))
|
||||
assert.Equal(t, "localhost:3389", hostnameFromURI("rdp://localhost"))
|
||||
assert.Equal(t, "localhost:3390", hostnameFromURI("rdp://localhost:3390"))
|
||||
assert.Equal(t, "", hostnameFromURI("trash"))
|
||||
assert.Equal(t, "", hostnameFromURI("https://awesomesauce.com"))
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -20,13 +19,13 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -48,11 +47,13 @@ func findDefaultOriginCertPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
id := make([]byte, 32)
|
||||
r.Read(id)
|
||||
return hex.EncodeToString(id)
|
||||
func generateRandomClientID(logger *logrus.Logger) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("couldn't create UUID for client ID")
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func handleDeprecatedOptions(c *cli.Context) error {
|
||||
@@ -64,26 +65,21 @@ func handleDeprecatedOptions(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate url. It can be either from --url or argument
|
||||
func validateUrl(c *cli.Context) (string, error) {
|
||||
var url = c.String("url")
|
||||
if c.NArg() > 0 {
|
||||
if c.IsSet("url") {
|
||||
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
|
||||
}
|
||||
url = c.Args().Get(0)
|
||||
}
|
||||
validUrl, err := validation.ValidateUrl(url)
|
||||
return validUrl, err
|
||||
}
|
||||
|
||||
func logClientOptions(c *cli.Context) {
|
||||
flags := make(map[string]interface{})
|
||||
for _, flag := range c.LocalFlagNames() {
|
||||
flags[flag] = c.Generic(flag)
|
||||
}
|
||||
|
||||
sliceFlags := []string{"header", "tag", "proxy-dns-upstream", "upstream", "edge"}
|
||||
for _, sliceFlag := range sliceFlags {
|
||||
if len(c.StringSlice(sliceFlag)) > 0 {
|
||||
flags[sliceFlag] = strings.Join(c.StringSlice(sliceFlag), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(flags) > 0 {
|
||||
logger.Infof("Flags %v", flags)
|
||||
logger.WithFields(flags).Info("Flags")
|
||||
}
|
||||
|
||||
envs := make(map[string]string)
|
||||
@@ -149,15 +145,24 @@ If you don't have a certificate signed by Cloudflare, run the command:
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version string, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *origin.BuildInfo,
|
||||
version string, logger,
|
||||
transportLogger *logrus.Logger,
|
||||
) (*origin.TunnelConfig, error) {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Invalid hostname")
|
||||
return nil, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID = generateRandomClientID()
|
||||
clientID, err = generateRandomClientID(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
@@ -168,16 +173,18 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version st
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
originURL, err := validateUrl(c)
|
||||
originURL, err := config.ValidateUrl(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating origin URL")
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
|
||||
originCert, err := getOriginCert(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCert, err = getOriginCert(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := loadCertPool(c, logger)
|
||||
@@ -188,12 +195,7 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version st
|
||||
|
||||
tunnelMetrics := origin.NewTunnelMetrics()
|
||||
httpTransport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||||
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||||
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||||
@@ -201,42 +203,74 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version st
|
||||
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||||
}
|
||||
|
||||
dialContext := (&net.Dialer{
|
||||
Timeout: c.Duration("proxy-connect-timeout"),
|
||||
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||||
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||||
}).DialContext
|
||||
|
||||
if c.IsSet("unix-socket") {
|
||||
unixSocket, err := config.ValidateUnixSocket(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error validating --unix-socket")
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
|
||||
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
|
||||
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
// if --unix-socket specified, enforce network type "unix"
|
||||
return dialContext(ctx, "unix", unixSocket)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("Proxying tunnel requests to %s", originURL)
|
||||
httpTransport.DialContext = dialContext
|
||||
}
|
||||
|
||||
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||||
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||||
}
|
||||
|
||||
err = validation.ValidateHTTPService(originURL, httpTransport)
|
||||
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 := createTunnelConfig(c)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("unable to create TLS config to connect with edge")
|
||||
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
OriginUrl: originURL,
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
Retries: c.Uint("retries"),
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
ClientID: clientID,
|
||||
BuildInfo: buildInfo,
|
||||
ReportedVersion: version,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
ProtocolLogger: protoLogger,
|
||||
Logger: logger,
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
BuildInfo: buildInfo,
|
||||
ClientID: clientID,
|
||||
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||||
CompressionQuality: c.Uint64("compression-quality"),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
HTTPTransport: httpTransport,
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
Hostname: hostname,
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Logger: logger,
|
||||
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||||
Metrics: tunnelMetrics,
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||||
OriginCert: originCert,
|
||||
OriginUrl: originURL,
|
||||
ReportedVersion: version,
|
||||
Retries: c.Uint("retries"),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
Tags: tags,
|
||||
TlsConfig: toEdgeTLSConfig,
|
||||
TransportLogger: transportLogger,
|
||||
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -253,7 +287,7 @@ func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error)
|
||||
}
|
||||
}
|
||||
|
||||
originCertPool, err := tlsconfig.LoadOriginCertPool(originCustomCAPool)
|
||||
originCertPool, err := loadOriginCertPool(originCustomCAPool)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading the certificate pool")
|
||||
}
|
||||
@@ -266,6 +300,86 @@ func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error)
|
||||
return originCertPool, nil
|
||||
}
|
||||
|
||||
func loadOriginCertPool(originCAPoolPEM []byte) (*x509.CertPool, error) {
|
||||
// Get the global pool
|
||||
certPool, err := loadGlobalCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Then, add any custom origin CA pool the user may have passed
|
||||
if originCAPoolPEM != nil {
|
||||
if !certPool.AppendCertsFromPEM(originCAPoolPEM) {
|
||||
logger.Warn("could not append the provided origin CA to the cloudflared certificate pool")
|
||||
}
|
||||
}
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func loadGlobalCertPool() (*x509.CertPool, error) {
|
||||
// First, obtain the system certificate pool
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" {
|
||||
logger.WithError(err).Warn("error obtaining the system certificates")
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Next, append the Cloudflare CAs into the system pool
|
||||
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
certPool.AddCert(cert)
|
||||
}
|
||||
|
||||
// Finally, add the Hello certificate into the pool (since it's self-signed)
|
||||
helloCert, err := tlsconfig.GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Hello server certificate to cloudflared certificate pool")
|
||||
}
|
||||
certPool.AddCert(helloCert)
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func createTunnelConfig(c *cli.Context) (*tls.Config, error) {
|
||||
var rootCAs []string
|
||||
if c.String("cacert") != "" {
|
||||
rootCAs = append(rootCAs, c.String("cacert"))
|
||||
}
|
||||
edgeAddrs := c.StringSlice("edge")
|
||||
|
||||
userConfig := &tlsconfig.TLSParameters{RootCAs: rootCAs}
|
||||
tlsConfig, err := tlsconfig.GetConfig(userConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsConfig.RootCAs == nil {
|
||||
rootCAPool := x509.NewCertPool()
|
||||
cfRootCA, err := tlsconfig.GetCloudflareRootCA()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not append Cloudflare Root CAs to cloudflared certificate pool")
|
||||
}
|
||||
for _, cert := range cfRootCA {
|
||||
rootCAPool.AddCert(cert)
|
||||
}
|
||||
tlsConfig.RootCAs = rootCAPool
|
||||
tlsConfig.ServerName = "cftunnel.com"
|
||||
} else if len(edgeAddrs) > 0 {
|
||||
// Set for development environments and for testing specific origintunneld instances
|
||||
tlsConfig.ServerName, _, _ = net.SplitHostPort(edgeAddrs[0])
|
||||
}
|
||||
|
||||
if tlsConfig.ServerName == "" && !tlsConfig.InsecureSkipVerify {
|
||||
return nil, fmt.Errorf("either ServerName or InsecureSkipVerify must be specified in the tls.Config")
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// +build ignore
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650`
|
||||
var samplePEM = []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQCb/H0EUrdXEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBPbmUwHhcNMTgwNDI2MTYxMDUxWhcNMjgwNDIzMTYxMDUxWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBPbmUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAwVQD
|
||||
K0SJ25UFLznm2pU3zhzMEvpDEofHVNnCjk4mlDrtVop7PkKZ8pDEmuQANltUrxC8
|
||||
yHBE2wXMv+GlH+bDtwIDAQABMA0GCSqGSIb3DQEBCwUAA0EAjVYQzozIFPkt/HRY
|
||||
uUoZ8zEHIDICb0syFf5VAjm9AgTwIPzUmD+c5vl6LWDnxq7L45nLCzhhQ6YmiwDz
|
||||
X7Wcyg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQDZfCdAJ+mwzDANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBUd28wHhcNMTgwNDI2MTYxMTIwWhcNMjgwNDIzMTYxMTIwWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBUd28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoHKp
|
||||
ROVK3zCSsH7ocYeyRAML4V7SFAbZcb4WIwDnE08oMBVRkQVcW5tqEkvG3RiClfzV
|
||||
wZIJ3CfqKIeSNSDU9wIDAQABMA0GCSqGSIb3DQEBCwUAA0EAJw2gUbnPiq4C2p5b
|
||||
iWzlA9Q7aKo+VQ4H7IZS7tTccr59nVjvH/TG3eWujpnocr4TOqW9M3CK1DF9mUGP
|
||||
3pQ3Jg==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
|
||||
var systemCertPoolSubjects []*pkix.Name
|
||||
|
||||
type certificateFixture struct {
|
||||
ou string
|
||||
cn string
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
systemCertPool, err := x509.SystemCertPool()
|
||||
if isUnrecoverableError(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if systemCertPool == nil {
|
||||
// On Windows, let's just assume the system cert pool was empty
|
||||
systemCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
systemCertPoolSubjects, err = getCertPoolSubjects(systemCertPool)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolJustSystemPool(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
// Remove extra subjects from the cert pool
|
||||
var filteredSystemCertPoolSubjects []*pkix.Name
|
||||
|
||||
t.Log(extraSubjects)
|
||||
|
||||
OUTER:
|
||||
for _, subject := range certPoolSubjects {
|
||||
for _, extraSubject := range extraSubjects {
|
||||
if subject == extraSubject {
|
||||
t.Log(extraSubject)
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
filteredSystemCertPoolSubjects = append(filteredSystemCertPoolSubjects, subject)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(filteredSystemCertPoolSubjects), len(systemCertPoolSubjects))
|
||||
|
||||
difference := subjectSubtract(systemCertPoolSubjects, filteredSystemCertPoolSubjects)
|
||||
assert.Equal(t, 0, len(difference))
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolCFCertificates(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{ou: "CloudFlare Origin SSL ECC Certificate Authority"},
|
||||
{ou: "CloudFlare Origin SSL Certificate Authority"},
|
||||
{cn: "origin-pull.cloudflare.net"},
|
||||
{cn: "Argo Tunnel Sample Hello Server Certificate"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, extraSubjects)
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolWithExtraPEMs(t *testing.T) {
|
||||
certPoolWithoutPEMSubjects := loadCertPoolSubjects(t, nil)
|
||||
certPoolWithPEMSubjects := loadCertPoolSubjects(t, samplePEM)
|
||||
|
||||
difference := subjectSubtract(certPoolWithoutPEMSubjects, certPoolWithPEMSubjects)
|
||||
|
||||
assert.Equal(t, 2, len(difference))
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{cn: "Test One"},
|
||||
{cn: "Test Two"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, difference)
|
||||
}
|
||||
|
||||
func loadCertPoolSubjects(t *testing.T, originCAPoolPEM []byte) []*pkix.Name {
|
||||
certPool, err := loadOriginCertPool(originCAPoolPEM)
|
||||
if isUnrecoverableError(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEmpty(t, certPool.Subjects())
|
||||
certPoolSubjects, err := getCertPoolSubjects(certPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return certPoolSubjects
|
||||
}
|
||||
|
||||
func assertFixturesMatchSubjects(t *testing.T, fixtures []*certificateFixture, subjects []*pkix.Name) {
|
||||
assert.Equal(t, len(fixtures), len(subjects))
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
found := false
|
||||
for _, subject := range subjects {
|
||||
found = found || fixtureMatchesSubjectPredicate(fixture, subject)
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureMatchesSubjectPredicate(fixture *certificateFixture, subject *pkix.Name) bool {
|
||||
cnMatch := true
|
||||
if fixture.cn != "" {
|
||||
cnMatch = fixture.cn == subject.CommonName
|
||||
}
|
||||
|
||||
ouMatch := true
|
||||
if fixture.ou != "" {
|
||||
ouMatch = len(subject.OrganizationalUnit) > 0 && fixture.ou == subject.OrganizationalUnit[0]
|
||||
}
|
||||
|
||||
return cnMatch && ouMatch
|
||||
}
|
||||
|
||||
func subjectSubtract(left []*pkix.Name, right []*pkix.Name) []*pkix.Name {
|
||||
var difference []*pkix.Name
|
||||
|
||||
var found bool
|
||||
for _, r := range right {
|
||||
found = false
|
||||
for _, l := range left {
|
||||
if (*l).String() == (*r).String() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
difference = append(difference, r)
|
||||
}
|
||||
}
|
||||
|
||||
return difference
|
||||
}
|
||||
|
||||
func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
|
||||
var subjects []*pkix.Name
|
||||
|
||||
for _, subject := range certPool.Subjects() {
|
||||
var sequence pkix.RDNSequence
|
||||
_, err := asn1.Unmarshal(subject, &sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := pkix.Name{}
|
||||
name.FillFromRDNSequence(&sequence)
|
||||
|
||||
subjects = append(subjects, &name)
|
||||
}
|
||||
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func isUnrecoverableError(err error) bool {
|
||||
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
)
|
||||
|
||||
func helloWorld(c *cli.Context) error {
|
||||
address := fmt.Sprintf(":%d", c.Int("port"))
|
||||
listener, err := hello.CreateTLSListener(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
err = hello.StartHelloWorldServer(logger, listener, nil)
|
||||
return err
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
|
||||
"Response status, content length and header will also be logged in debug level."
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
func configMainLogger(c *cli.Context) error {
|
||||
@@ -22,18 +25,21 @@ func configMainLogger(c *cli.Context) error {
|
||||
return errors.Wrap(err, "Unknown logging level specified")
|
||||
}
|
||||
logger.SetLevel(logLevel)
|
||||
if logLevel == logrus.DebugLevel {
|
||||
logger.Warn(debugLevelWarning)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configProtoLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
protoLogLevel, err := logrus.ParseLevel(c.String("proto-loglevel"))
|
||||
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
|
||||
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Unknown protocol logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown protocol logging level specified")
|
||||
logger.WithError(err).Fatal("Unknown transport logging level specified")
|
||||
return nil, errors.Wrap(err, "Unknown transport logging level specified")
|
||||
}
|
||||
protoLogger := logrus.New()
|
||||
protoLogger.Level = protoLogLevel
|
||||
return protoLogger, nil
|
||||
transportLogger := logrus.New()
|
||||
transportLogger.Level = transportLogLevel
|
||||
return transportLogger, nil
|
||||
}
|
||||
|
||||
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
@@ -53,6 +59,8 @@ func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
|
||||
}
|
||||
defer f.Close()
|
||||
pathMap := lfshook.PathMap{
|
||||
logrus.DebugLevel: filePath,
|
||||
logrus.WarnLevel: filePath,
|
||||
logrus.InfoLevel: filePath,
|
||||
logrus.ErrorLevel: filePath,
|
||||
logrus.FatalLevel: filePath,
|
||||
|
||||
@@ -33,7 +33,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(c, loginURL, "cert", "callback", callbackStoreURL, path, false)
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
|
||||
return err
|
||||
|
||||
@@ -30,45 +30,56 @@ EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
|
||||
logger = log.CreateLogger()
|
||||
)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
type UpdateOutcome struct {
|
||||
Updated bool
|
||||
Version string
|
||||
Error error
|
||||
}
|
||||
|
||||
func checkForUpdates() ReleaseInfo {
|
||||
func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error != nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func checkForUpdateAndApply() UpdateOutcome {
|
||||
var opts equinox.Options
|
||||
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
resp, err := equinox.Check(appID, opts)
|
||||
switch {
|
||||
case err == equinox.NotAvailableErr:
|
||||
return ReleaseInfo{}
|
||||
return UpdateOutcome{}
|
||||
case err != nil:
|
||||
return ReleaseInfo{Error: err}
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
err = resp.Apply()
|
||||
if err != nil {
|
||||
return ReleaseInfo{Error: err}
|
||||
return UpdateOutcome{Error: err}
|
||||
}
|
||||
|
||||
return ReleaseInfo{Updated: true, Version: resp.ReleaseVersion}
|
||||
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
|
||||
}
|
||||
|
||||
func Update(_ *cli.Context) error {
|
||||
if updateApplied() {
|
||||
os.Exit(64)
|
||||
updateOutcome := loggedUpdate()
|
||||
if updateOutcome.Error != nil {
|
||||
os.Exit(10)
|
||||
}
|
||||
return nil
|
||||
|
||||
if updateOutcome.noUpdate() {
|
||||
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
|
||||
}
|
||||
|
||||
return updateOutcome.Error
|
||||
}
|
||||
|
||||
func Autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
|
||||
tickC := time.Tick(freq)
|
||||
for {
|
||||
if updateApplied() {
|
||||
updateOutcome := loggedUpdate()
|
||||
if updateOutcome.Updated {
|
||||
os.Args = append(os.Args, "--is-autoupdated=true")
|
||||
pid, err := listeners.StartProcess()
|
||||
if err != nil {
|
||||
@@ -88,16 +99,17 @@ func Autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan stru
|
||||
}
|
||||
}
|
||||
|
||||
func updateApplied() bool {
|
||||
releaseInfo := checkForUpdates()
|
||||
if releaseInfo.Updated {
|
||||
logger.Infof("Updated to version %s", releaseInfo.Version)
|
||||
return true
|
||||
// Checks for an update and applies it if one is available
|
||||
func loggedUpdate() UpdateOutcome {
|
||||
updateOutcome := checkForUpdateAndApply()
|
||||
if updateOutcome.Updated {
|
||||
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
|
||||
}
|
||||
if releaseInfo.Error != nil {
|
||||
logger.WithError(releaseInfo.Error).Error("Update check failed")
|
||||
if updateOutcome.Error != nil {
|
||||
logger.WithError(updateOutcome.Error).Error("update check failed")
|
||||
}
|
||||
return false
|
||||
|
||||
return updateOutcome
|
||||
}
|
||||
|
||||
func IsAutoupdateEnabled(c *cli.Context) bool {
|
||||
|
||||
@@ -8,6 +8,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -30,6 +31,10 @@ const (
|
||||
// not defined in golang.org/x/sys/windows package
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681988(v=vs.85).aspx
|
||||
serviceConfigFailureActionsFlag = 4
|
||||
|
||||
// ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1000-1299-
|
||||
serviceControllerConnectionFailure = 1063
|
||||
)
|
||||
|
||||
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
@@ -50,16 +55,48 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
},
|
||||
})
|
||||
|
||||
// `IsAnInteractiveSession()` isn't exactly equivalent to "should the
|
||||
// process run as a normal EXE?" There are legitimate non-service cases,
|
||||
// like running cloudflared in a GCP startup script, for which
|
||||
// `IsAnInteractiveSession()` returns false. For more context, see:
|
||||
// https://github.com/judwhite/go-svc/issues/6
|
||||
// It seems that the "correct way" to check "is this a normal EXE?" is:
|
||||
// 1. attempt to connect to the Service Control Manager
|
||||
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
|
||||
// This involves actually trying to start the service.
|
||||
|
||||
isIntSess, err := svc.IsAnInteractiveSession()
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
}
|
||||
|
||||
if isIntSess {
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && int(errno) == serviceControllerConnectionFailure {
|
||||
// Hack: assume this is a false negative from the IsAnInteractiveSession() check above.
|
||||
// Run the app in "interactive" mode anyway.
|
||||
app.Run(os.Args)
|
||||
return
|
||||
}
|
||||
logger.Fatalf("%s service failed: %v", windowsServiceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
elog, err := eventlog.Open(windowsServiceName)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
|
||||
@@ -68,26 +105,23 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
defer elog.Close()
|
||||
|
||||
elog.Info(1, fmt.Sprintf("%s service starting", windowsServiceName))
|
||||
// Run executes service name by calling windowsService which is a Handler
|
||||
// interface that implements Execute method.
|
||||
// It will set service status to stop after Execute returns
|
||||
err = svc.Run(windowsServiceName, &windowsService{app: app, elog: elog, shutdownC: shutdownC, graceShutdownC: graceShutdownC})
|
||||
if err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", windowsServiceName, err))
|
||||
return
|
||||
defer func() {
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}()
|
||||
|
||||
// the arguments passed here are only meaningful if they were manually
|
||||
// specified by the user, e.g. using the Services console or `sc start`.
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/services/service-entry-point
|
||||
// https://stackoverflow.com/a/6235139
|
||||
var args []string
|
||||
if len(serviceArgs) > 1 {
|
||||
args = serviceArgs
|
||||
} else {
|
||||
// fall back to the arguments from ImagePath (or, as sc calls it, binPath)
|
||||
args = os.Args
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service stopped", windowsServiceName))
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("%s service arguments: %v", windowsServiceName, args))
|
||||
|
||||
type windowsService struct {
|
||||
app *cli.App
|
||||
elog *eventlog.Log
|
||||
shutdownC chan struct{}
|
||||
graceShutdownC chan struct{}
|
||||
}
|
||||
|
||||
// called by the package code at the start of the service
|
||||
func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
statusChan <- svc.Status{State: svc.StartPending}
|
||||
errC := make(chan error)
|
||||
@@ -101,26 +135,23 @@ func (s *windowsService) Execute(args []string, r <-chan svc.ChangeRequest, stat
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
s.elog.Info(1, fmt.Sprintf("control request 1 #%d", c))
|
||||
statusChan <- c.CurrentStatus
|
||||
case svc.Stop:
|
||||
s.elog.Info(1, "received stop control request")
|
||||
close(s.graceShutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
case svc.Shutdown:
|
||||
s.elog.Info(1, "received shutdown control request")
|
||||
close(s.shutdownC)
|
||||
statusChan <- svc.Status{State: svc.StopPending}
|
||||
default:
|
||||
s.elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
case err := <-errC:
|
||||
ssec = true
|
||||
if err != nil {
|
||||
s.elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
elog.Error(1, fmt.Sprintf("cloudflared terminated with error %v", err))
|
||||
errno = 1
|
||||
} else {
|
||||
s.elog.Info(1, "cloudflared terminated without error")
|
||||
elog.Info(1, "cloudflared terminated without error")
|
||||
errno = 0
|
||||
}
|
||||
return
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 5 * time.Second
|
||||
openStreamTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type dialError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e dialError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
type muxerShutdownError struct{}
|
||||
|
||||
func (e muxerShutdownError) Error() string {
|
||||
return "muxer shutdown"
|
||||
}
|
||||
|
||||
type ConnectionConfig struct {
|
||||
TLSConfig *tls.Config
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
Logger *logrus.Entry
|
||||
}
|
||||
|
||||
type connectionHandler interface {
|
||||
serve(ctx context.Context) error
|
||||
connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error)
|
||||
shutdown()
|
||||
}
|
||||
|
||||
type h2muxHandler struct {
|
||||
muxer *h2mux.Muxer
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
type muxedStreamHandler struct {
|
||||
}
|
||||
|
||||
// Implements MuxedStreamHandler interface
|
||||
func (h *muxedStreamHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) serve(ctx context.Context) error {
|
||||
// Serve doesn't return until h2mux is shutdown
|
||||
if err := h.muxer.Serve(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return muxerShutdownError{}
|
||||
}
|
||||
|
||||
// Connect is used to establish connections with cloudflare's edge network
|
||||
func (h *h2muxHandler) connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error) {
|
||||
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer cancel()
|
||||
conn, err := h.newRPConn(openStreamCtx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create new RPC connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
tsClient := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
|
||||
return tsClient.Connect(ctx, parameters)
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) shutdown() {
|
||||
h.muxer.Shutdown()
|
||||
}
|
||||
|
||||
func (h *h2muxHandler) newRPConn(ctx context.Context) (*rpc.Conn, error) {
|
||||
stream, err := h.muxer.OpenStream(ctx, []h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(h.logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(h.logger.WithField("subsystem", "rpc-transport")),
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewConnectionHandler returns a connectionHandler, wrapping h2mux to make RPC calls
|
||||
func newH2MuxHandler(ctx context.Context,
|
||||
config *ConnectionConfig,
|
||||
edgeIP *net.TCPAddr,
|
||||
) (connectionHandler, error) {
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer dialCancel()
|
||||
dialer := net.Dialer{DualStack: true}
|
||||
plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String())
|
||||
if err != nil {
|
||||
return nil, dialError{cause: errors.Wrap(err, "DialContext error")}
|
||||
}
|
||||
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
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
|
||||
Timeout: dialTimeout,
|
||||
Handler: &muxedStreamHandler{},
|
||||
IsClient: true,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &h2muxHandler{
|
||||
muxer: muxer,
|
||||
logger: config.Logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// connectionPool is a pool of connection handlers
|
||||
type connectionPool struct {
|
||||
sync.Mutex
|
||||
connectionHandlers []connectionHandler
|
||||
}
|
||||
|
||||
func (cp *connectionPool) put(h connectionHandler) {
|
||||
cp.Lock()
|
||||
defer cp.Unlock()
|
||||
cp.connectionHandlers = append(cp.connectionHandlers, h)
|
||||
}
|
||||
|
||||
func (cp *connectionPool) close() {
|
||||
cp.Lock()
|
||||
defer cp.Unlock()
|
||||
for _, h := range cp.connectionHandlers {
|
||||
h.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA Warp servers
|
||||
srvService = "warp"
|
||||
srvProto = "tcp"
|
||||
srvName = "cloudflarewarp.com"
|
||||
|
||||
// Used to fallback to DoT when we can't use the default resolver to
|
||||
// discover HA Warp servers (GitHub issue #75).
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = time.Duration(15 * time.Second)
|
||||
)
|
||||
|
||||
var friendlyDNSErrorLines = []string{
|
||||
`Please try the following things to diagnose this issue:`,
|
||||
` 1. ensure that cloudflarewarp.com is returning "warp" service records.`,
|
||||
` Run your system's equivalent of: dig srv _warp._tcp.cloudflarewarp.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/`,
|
||||
}
|
||||
|
||||
func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, error) {
|
||||
if len(addresses) > 0 {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
for _, address := range addresses {
|
||||
// Addresses specified (for testing, usually)
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
}
|
||||
// HA service discovery lookup
|
||||
_, 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=_warp._tcp.cloudflarewarp.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 _warp._tcp.cloudflarewarp.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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package connection
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -0,0 +1,147 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// Waiting time before retrying a failed tunnel connection
|
||||
reconnectDuration = time.Second * 10
|
||||
// SRV record resolution TTL
|
||||
resolveTTL = time.Hour
|
||||
// Interval between establishing new connection
|
||||
connectionInterval = time.Second
|
||||
)
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
ConnectionConfig *ConnectionConfig
|
||||
OriginCert []byte
|
||||
Tags []tunnelpogs.Tag
|
||||
EdgeAddrs []string
|
||||
HAConnections uint
|
||||
Logger *logrus.Logger
|
||||
CloudflaredVersion string
|
||||
}
|
||||
|
||||
// Supervisor is a stateful object that manages connections with the edge
|
||||
type Supervisor struct {
|
||||
config *CloudflaredConfig
|
||||
state *supervisorState
|
||||
connErrors chan error
|
||||
}
|
||||
|
||||
type supervisorState struct {
|
||||
// IPs to connect to cloudflare's edge network
|
||||
edgeIPs []*net.TCPAddr
|
||||
// index of the next element to use in edgeIPs
|
||||
nextEdgeIPIndex int
|
||||
// last time edgeIPs were refreshed
|
||||
lastResolveTime time.Time
|
||||
// ID of this cloudflared instance
|
||||
cloudflaredID uuid.UUID
|
||||
// connectionPool is a pool of connectionHandlers that can be used to make RPCs
|
||||
connectionPool *connectionPool
|
||||
}
|
||||
|
||||
func (s *supervisorState) getNextEdgeIP() *net.TCPAddr {
|
||||
ip := s.edgeIPs[s.nextEdgeIPIndex%len(s.edgeIPs)]
|
||||
s.nextEdgeIPIndex++
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewSupervisor(config *CloudflaredConfig) *Supervisor {
|
||||
return &Supervisor{
|
||||
config: config,
|
||||
state: &supervisorState{
|
||||
connectionPool: &connectionPool{},
|
||||
},
|
||||
connErrors: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(); err != nil {
|
||||
logger.WithError(err).Error("Failed to get edge IPs")
|
||||
return err
|
||||
}
|
||||
defer s.state.connectionPool.close()
|
||||
|
||||
var currentConnectionCount uint
|
||||
expectedConnectionCount := s.config.HAConnections
|
||||
if uint(len(s.state.edgeIPs)) < s.config.HAConnections {
|
||||
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, len(s.state.edgeIPs))
|
||||
expectedConnectionCount = uint(len(s.state.edgeIPs))
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case connErr := <-s.connErrors:
|
||||
logger.WithError(connErr).Warnf("Connection dropped unexpectedly")
|
||||
currentConnectionCount--
|
||||
default:
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
if currentConnectionCount < expectedConnectionCount {
|
||||
h, err := newH2MuxHandler(ctx, s.config.ConnectionConfig, s.state.getNextEdgeIP())
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Failed to create new connection handler")
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
s.connErrors <- h.serve(ctx)
|
||||
}()
|
||||
connResult, err := s.connect(ctx, s.config, s.state.cloudflaredID, h)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("Failed to connect to cloudflared's edge network")
|
||||
h.shutdown()
|
||||
continue
|
||||
}
|
||||
if connErr := connResult.Err; connErr != nil && !connErr.ShouldRetry {
|
||||
logger.WithError(connErr).Errorf("Server respond with don't retry to connect")
|
||||
h.shutdown()
|
||||
return err
|
||||
}
|
||||
logger.Infof("Connected to %s", connResult.ServerInfo.LocationName)
|
||||
s.state.connectionPool.put(h)
|
||||
currentConnectionCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize() error {
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to resolve cloudflare edge network address")
|
||||
}
|
||||
s.state.edgeIPs = edgeIPs
|
||||
s.state.lastResolveTime = time.Now()
|
||||
cloudflaredID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate cloudflared ID")
|
||||
}
|
||||
s.state.cloudflaredID = cloudflaredID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Supervisor) connect(ctx context.Context,
|
||||
config *CloudflaredConfig,
|
||||
cloudflaredID uuid.UUID,
|
||||
h connectionHandler,
|
||||
) (*tunnelpogs.ConnectResult, error) {
|
||||
connectParameters := &tunnelpogs.ConnectParameters{
|
||||
OriginCert: config.OriginCert,
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: 0,
|
||||
CloudflaredVersion: config.CloudflaredVersion,
|
||||
}
|
||||
return h.connect(ctx, connectParameters)
|
||||
}
|
||||
+6
-3
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// HTTP2 error codes: https://http2.github.io/http2-spec/#ErrorCodes
|
||||
ErrHandshakeTimeout = MuxerHandshakeError{"1000 handshake timeout"}
|
||||
ErrBadHandshakeNotSettings = MuxerHandshakeError{"1001 unexpected response"}
|
||||
ErrBadHandshakeUnexpectedAck = MuxerHandshakeError{"1002 unexpected response"}
|
||||
@@ -19,9 +20,11 @@ var (
|
||||
ErrUnknownStream = MuxerProtocolError{"2002 unknown stream", http2.ErrCodeProtocol}
|
||||
ErrInvalidStream = MuxerProtocolError{"2003 invalid stream", http2.ErrCodeProtocol}
|
||||
|
||||
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
|
||||
ErrConnectionClosed = MuxerApplicationError{"3001 connection closed"}
|
||||
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
|
||||
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
|
||||
ErrConnectionClosed = MuxerApplicationError{"3001 connection closed"}
|
||||
ErrConnectionDropped = MuxerApplicationError{"3002 connection dropped"}
|
||||
ErrOpenStreamTimeout = MuxerApplicationError{"3003 open stream timeout"}
|
||||
ErrResponseHeadersTimeout = MuxerApplicationError{"3004 timeout waiting for initial response headers"}
|
||||
|
||||
ErrClosedStream = MuxerStreamError{"4000 stream closed", http2.ErrCodeStreamClosed}
|
||||
)
|
||||
|
||||
+71
-54
@@ -15,11 +15,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFrameSize uint32 = 1 << 14 // Minimum frame size in http2 spec
|
||||
defaultWindowSize uint32 = 65535
|
||||
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size specified in http2 spec
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultFrameSize uint32 = 1 << 14 // Minimum frame size in http2 spec
|
||||
defaultWindowSize uint32 = (1 << 16) - 1 // Minimum window size in http2 spec
|
||||
maxWindowSize uint32 = (1 << 31) - 1 // 2^31-1 = 2147483647, max window size in http2 spec
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultWriteBufferMaxLen int = 1024 * 1024 * 512 // 500mb
|
||||
|
||||
SettingMuxerMagic http2.SettingID = 0x42db
|
||||
MuxerMagicOrigin uint32 = 0xa2e43c8b
|
||||
@@ -49,6 +50,12 @@ type MuxerConfig struct {
|
||||
// Logger to use
|
||||
Logger *log.Entry
|
||||
CompressionQuality CompressionSetting
|
||||
// Initial size for HTTP2 flow control windows
|
||||
DefaultWindowSize uint32
|
||||
// Largest allowable size for HTTP2 flow control windows
|
||||
MaxWindowSize uint32
|
||||
// Largest allowable capacity for the buffer of data to be sent
|
||||
StreamWriteBufferMaxLen int
|
||||
}
|
||||
|
||||
type Muxer struct {
|
||||
@@ -64,7 +71,7 @@ type Muxer struct {
|
||||
// muxWriter is the write process.
|
||||
muxWriter *MuxWriter
|
||||
// muxMetricsUpdater is the process to update metrics
|
||||
muxMetricsUpdater *muxMetricsUpdater
|
||||
muxMetricsUpdater muxMetricsUpdater
|
||||
// newStreamChan is used to create new streams on the writer thread.
|
||||
// The writer will assign the next available stream ID.
|
||||
newStreamChan chan MuxedStreamRequest
|
||||
@@ -98,6 +105,15 @@ func Handshake(
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = defaultTimeout
|
||||
}
|
||||
if config.DefaultWindowSize == 0 {
|
||||
config.DefaultWindowSize = defaultWindowSize
|
||||
}
|
||||
if config.MaxWindowSize == 0 {
|
||||
config.MaxWindowSize = maxWindowSize
|
||||
}
|
||||
if config.StreamWriteBufferMaxLen == 0 {
|
||||
config.StreamWriteBufferMaxLen = defaultWriteBufferMaxLen
|
||||
}
|
||||
// Initialise connection state fields
|
||||
m := &Muxer{
|
||||
f: http2.NewFramer(w, r), // A framer that writes to w and reads from r
|
||||
@@ -147,11 +163,6 @@ func Handshake(
|
||||
// set up reader/writer pair ready for serve
|
||||
streamErrors := NewStreamErrorMap()
|
||||
goAwayChan := make(chan http2.ErrCode, 1)
|
||||
updateRTTChan := make(chan *roundTripMeasurement, 1)
|
||||
updateReceiveWindowChan := make(chan uint32, 1)
|
||||
updateSendWindowChan := make(chan uint32, 1)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
inBoundCounter := NewAtomicCounter(0)
|
||||
outBoundCounter := NewAtomicCounter(0)
|
||||
pingTimestamp := NewPingTimestamp()
|
||||
@@ -168,6 +179,14 @@ func Handshake(
|
||||
config.Logger.Warn("Minimum number of unacked heartbeats to send before closing the connection has been adjusted to ", maxRetries)
|
||||
}
|
||||
|
||||
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
|
||||
m.muxMetricsUpdater = newMuxMetricsUpdater(
|
||||
m.abortChan,
|
||||
compBytesBefore,
|
||||
compBytesAfter,
|
||||
)
|
||||
|
||||
m.explicitShutdown = NewBooleanFuse()
|
||||
m.muxReader = &MuxReader{
|
||||
f: m.f,
|
||||
@@ -179,47 +198,30 @@ func Handshake(
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
connActive: connActive,
|
||||
initialStreamWindow: defaultWindowSize,
|
||||
streamWindowMax: maxWindowSize,
|
||||
initialStreamWindow: m.config.DefaultWindowSize,
|
||||
streamWindowMax: m.config.MaxWindowSize,
|
||||
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
r: m.r,
|
||||
updateRTTChan: updateRTTChan,
|
||||
updateReceiveWindowChan: updateReceiveWindowChan,
|
||||
updateSendWindowChan: updateSendWindowChan,
|
||||
metricsUpdater: m.muxMetricsUpdater,
|
||||
bytesRead: inBoundCounter,
|
||||
updateInBoundBytesChan: updateInBoundBytesChan,
|
||||
}
|
||||
m.muxWriter = &MuxWriter{
|
||||
f: m.f,
|
||||
streams: m.streams,
|
||||
streamErrors: streamErrors,
|
||||
readyStreamChan: m.readyList.ReadyChannel(),
|
||||
newStreamChan: m.newStreamChan,
|
||||
goAwayChan: goAwayChan,
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
idleTimer: NewIdleTimer(idleDuration, maxRetries),
|
||||
connActiveChan: connActive.WaitChannel(),
|
||||
maxFrameSize: defaultFrameSize,
|
||||
updateReceiveWindowChan: updateReceiveWindowChan,
|
||||
updateSendWindowChan: updateSendWindowChan,
|
||||
bytesWrote: outBoundCounter,
|
||||
updateOutBoundBytesChan: updateOutBoundBytesChan,
|
||||
f: m.f,
|
||||
streams: m.streams,
|
||||
streamErrors: streamErrors,
|
||||
readyStreamChan: m.readyList.ReadyChannel(),
|
||||
newStreamChan: m.newStreamChan,
|
||||
goAwayChan: goAwayChan,
|
||||
abortChan: m.abortChan,
|
||||
pingTimestamp: pingTimestamp,
|
||||
idleTimer: NewIdleTimer(idleDuration, maxRetries),
|
||||
connActiveChan: connActive.WaitChannel(),
|
||||
maxFrameSize: defaultFrameSize,
|
||||
metricsUpdater: m.muxMetricsUpdater,
|
||||
bytesWrote: outBoundCounter,
|
||||
}
|
||||
m.muxWriter.headerEncoder = hpack.NewEncoder(&m.muxWriter.headerBuffer)
|
||||
|
||||
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
|
||||
m.muxMetricsUpdater = newMuxMetricsUpdater(
|
||||
updateRTTChan,
|
||||
updateReceiveWindowChan,
|
||||
updateSendWindowChan,
|
||||
updateInBoundBytesChan,
|
||||
updateOutBoundBytesChan,
|
||||
m.abortChan,
|
||||
compBytesBefore,
|
||||
compBytesAfter,
|
||||
)
|
||||
|
||||
if m.compressionQuality.dictSize > 0 && m.compressionQuality.nDicts > 0 {
|
||||
nd, sz := m.compressionQuality.nDicts, m.compressionQuality.dictSize
|
||||
writeDicts, dictChan := newH2WriteDictionaries(
|
||||
@@ -305,6 +307,12 @@ func joinErrorsWithTimeout(errChan <-chan error, receiveCount int, timeout time.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve runs the event loops that comprise h2mux:
|
||||
// - MuxReader.run()
|
||||
// - MuxWriter.run()
|
||||
// - muxMetricsUpdater.run()
|
||||
// In the normal case, Shutdown() is called concurrently with Serve() to stop
|
||||
// these loops.
|
||||
func (m *Muxer) Serve(ctx context.Context) error {
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
@@ -335,6 +343,7 @@ func (m *Muxer) Serve(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown is called to initiate the "happy path" of muxer termination.
|
||||
func (m *Muxer) Shutdown() {
|
||||
m.explicitShutdown.Fuse(true)
|
||||
m.muxReader.Shutdown()
|
||||
@@ -370,15 +379,17 @@ func isConnectionClosedError(err error) bool {
|
||||
|
||||
// OpenStream opens a new data stream with the given headers.
|
||||
// Called by proxy server and tunnel
|
||||
func (m *Muxer) OpenStream(headers []Header, body io.Reader) (*MuxedStream, error) {
|
||||
func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader) (*MuxedStream, error) {
|
||||
stream := &MuxedStream{
|
||||
responseHeadersReceived: make(chan struct{}),
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
receiveWindow: defaultWindowSize,
|
||||
receiveWindowCurrentMax: defaultWindowSize, // Initial window size limit. exponentially increase it when receiveWindow is exhausted
|
||||
receiveWindowMax: maxWindowSize,
|
||||
sendWindow: defaultWindowSize,
|
||||
writeBufferMaxLen: m.config.StreamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: m.config.DefaultWindowSize,
|
||||
receiveWindowCurrentMax: m.config.DefaultWindowSize,
|
||||
receiveWindowMax: m.config.MaxWindowSize,
|
||||
sendWindow: m.config.DefaultWindowSize,
|
||||
readyList: m.readyList,
|
||||
writeHeaders: headers,
|
||||
dictionaries: m.muxReader.dictionaries,
|
||||
@@ -386,25 +397,31 @@ func (m *Muxer) OpenStream(headers []Header, body io.Reader) (*MuxedStream, erro
|
||||
|
||||
select {
|
||||
// Will be received by mux writer
|
||||
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: body}:
|
||||
case <-ctx.Done():
|
||||
return nil, ErrOpenStreamTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case m.newStreamChan <- MuxedStreamRequest{stream: stream, body: body}:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ErrResponseHeadersTimeout
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
case <-stream.responseHeadersReceived:
|
||||
return stream, nil
|
||||
case <-m.abortChan:
|
||||
return nil, ErrConnectionClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) Metrics() *MuxerMetrics {
|
||||
return m.muxMetricsUpdater.Metrics()
|
||||
return m.muxMetricsUpdater.metrics()
|
||||
}
|
||||
|
||||
func (m *Muxer) abort() {
|
||||
m.abortOnce.Do(func() {
|
||||
close(m.abortChan)
|
||||
m.readyList.Close()
|
||||
m.streams.Abort()
|
||||
})
|
||||
}
|
||||
|
||||
+298
-238
@@ -15,7 +15,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
testOpenStreamTimeout = time.Millisecond * 5000
|
||||
testHandshakeTimeout = time.Millisecond * 1000
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -35,86 +43,80 @@ type DefaultMuxerPair struct {
|
||||
doneC chan struct{}
|
||||
}
|
||||
|
||||
func NewDefaultMuxerPair() *DefaultMuxerPair {
|
||||
func NewDefaultMuxerPair(t assert.TestingT, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
return &DefaultMuxerPair{
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Timeout: testHandshakeTimeout,
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: log.NewEntry(log.New()),
|
||||
DefaultWindowSize: (1 << 8) - 1,
|
||||
MaxWindowSize: (1 << 15) - 1,
|
||||
StreamWriteBufferMaxLen: 1024,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
return p
|
||||
}
|
||||
|
||||
func NewCompressedMuxerPair(quality CompressionSetting) *DefaultMuxerPair {
|
||||
func NewCompressedMuxerPair(t assert.TestingT, quality CompressionSetting, f MuxedStreamFunc) *DefaultMuxerPair {
|
||||
origin, edge := net.Pipe()
|
||||
return &DefaultMuxerPair{
|
||||
p := &DefaultMuxerPair{
|
||||
OriginMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Timeout: time.Second,
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: log.NewEntry(log.New()),
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: time.Second,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
CompressionQuality: quality,
|
||||
Logger: log.NewEntry(log.New()),
|
||||
Logger: log.NewEntry(log.New()),
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(t, p.Handshake())
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) Handshake(t *testing.T) {
|
||||
edgeErrC := make(chan error)
|
||||
originErrC := make(chan error)
|
||||
go func() {
|
||||
var err error
|
||||
func (p *DefaultMuxerPair) Handshake() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testHandshakeTimeout)
|
||||
defer cancel()
|
||||
errGroup, _ := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() (err error) {
|
||||
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig)
|
||||
edgeErrC <- err
|
||||
}()
|
||||
go func() {
|
||||
var err error
|
||||
return errors.Wrap(err, "edge handshake failure")
|
||||
})
|
||||
errGroup.Go(func() (err error) {
|
||||
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig)
|
||||
originErrC <- err
|
||||
}()
|
||||
return errors.Wrap(err, "origin handshake failure")
|
||||
})
|
||||
|
||||
select {
|
||||
case err := <-edgeErrC:
|
||||
if err != nil {
|
||||
t.Fatalf("edge handshake failure: %s", err)
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("edge handshake timeout")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-originErrC:
|
||||
if err != nil {
|
||||
t.Fatalf("origin handshake failure: %s", err)
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("origin handshake timeout")
|
||||
}
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) HandshakeAndServe(t *testing.T) {
|
||||
func (p *DefaultMuxerPair) Serve(t assert.TestingT) {
|
||||
ctx := context.Background()
|
||||
p.Handshake(t)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
@@ -149,18 +151,23 @@ func (p *DefaultMuxerPair) Wait(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DefaultMuxerPair) OpenEdgeMuxStream(headers []Header, body io.Reader) (*MuxedStream, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testOpenStreamTimeout)
|
||||
defer cancel()
|
||||
return p.EdgeMux.OpenStream(ctx, headers, body)
|
||||
}
|
||||
|
||||
func TestHandshake(t *testing.T) {
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.Handshake(t)
|
||||
f := func(stream *MuxedStream) error {
|
||||
return nil
|
||||
}
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
AssertIfPipeReadable(t, muxPair.OriginConn)
|
||||
AssertIfPipeReadable(t, muxPair.EdgeConn)
|
||||
}
|
||||
|
||||
func TestSingleStream(t *testing.T) {
|
||||
closeC := make(chan struct{})
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
defer close(closeC)
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
@@ -175,8 +182,6 @@ func TestSingleStream(t *testing.T) {
|
||||
})
|
||||
buf := []byte("Hello world")
|
||||
stream.Write(buf)
|
||||
// after this receive, the edge closed the stream
|
||||
<-closeC
|
||||
n, err := io.ReadFull(stream, buf)
|
||||
if n > 0 {
|
||||
t.Fatalf("read %d bytes after EOF", n)
|
||||
@@ -186,9 +191,10 @@ func TestSingleStream(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
@@ -216,7 +222,6 @@ func TestSingleStream(t *testing.T) {
|
||||
t.Fatalf("expected response body %s, got %s", "Hello world", responseBody)
|
||||
}
|
||||
stream.Close()
|
||||
closeC <- struct{}{}
|
||||
n, err = stream.Write([]byte("aaaaa"))
|
||||
if n > 0 {
|
||||
t.Fatalf("wrote %d bytes after EOF", n)
|
||||
@@ -224,14 +229,11 @@ func TestSingleStream(t *testing.T) {
|
||||
if err != io.EOF {
|
||||
t.Fatalf("expected EOF, got %s", err)
|
||||
}
|
||||
<-closeC
|
||||
}
|
||||
|
||||
func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
bodySize := 1 << 24
|
||||
streamReady := make(chan struct{})
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
@@ -257,14 +259,13 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
if n != len(payload) {
|
||||
t.Fatalf("origin short write: %d/%d bytes", n, len(payload))
|
||||
}
|
||||
t.Log("Payload written; signaling that the stream is ready")
|
||||
streamReady <- struct{}{}
|
||||
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
@@ -282,9 +283,6 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
}
|
||||
responseBody := make([]byte, bodySize)
|
||||
|
||||
<-streamReady
|
||||
t.Log("Received stream ready signal; resuming the test")
|
||||
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
@@ -295,10 +293,7 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMultipleStreams(t *testing.T) {
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
maxStreams := 64
|
||||
errorsC := make(chan error, maxStreams)
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
@@ -314,15 +309,18 @@ func TestMultipleStreams(t *testing.T) {
|
||||
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
maxStreams := 64
|
||||
errorsC := make(chan error, maxStreams)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(maxStreams)
|
||||
for i := 0; i < maxStreams; i++ {
|
||||
go func(tokenId int) {
|
||||
defer wg.Done()
|
||||
tokenString := fmt.Sprintf("%d", tokenId)
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "client-token", Value: tokenString}},
|
||||
nil,
|
||||
)
|
||||
@@ -367,20 +365,18 @@ func TestMultipleStreams(t *testing.T) {
|
||||
log.Error(err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreamsFlowControl failed")
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
maxStreams := 32
|
||||
errorsC := make(chan error, maxStreams)
|
||||
streamReady := make(chan struct{})
|
||||
responseSizes := make([]int32, maxStreams)
|
||||
for i := 0; i < maxStreams; i++ {
|
||||
responseSizes[i] = rand.Int31n(int32(defaultWindowSize << 4))
|
||||
}
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
@@ -398,7 +394,6 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
payload[i] = byte(i % 256)
|
||||
}
|
||||
n, err := stream.Write(payload)
|
||||
streamReady <- struct{}{}
|
||||
if err != nil {
|
||||
t.Fatalf("origin write error: %s", err)
|
||||
}
|
||||
@@ -407,64 +402,48 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(maxStreams)
|
||||
errGroup, _ := errgroup.WithContext(context.Background())
|
||||
for i := 0; i < maxStreams; i++ {
|
||||
go func(tokenId int) {
|
||||
defer wg.Done()
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
errGroup.Go(func() error {
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
errorsC <- fmt.Errorf("stream %d error in OpenStream: %s", stream.streamID, err)
|
||||
return
|
||||
return fmt.Errorf("error in OpenStream: %d %s", stream.streamID, err)
|
||||
}
|
||||
if len(stream.Headers) != 1 {
|
||||
errorsC <- fmt.Errorf("stream %d expected %d headers, got %d", stream.streamID, 1, len(stream.Headers))
|
||||
return
|
||||
return fmt.Errorf("stream %d expected %d headers, got %d", stream.streamID, 1, len(stream.Headers))
|
||||
}
|
||||
if stream.Headers[0].Name != "response-header" {
|
||||
errorsC <- fmt.Errorf("stream %d expected header name %s, got %s", stream.streamID, "response-header", stream.Headers[0].Name)
|
||||
return
|
||||
return fmt.Errorf("stream %d expected header name %s, got %s", stream.streamID, "response-header", stream.Headers[0].Name)
|
||||
}
|
||||
if stream.Headers[0].Value != "responseValue" {
|
||||
errorsC <- fmt.Errorf("stream %d expected header value %s, got %s", stream.streamID, "responseValue", stream.Headers[0].Value)
|
||||
return
|
||||
return fmt.Errorf("stream %d expected header value %s, got %s", stream.streamID, "responseValue", stream.Headers[0].Value)
|
||||
}
|
||||
|
||||
<-streamReady
|
||||
responseBody := make([]byte, responseSizes[(stream.streamID-2)/2])
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
if err != nil {
|
||||
errorsC <- fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
|
||||
return
|
||||
return fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
|
||||
}
|
||||
if n != len(responseBody) {
|
||||
errorsC <- fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(responseBody), n)
|
||||
return
|
||||
return fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(responseBody), n)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errorsC)
|
||||
testFail := false
|
||||
for err := range errorsC {
|
||||
testFail = true
|
||||
log.Error(err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreamsFlowControl failed")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
assert.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
func TestGracefulShutdown(t *testing.T) {
|
||||
sendC := make(chan struct{})
|
||||
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
stream.WriteHeaders([]Header{
|
||||
{Name: "response-header", Value: "responseValue"},
|
||||
})
|
||||
@@ -482,18 +461,19 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
log.Debugf("Handler ends")
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
// Start graceful shutdown of the edge mux - this should also close the origin mux when done
|
||||
muxPair.EdgeMux.Shutdown()
|
||||
close(sendC)
|
||||
if err != nil {
|
||||
t.Fatalf("error in OpenStream: %s", err)
|
||||
}
|
||||
// Start graceful shutdown of the edge mux - this should also close the origin mux when done
|
||||
muxPair.EdgeMux.Shutdown()
|
||||
close(sendC)
|
||||
responseBody := make([]byte, len(responseBuf))
|
||||
log.Debugf("Waiting for %d bytes", len(responseBuf))
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
@@ -514,8 +494,8 @@ func TestUnexpectedShutdown(t *testing.T) {
|
||||
sendC := make(chan struct{})
|
||||
handlerFinishC := make(chan struct{})
|
||||
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
defer close(handlerFinishC)
|
||||
stream.WriteHeaders([]Header{
|
||||
{Name: "response-header", Value: "responseValue"},
|
||||
@@ -536,9 +516,10 @@ func TestUnexpectedShutdown(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
@@ -583,9 +564,8 @@ func EchoHandler(stream *MuxedStream) error {
|
||||
|
||||
func TestOpenAfterDisconnect(t *testing.T) {
|
||||
for i := 0; i < 3; i++ {
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(EchoHandler)
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, EchoHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
switch i {
|
||||
case 0:
|
||||
@@ -600,7 +580,7 @@ func TestOpenAfterDisconnect(t *testing.T) {
|
||||
muxPair.EdgeConn.Close()
|
||||
}
|
||||
|
||||
_, err := muxPair.EdgeMux.OpenStream(
|
||||
_, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
@@ -611,11 +591,10 @@ func TestOpenAfterDisconnect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHPACK(t *testing.T) {
|
||||
muxPair := NewDefaultMuxerPair()
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(EchoHandler)
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewDefaultMuxerPair(t, EchoHandler)
|
||||
muxPair.Serve(t)
|
||||
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
@@ -629,7 +608,7 @@ func TestHPACK(t *testing.T) {
|
||||
stream.Close()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":scheme", Value: "https"},
|
||||
@@ -691,8 +670,6 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
|
||||
func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
|
||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
||||
muxPair := NewCompressedMuxerPair(q)
|
||||
|
||||
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
|
||||
`"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">` +
|
||||
`<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">` +
|
||||
@@ -715,7 +692,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
`</body>` +
|
||||
`</html>`
|
||||
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
var contentType string
|
||||
var pathHeader Header
|
||||
|
||||
@@ -747,8 +724,8 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewCompressedMuxerPair(t, q, f)
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -782,88 +759,105 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Add(len(paths))
|
||||
errorsC := make(chan error, len(paths))
|
||||
|
||||
for i, s := range paths {
|
||||
go func(i int, path string) {
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
go func(index int, path string) {
|
||||
defer wg.Done()
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":scheme", Value: "https"},
|
||||
{Name: ":authority", Value: "tunnel.otterlyadorable.co.uk"},
|
||||
{Name: ":path", Value: path},
|
||||
{Name: "cf-ray", Value: "378948953f044408-SFO-DOG"},
|
||||
{Name: "idx", Value: strconv.Itoa(i)},
|
||||
{Name: "idx", Value: strconv.Itoa(index)},
|
||||
{Name: "accept-encoding", Value: "gzip, br"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("error in OpenStream: %s", err)
|
||||
errorsC <- fmt.Errorf("error in OpenStream: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(i)
|
||||
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(index)
|
||||
responseBody := make([]byte, len(expectBody)*2)
|
||||
n, err := stream.Read(responseBody)
|
||||
if err != nil {
|
||||
log.Printf("error from (*MuxedStream).Read: %s", err)
|
||||
t.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
errorsC <- fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
|
||||
return
|
||||
}
|
||||
if n != len(expectBody) {
|
||||
log.Printf("expected response body to have %d bytes, got %d", len(expectBody), n)
|
||||
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
|
||||
errorsC <- fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(expectBody), n)
|
||||
return
|
||||
}
|
||||
if string(responseBody[:n]) != expectBody {
|
||||
log.Printf("expected response body %s, got %s", expectBody, responseBody[:n])
|
||||
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
|
||||
errorsC <- fmt.Errorf("stream %d expected response body %s, got %s", stream.streamID, expectBody, responseBody[:n])
|
||||
return
|
||||
}
|
||||
wg.Done()
|
||||
}(i, s)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
wg.Wait()
|
||||
close(errorsC)
|
||||
testFail := false
|
||||
for err := range errorsC {
|
||||
testFail = true
|
||||
log.Error(err)
|
||||
}
|
||||
if testFail {
|
||||
t.Fatalf("TestMultipleStreams failed")
|
||||
}
|
||||
|
||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sampleSiteHandler(stream *MuxedStream) error {
|
||||
var contentType string
|
||||
var pathHeader Header
|
||||
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
|
||||
return func(stream *MuxedStream) error {
|
||||
var contentType string
|
||||
var pathHeader Header
|
||||
|
||||
for _, h := range stream.Headers {
|
||||
if h.Name == ":path" {
|
||||
pathHeader = h
|
||||
break
|
||||
for _, h := range stream.Headers {
|
||||
if h.Name == ":path" {
|
||||
pathHeader = h
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pathHeader.Name != ":path" {
|
||||
panic("Couldn't find :path header in test")
|
||||
}
|
||||
if pathHeader.Name != ":path" {
|
||||
return fmt.Errorf("Couldn't find :path header in test")
|
||||
}
|
||||
|
||||
if strings.Contains(pathHeader.Value, "html") {
|
||||
contentType = "text/html; charset=utf-8"
|
||||
} else if strings.Contains(pathHeader.Value, "js") {
|
||||
contentType = "application/javascript"
|
||||
} else if strings.Contains(pathHeader.Value, "css") {
|
||||
contentType = "text/css"
|
||||
} else {
|
||||
contentType = "img/gif"
|
||||
if strings.Contains(pathHeader.Value, "html") {
|
||||
contentType = "text/html; charset=utf-8"
|
||||
} else if strings.Contains(pathHeader.Value, "js") {
|
||||
contentType = "application/javascript"
|
||||
} else if strings.Contains(pathHeader.Value, "css") {
|
||||
contentType = "text/css"
|
||||
} else {
|
||||
contentType = "img/gif"
|
||||
}
|
||||
stream.WriteHeaders([]Header{
|
||||
Header{Name: "content-type", Value: contentType},
|
||||
})
|
||||
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
|
||||
file, ok := files[pathHeader.Value]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s content is not preloaded", pathHeader.Value)
|
||||
}
|
||||
stream.Write(file)
|
||||
log.Debugf("Wrote body for stream %s", pathHeader.Value)
|
||||
return nil
|
||||
}
|
||||
stream.WriteHeaders([]Header{
|
||||
Header{Name: "content-type", Value: contentType},
|
||||
})
|
||||
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
|
||||
b, _ := ioutil.ReadFile("./sample" + pathHeader.Value)
|
||||
stream.Write(b)
|
||||
log.Debugf("Wrote body for stream %s", pathHeader.Value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sampleSiteTest(t *testing.T, muxPair *DefaultMuxerPair, path string) {
|
||||
stream, err := muxPair.EdgeMux.OpenStream(
|
||||
func sampleSiteTest(muxPair *DefaultMuxerPair, path string, files map[string][]byte) error {
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{
|
||||
{Name: ":method", Value: "GET"},
|
||||
{Name: ":scheme", Value: "https"},
|
||||
@@ -875,86 +869,152 @@ func sampleSiteTest(t *testing.T, muxPair *DefaultMuxerPair, path string) {
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("error in OpenStream: %s", err)
|
||||
return fmt.Errorf("error in OpenStream: %v", err)
|
||||
}
|
||||
expectBody, _ := ioutil.ReadFile("./sample" + path)
|
||||
responseBody := make([]byte, len(expectBody))
|
||||
file, ok := files[path]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s content is not preloaded", path)
|
||||
}
|
||||
responseBody := make([]byte, len(file))
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
log.Debugf("Got body for stream %s", path)
|
||||
if err != nil {
|
||||
t.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
return fmt.Errorf("error from (*MuxedStream).Read: %v", err)
|
||||
}
|
||||
if n != len(expectBody) {
|
||||
t.Fatalf("expected response body to have %d bytes, got %d", len(expectBody), n)
|
||||
if n != len(file) {
|
||||
return fmt.Errorf("expected response body to have %d bytes, got %d", len(file), n)
|
||||
}
|
||||
if string(responseBody[:n]) != string(expectBody) {
|
||||
t.Fatalf("expected response body %s, got %s", expectBody, responseBody[:n])
|
||||
if string(responseBody[:n]) != string(file) {
|
||||
return fmt.Errorf("expected response body %s, got %s", file, responseBody[:n])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSampleFiles(paths []string) (map[string][]byte, error) {
|
||||
files := make(map[string][]byte)
|
||||
for _, path := range paths {
|
||||
if _, ok := files[path]; !ok {
|
||||
expectBody, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files[path] = expectBody
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func TestSampleSiteWithDictionaries(t *testing.T) {
|
||||
paths := []string{
|
||||
"./sample/index.html",
|
||||
"./sample/index2.html",
|
||||
"./sample/index1.html",
|
||||
"./sample/ghost-url.min.js",
|
||||
"./sample/jquery.fitvids.js",
|
||||
"./sample/index1.html",
|
||||
"./sample/index2.html",
|
||||
"./sample/index.html",
|
||||
}
|
||||
files, err := loadSampleFiles(paths)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
||||
muxPair := NewCompressedMuxerPair(q)
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewCompressedMuxerPair(t, q, sampleSiteHandler(files))
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
paths := []string{
|
||||
"/index.html",
|
||||
"/index2.html",
|
||||
"/index1.html",
|
||||
"/ghost-url.min.js",
|
||||
"/jquery.fitvids.js",
|
||||
"/index1.html",
|
||||
"/index2.html",
|
||||
"/index.html",
|
||||
}
|
||||
errC := make(chan error, len(paths))
|
||||
|
||||
wg.Add(len(paths))
|
||||
for _, s := range paths {
|
||||
go func(path string) {
|
||||
sampleSiteTest(t, muxPair, path)
|
||||
wg.Done()
|
||||
defer wg.Done()
|
||||
errC <- sampleSiteTest(muxPair, path, files)
|
||||
}(s)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 10*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
wg.Wait()
|
||||
close(errC)
|
||||
|
||||
for err := range errC {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongSiteWithDictionaries(t *testing.T) {
|
||||
paths := []string{
|
||||
"./sample/index.html",
|
||||
"./sample/index1.html",
|
||||
"./sample/index2.html",
|
||||
"./sample/ghost-url.min.js",
|
||||
"./sample/jquery.fitvids.js",
|
||||
}
|
||||
files, err := loadSampleFiles(paths)
|
||||
assert.NoError(t, err)
|
||||
for q := CompressionNone; q <= CompressionMedium; q++ {
|
||||
muxPair := NewCompressedMuxerPair(q)
|
||||
muxPair.OriginMuxConfig.Handler = MuxedStreamFunc(sampleSiteHandler)
|
||||
muxPair.HandshakeAndServe(t)
|
||||
muxPair := NewCompressedMuxerPair(t, q, sampleSiteHandler(files))
|
||||
muxPair.Serve(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
paths := []string{
|
||||
"/index.html",
|
||||
"/index1.html",
|
||||
"/index2.html",
|
||||
"/ghost-url.min.js",
|
||||
"/jquery.fitvids.js"}
|
||||
|
||||
tstLen := 1000
|
||||
wg.Add(tstLen)
|
||||
tstLen := 500
|
||||
errGroup, _ := errgroup.WithContext(context.Background())
|
||||
for i := 0; i < tstLen; i++ {
|
||||
path := paths[rand.Int()%len(paths)]
|
||||
go func(path string) {
|
||||
sampleSiteTest(t, muxPair, path)
|
||||
wg.Done()
|
||||
}(path)
|
||||
errGroup.Go(func() error {
|
||||
path := paths[rand.Int()%len(paths)]
|
||||
return sampleSiteTest(muxPair, path, files)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
assert.NoError(t, errGroup.Wait())
|
||||
|
||||
if q > CompressionNone && muxPair.OriginMux.muxMetricsUpdater.compBytesBefore.Value() <= 100*muxPair.OriginMux.muxMetricsUpdater.compBytesAfter.Value() {
|
||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOpenStream(b *testing.B) {
|
||||
const streams = 5000
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
if stream.Headers[0].Name != "test-header" {
|
||||
b.Fatalf("expected header name %s, got %s", "test-header", stream.Headers[0].Name)
|
||||
}
|
||||
if stream.Headers[0].Value != "headerValue" {
|
||||
b.Fatalf("expected header value %s, got %s", "headerValue", stream.Headers[0].Value)
|
||||
}
|
||||
stream.WriteHeaders([]Header{
|
||||
{Name: "response-header", Value: "responseValue"},
|
||||
})
|
||||
return nil
|
||||
})
|
||||
muxPair := NewDefaultMuxerPair(b, f)
|
||||
muxPair.Serve(b)
|
||||
b.StartTimer()
|
||||
openStreams(b, muxPair, streams)
|
||||
}
|
||||
}
|
||||
|
||||
func openStreams(b *testing.B, muxPair *DefaultMuxerPair, n int) {
|
||||
errGroup, _ := errgroup.WithContext(context.Background())
|
||||
for i := 0; i < n; i++ {
|
||||
errGroup.Go(func() error {
|
||||
_, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
return err
|
||||
})
|
||||
}
|
||||
assert.NoError(b, errGroup.Wait())
|
||||
}
|
||||
|
||||
+103
-35
@@ -17,32 +17,51 @@ type ReadWriteClosedCloser interface {
|
||||
Closed() bool
|
||||
}
|
||||
|
||||
// MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data.
|
||||
type MuxedStream struct {
|
||||
Headers []Header
|
||||
|
||||
streamID uint32
|
||||
|
||||
// The "Receive" end of the stream
|
||||
readBufferLock sync.RWMutex
|
||||
readBuffer ReadWriteClosedCloser
|
||||
// This is the amount of bytes that are in our receive window
|
||||
// (how much data we can receive into this stream).
|
||||
receiveWindow uint32
|
||||
// current receive window size limit. Exponentially increase it when it's exhausted
|
||||
receiveWindowCurrentMax uint32
|
||||
// hard limit set in http2 spec. 2^31-1
|
||||
receiveWindowMax uint32
|
||||
// The desired size increment for receiveWindow.
|
||||
// If this is nonzero, a WINDOW_UPDATE frame needs to be sent.
|
||||
windowUpdate uint32
|
||||
// The headers that were most recently received.
|
||||
// Particularly:
|
||||
// * for an eyeball-initiated stream (as passed to TunnelHandler::ServeStream),
|
||||
// these are the request headers
|
||||
// * for a cloudflared-initiated stream (as created by Register/UnregisterTunnel),
|
||||
// these are the response headers.
|
||||
// They are useful in both of these contexts; hence `Headers` is public.
|
||||
Headers []Header
|
||||
// For use in the context of a cloudflared-initiated stream.
|
||||
responseHeadersReceived chan struct{}
|
||||
|
||||
readBuffer ReadWriteClosedCloser
|
||||
receiveWindow uint32
|
||||
// current window size limit. Exponentially increase it when it's exhausted
|
||||
receiveWindowCurrentMax uint32
|
||||
// limit set in http2 spec. 2^31-1
|
||||
receiveWindowMax uint32
|
||||
|
||||
// nonzero if a WINDOW_UPDATE frame for a stream needs to be sent
|
||||
windowUpdate uint32
|
||||
|
||||
writeLock sync.Mutex
|
||||
// The zero value for Buffer is an empty buffer ready to use.
|
||||
// The "Send" end of the stream
|
||||
writeLock sync.Mutex
|
||||
writeBuffer ReadWriteLengther
|
||||
|
||||
// The maximum capacity that the send buffer should grow to.
|
||||
writeBufferMaxLen int
|
||||
// A channel to be notified when the send buffer is not full.
|
||||
writeBufferHasSpace chan struct{}
|
||||
// This is the amount of bytes that are in the peer's receive window
|
||||
// (how much data we can send from this stream).
|
||||
sendWindow uint32
|
||||
|
||||
readyList *ReadyList
|
||||
// Reference to the muxer's readyList; signal this for stream data to be sent.
|
||||
readyList *ReadyList
|
||||
// The headers that should be sent, and a flag so we only send them once.
|
||||
headersSent bool
|
||||
writeHeaders []Header
|
||||
|
||||
// EOF-related fields
|
||||
// true if the write end of this stream has been closed
|
||||
writeEOF bool
|
||||
// true if we have sent EOF to the peer
|
||||
@@ -50,40 +69,63 @@ type MuxedStream struct {
|
||||
// true if the peer sent us an EOF
|
||||
receivedEOF bool
|
||||
|
||||
// dictionary that was used to compress the stream
|
||||
// Compression-related fields
|
||||
receivedUseDict bool
|
||||
method string
|
||||
contentType string
|
||||
path string
|
||||
dictionaries h2Dictionaries
|
||||
readBufferLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Read(p []byte) (n int, err error) {
|
||||
var readBuffer ReadWriteClosedCloser
|
||||
if s.dictionaries.read != nil {
|
||||
s.readBufferLock.RLock()
|
||||
b := s.readBuffer
|
||||
readBuffer = s.readBuffer
|
||||
s.readBufferLock.RUnlock()
|
||||
return b.Read(p)
|
||||
} else {
|
||||
readBuffer = s.readBuffer
|
||||
}
|
||||
return s.readBuffer.Read(p)
|
||||
n, err = readBuffer.Read(p)
|
||||
s.replenishReceiveWindow(uint32(n))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Write(p []byte) (n int, err error) {
|
||||
// Blocks until len(p) bytes have been written to the buffer
|
||||
func (s *MuxedStream) Write(p []byte) (int, error) {
|
||||
// If assignDictToStream returns success, then it will have acquired the
|
||||
// writeLock. Otherwise we must acquire it ourselves.
|
||||
ok := assignDictToStream(s, p)
|
||||
if !ok {
|
||||
s.writeLock.Lock()
|
||||
}
|
||||
defer s.writeLock.Unlock()
|
||||
|
||||
if s.writeEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err = s.writeBuffer.Write(p)
|
||||
if n != len(p) || err != nil {
|
||||
return n, err
|
||||
totalWritten := 0
|
||||
for totalWritten < len(p) {
|
||||
// If the buffer is full, block till there is more room.
|
||||
// Use a loop to recheck the buffer size after the lock is reacquired.
|
||||
for s.writeBufferMaxLen <= s.writeBuffer.Len() {
|
||||
s.writeLock.Unlock()
|
||||
<-s.writeBufferHasSpace
|
||||
s.writeLock.Lock()
|
||||
}
|
||||
amountToWrite := len(p) - totalWritten
|
||||
spaceAvailable := s.writeBufferMaxLen - s.writeBuffer.Len()
|
||||
if spaceAvailable < amountToWrite {
|
||||
amountToWrite = spaceAvailable
|
||||
}
|
||||
amountWritten, err := s.writeBuffer.Write(p[totalWritten : totalWritten+amountToWrite])
|
||||
totalWritten += amountWritten
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
s.writeNotify()
|
||||
}
|
||||
s.writeNotify()
|
||||
return n, nil
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
func (s *MuxedStream) Close() error {
|
||||
@@ -164,9 +206,9 @@ func (s *MuxedStream) writeNotify() {
|
||||
// receive window (how much data we can send).
|
||||
func (s *MuxedStream) replenishSendWindow(bytes uint32) {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
s.sendWindow += bytes
|
||||
s.writeNotify()
|
||||
s.writeLock.Unlock()
|
||||
}
|
||||
|
||||
// Call by muxreader when it receives a data frame
|
||||
@@ -178,17 +220,30 @@ func (s *MuxedStream) consumeReceiveWindow(bytes uint32) bool {
|
||||
return false
|
||||
}
|
||||
s.receiveWindow -= bytes
|
||||
if s.receiveWindow < s.receiveWindowCurrentMax/2 {
|
||||
if s.receiveWindow < s.receiveWindowCurrentMax/2 && s.receiveWindowCurrentMax < s.receiveWindowMax {
|
||||
// exhausting client send window (how much data client can send)
|
||||
if s.receiveWindowCurrentMax < s.receiveWindowMax {
|
||||
s.receiveWindowCurrentMax <<= 1
|
||||
// and there is room to grow the receive window
|
||||
newMax := s.receiveWindowCurrentMax << 1
|
||||
if newMax > s.receiveWindowMax {
|
||||
newMax = s.receiveWindowMax
|
||||
}
|
||||
s.windowUpdate += s.receiveWindowCurrentMax - s.receiveWindow
|
||||
s.windowUpdate += newMax - s.receiveWindowCurrentMax
|
||||
s.receiveWindowCurrentMax = newMax
|
||||
// notify MuxWriter to write WINDOW_UPDATE frame
|
||||
s.writeNotify()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Arranges for the MuxWriter to send a WINDOW_UPDATE
|
||||
// Called by MuxedStream::Read when data has left the read buffer.
|
||||
func (s *MuxedStream) replenishReceiveWindow(bytes uint32) {
|
||||
s.writeLock.Lock()
|
||||
defer s.writeLock.Unlock()
|
||||
s.windowUpdate += bytes
|
||||
s.writeNotify()
|
||||
}
|
||||
|
||||
// receiveEOF should be called when the peer indicates no more data will be sent.
|
||||
// Returns true if the socket is now closed (i.e. the write side is already closed).
|
||||
func (s *MuxedStream) receiveEOF() (closed bool) {
|
||||
@@ -226,7 +281,8 @@ type streamChunk struct {
|
||||
// true if a HEADERS frame should be sent
|
||||
sendHeaders bool
|
||||
headers []Header
|
||||
// nonzero if a WINDOW_UPDATE frame should be sent
|
||||
// nonzero if a WINDOW_UPDATE frame should be sent;
|
||||
// in that case, it is the increment value to use
|
||||
windowUpdate uint32
|
||||
// true if data frames should be sent
|
||||
sendData bool
|
||||
@@ -249,11 +305,23 @@ func (s *MuxedStream) getChunk() *streamChunk {
|
||||
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
|
||||
}
|
||||
|
||||
// Copies at most s.sendWindow bytes
|
||||
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
|
||||
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
|
||||
s.sendWindow -= uint32(writeLen)
|
||||
|
||||
// Non-blocking channel send. This will allow MuxedStream::Write() to continue, if needed
|
||||
if s.writeBuffer.Len() < s.writeBufferMaxLen {
|
||||
select {
|
||||
case s.writeBufferHasSpace <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// When we write the chunk, we'll write the WINDOW_UPDATE frame if needed
|
||||
s.receiveWindow += s.windowUpdate
|
||||
s.windowUpdate = 0
|
||||
|
||||
// When we write the chunk, we'll write the headers if needed
|
||||
s.headersSent = true
|
||||
|
||||
// if this chunk contains the end of the stream, close the stream now
|
||||
|
||||
+27
-19
@@ -23,47 +23,55 @@ func TestFlowControlSingleStream(t *testing.T) {
|
||||
sendWindow: testWindowSize,
|
||||
readyList: NewReadyList(),
|
||||
}
|
||||
var tempWindowUpdate uint32
|
||||
var tempStreamChunk *streamChunk
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(testWindowSize/2))
|
||||
dataSent := testWindowSize / 2
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempWindowUpdate := stream.windowUpdate
|
||||
|
||||
streamChunk := stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, uint32(0), tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(2))
|
||||
dataSent += 2
|
||||
assert.Equal(t, testWindowSize-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, (testWindowSize<<1)-stream.receiveWindow, stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, testWindowSize, stream.windowUpdate)
|
||||
tempWindowUpdate = stream.windowUpdate
|
||||
|
||||
streamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, (testWindowSize<<1)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.True(t, stream.consumeReceiveWindow(testWindowSize+10))
|
||||
dataSent = testWindowSize + 10
|
||||
dataSent += testWindowSize + 10
|
||||
assert.Equal(t, (testWindowSize<<1)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, (testWindowSize<<2)-stream.receiveWindow, stream.windowUpdate)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, testWindowSize<<1, stream.windowUpdate)
|
||||
tempWindowUpdate = stream.windowUpdate
|
||||
|
||||
streamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, streamChunk.windowUpdate)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
tempStreamChunk = stream.getChunk()
|
||||
assert.Equal(t, tempWindowUpdate, tempStreamChunk.windowUpdate)
|
||||
assert.Equal(t, (testWindowSize<<2)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindowCurrentMax)
|
||||
assert.Equal(t, testWindowSize, stream.sendWindow)
|
||||
assert.Equal(t, uint32(0), stream.windowUpdate)
|
||||
|
||||
assert.False(t, stream.consumeReceiveWindow(testMaxWindowSize+1))
|
||||
assert.Equal(t, testWindowSize<<2, stream.receiveWindow)
|
||||
assert.Equal(t, (testWindowSize<<2)-dataSent, stream.receiveWindow)
|
||||
assert.Equal(t, testMaxWindowSize, stream.receiveWindowCurrentMax)
|
||||
}
|
||||
|
||||
|
||||
+75
-20
@@ -17,7 +17,24 @@ const (
|
||||
updateFreq = time.Second
|
||||
)
|
||||
|
||||
type muxMetricsUpdater struct {
|
||||
type muxMetricsUpdater interface {
|
||||
// metrics returns the latest metrics
|
||||
metrics() *MuxerMetrics
|
||||
// run is a blocking call to start the event loop
|
||||
run(logger *log.Entry) error
|
||||
// updateRTTChan is called by muxReader to report new RTT measurements
|
||||
updateRTT(rtt *roundTripMeasurement)
|
||||
//updateReceiveWindowChan is called by muxReader and muxWriter when receiveWindow size is updated
|
||||
updateReceiveWindow(receiveWindow uint32)
|
||||
//updateSendWindowChan is called by muxReader and muxWriter when sendWindow size is updated
|
||||
updateSendWindow(sendWindow uint32)
|
||||
// updateInBoundBytesChan is called periodicallyby muxReader to report bytesRead
|
||||
updateInBoundBytes(inBoundBytes uint64)
|
||||
// updateOutBoundBytesChan is called periodically by muxWriter to report bytesWrote
|
||||
updateOutBoundBytes(outBoundBytes uint64)
|
||||
}
|
||||
|
||||
type muxMetricsUpdaterImpl struct {
|
||||
// rttData keeps record of rtt, rttMin, rttMax and last measured time
|
||||
rttData *rttData
|
||||
// receiveWindowData keeps record of receive window measurement
|
||||
@@ -28,16 +45,16 @@ type muxMetricsUpdater struct {
|
||||
inBoundRate *rate
|
||||
// outBoundRate is outgoing bytes/sec
|
||||
outBoundRate *rate
|
||||
// updateRTTChan is the channel to receive new RTT measurement from muxReader
|
||||
updateRTTChan <-chan *roundTripMeasurement
|
||||
//updateReceiveWindowChan is the channel to receive updated receiveWindow size from muxReader and muxWriter
|
||||
updateReceiveWindowChan <-chan uint32
|
||||
//updateSendWindowChan is the channel to receive updated sendWindow size from muxReader and muxWriter
|
||||
updateSendWindowChan <-chan uint32
|
||||
// updateInBoundBytesChan us the channel to receive bytesRead from muxReader
|
||||
updateInBoundBytesChan <-chan uint64
|
||||
// updateOutBoundBytesChan us the channel to receive bytesWrote from muxWriter
|
||||
updateOutBoundBytesChan <-chan uint64
|
||||
// updateRTTChan is the channel to receive new RTT measurement
|
||||
updateRTTChan chan *roundTripMeasurement
|
||||
//updateReceiveWindowChan is the channel to receive updated receiveWindow size
|
||||
updateReceiveWindowChan chan uint32
|
||||
//updateSendWindowChan is the channel to receive updated sendWindow size
|
||||
updateSendWindowChan chan uint32
|
||||
// updateInBoundBytesChan us the channel to receive bytesRead
|
||||
updateInBoundBytesChan chan uint64
|
||||
// updateOutBoundBytesChan us the channel to receive bytesWrote
|
||||
updateOutBoundBytesChan chan uint64
|
||||
// shutdownC is to signal the muxerMetricsUpdater to shutdown
|
||||
abortChan <-chan struct{}
|
||||
|
||||
@@ -84,15 +101,16 @@ type rate struct {
|
||||
}
|
||||
|
||||
func newMuxMetricsUpdater(
|
||||
updateRTTChan <-chan *roundTripMeasurement,
|
||||
updateReceiveWindowChan <-chan uint32,
|
||||
updateSendWindowChan <-chan uint32,
|
||||
updateInBoundBytesChan <-chan uint64,
|
||||
updateOutBoundBytesChan <-chan uint64,
|
||||
abortChan <-chan struct{},
|
||||
compBytesBefore, compBytesAfter *AtomicCounter,
|
||||
) *muxMetricsUpdater {
|
||||
return &muxMetricsUpdater{
|
||||
) muxMetricsUpdater {
|
||||
updateRTTChan := make(chan *roundTripMeasurement, 1)
|
||||
updateReceiveWindowChan := make(chan uint32, 1)
|
||||
updateSendWindowChan := make(chan uint32, 1)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
|
||||
return &muxMetricsUpdaterImpl{
|
||||
rttData: newRTTData(),
|
||||
receiveWindowData: newFlowControlData(),
|
||||
sendWindowData: newFlowControlData(),
|
||||
@@ -109,7 +127,7 @@ func newMuxMetricsUpdater(
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdater) Metrics() *MuxerMetrics {
|
||||
func (updater *muxMetricsUpdaterImpl) metrics() *MuxerMetrics {
|
||||
m := &MuxerMetrics{}
|
||||
m.RTT, m.RTTMin, m.RTTMax = updater.rttData.metrics()
|
||||
m.ReceiveWindowAve, m.ReceiveWindowMin, m.ReceiveWindowMax = updater.receiveWindowData.metrics()
|
||||
@@ -120,7 +138,7 @@ func (updater *muxMetricsUpdater) Metrics() *MuxerMetrics {
|
||||
return m
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdater) run(parentLogger *log.Entry) error {
|
||||
func (updater *muxMetricsUpdaterImpl) run(parentLogger *log.Entry) error {
|
||||
logger := parentLogger.WithFields(log.Fields{
|
||||
"subsystem": "mux",
|
||||
"dir": "metrics",
|
||||
@@ -152,6 +170,43 @@ func (updater *muxMetricsUpdater) run(parentLogger *log.Entry) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateRTT(rtt *roundTripMeasurement) {
|
||||
select {
|
||||
case updater.updateRTTChan <- rtt:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateReceiveWindow(receiveWindow uint32) {
|
||||
select {
|
||||
case updater.updateReceiveWindowChan <- receiveWindow:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateSendWindow(sendWindow uint32) {
|
||||
select {
|
||||
case updater.updateSendWindowChan <- sendWindow:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateInBoundBytes(inBoundBytes uint64) {
|
||||
select {
|
||||
case updater.updateInBoundBytesChan <- inBoundBytes:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (updater *muxMetricsUpdaterImpl) updateOutBoundBytes(outBoundBytes uint64) {
|
||||
select {
|
||||
case updater.updateOutBoundBytesChan <- outBoundBytes:
|
||||
case <-updater.abortChan:
|
||||
}
|
||||
}
|
||||
|
||||
func newRTTData() *rttData {
|
||||
return &rttData{}
|
||||
}
|
||||
|
||||
+18
-29
@@ -86,24 +86,11 @@ func TestFlowControlDataUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMuxMetricsUpdater(t *testing.T) {
|
||||
t.Skip("Race condition")
|
||||
updateRTTChan := make(chan *roundTripMeasurement)
|
||||
updateReceiveWindowChan := make(chan uint32)
|
||||
updateSendWindowChan := make(chan uint32)
|
||||
updateInBoundBytesChan := make(chan uint64)
|
||||
updateOutBoundBytesChan := make(chan uint64)
|
||||
abortChan := make(chan struct{})
|
||||
t.Skip("Inherently racy test due to muxMetricsUpdaterImpl.run()")
|
||||
errChan := make(chan error)
|
||||
abortChan := make(chan struct{})
|
||||
compBefore, compAfter := NewAtomicCounter(0), NewAtomicCounter(0)
|
||||
m := newMuxMetricsUpdater(updateRTTChan,
|
||||
updateReceiveWindowChan,
|
||||
updateSendWindowChan,
|
||||
updateInBoundBytesChan,
|
||||
updateOutBoundBytesChan,
|
||||
abortChan,
|
||||
compBefore,
|
||||
compAfter,
|
||||
)
|
||||
m := newMuxMetricsUpdater(abortChan, compBefore, compAfter)
|
||||
logger := log.NewEntry(log.New())
|
||||
|
||||
go func() {
|
||||
@@ -116,42 +103,44 @@ func TestMuxMetricsUpdater(t *testing.T) {
|
||||
// mock muxReader
|
||||
readerStart := time.Now()
|
||||
rm := &roundTripMeasurement{receiveTime: readerStart, sendTime: readerStart}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// Becareful if dataPoints is not divisibile by 4
|
||||
assert.Equal(t, 0, dataPoints%4,
|
||||
"dataPoints is not divisible by 4; this test should be adjusted accordingly")
|
||||
readerSend := readerStart.Add(time.Millisecond)
|
||||
for i := 1; i <= dataPoints/4; i++ {
|
||||
readerReceive := readerSend.Add(time.Duration(i) * time.Millisecond)
|
||||
rm := &roundTripMeasurement{receiveTime: readerReceive, sendTime: readerSend}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
readerSend = readerReceive.Add(time.Millisecond)
|
||||
m.updateReceiveWindow(uint32(i))
|
||||
m.updateSendWindow(uint32(i))
|
||||
|
||||
updateReceiveWindowChan <- uint32(i)
|
||||
updateSendWindowChan <- uint32(i)
|
||||
|
||||
updateInBoundBytesChan <- uint64(i)
|
||||
m.updateInBoundBytes(uint64(i))
|
||||
}
|
||||
}()
|
||||
|
||||
// mock muxWriter
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
assert.Equal(t, 0, dataPoints%4,
|
||||
"dataPoints is not divisible by 4; this test should be adjusted accordingly")
|
||||
for j := dataPoints/4 + 1; j <= dataPoints/2; j++ {
|
||||
updateReceiveWindowChan <- uint32(j)
|
||||
updateSendWindowChan <- uint32(j)
|
||||
m.updateReceiveWindow(uint32(j))
|
||||
m.updateSendWindow(uint32(j))
|
||||
|
||||
// should always be disgard since the send time is before readerSend
|
||||
// should always be disgarded since the send time is before readerSend
|
||||
rm := &roundTripMeasurement{receiveTime: readerStart, sendTime: readerStart.Add(-time.Duration(j*dataPoints) * time.Millisecond)}
|
||||
updateRTTChan <- rm
|
||||
m.updateRTT(rm)
|
||||
|
||||
updateOutBoundBytesChan <- uint64(j)
|
||||
m.updateOutBoundBytes(uint64(j))
|
||||
}
|
||||
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
metrics := m.Metrics()
|
||||
metrics := m.metrics()
|
||||
points := dataPoints / 2
|
||||
assert.Equal(t, time.Millisecond, metrics.RTTMin)
|
||||
assert.Equal(t, time.Duration(dataPoints/4)*time.Millisecond, metrics.RTTMax)
|
||||
|
||||
+15
-17
@@ -35,18 +35,14 @@ type MuxReader struct {
|
||||
initialStreamWindow uint32
|
||||
// The max value for the send window of a stream.
|
||||
streamWindowMax uint32
|
||||
// The max size for the write buffer of a stream
|
||||
streamWriteBufferMaxLen int
|
||||
// r is a reference to the underlying connection used when shutting down.
|
||||
r io.Closer
|
||||
// updateRTTChan is the channel to send new RTT measurement to muxerMetricsUpdater
|
||||
updateRTTChan chan<- *roundTripMeasurement
|
||||
// updateReceiveWindowChan is the channel to update receiveWindow size to muxerMetricsUpdater
|
||||
updateReceiveWindowChan chan<- uint32
|
||||
// updateSendWindowChan is the channel to update sendWindow size to muxerMetricsUpdater
|
||||
updateSendWindowChan chan<- uint32
|
||||
// bytesRead is the amount of bytes read from data frame since the last time we send bytes read to metrics
|
||||
// metricsUpdater is used to report metrics
|
||||
metricsUpdater muxMetricsUpdater
|
||||
// bytesRead is the amount of bytes read from data frames since the last time we called metricsUpdater.updateInBoundBytes()
|
||||
bytesRead *AtomicCounter
|
||||
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
|
||||
updateInBoundBytesChan chan<- uint64
|
||||
// dictionaries holds the h2 cross-stream compression dictionaries
|
||||
dictionaries h2Dictionaries
|
||||
}
|
||||
@@ -79,7 +75,7 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
|
||||
case <-r.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
r.updateInBoundBytesChan <- r.bytesRead.Count()
|
||||
r.metricsUpdater.updateInBoundBytes(r.bytesRead.Count())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -153,6 +149,8 @@ func (r *MuxReader) newMuxedStream(streamID uint32) *MuxedStream {
|
||||
streamID: streamID,
|
||||
readBuffer: NewSharedBuffer(),
|
||||
writeBuffer: &bytes.Buffer{},
|
||||
writeBufferMaxLen: r.streamWriteBufferMaxLen,
|
||||
writeBufferHasSpace: make(chan struct{}, 1),
|
||||
receiveWindow: r.initialStreamWindow,
|
||||
receiveWindowCurrentMax: r.initialStreamWindow,
|
||||
receiveWindowMax: r.streamWindowMax,
|
||||
@@ -285,7 +283,7 @@ func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.E
|
||||
if !stream.consumeReceiveWindow(uint32(len(data))) {
|
||||
return r.streamError(stream.streamID, http2.ErrCodeFlowControl)
|
||||
}
|
||||
r.updateReceiveWindowChan <- stream.getReceiveWindow()
|
||||
r.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -297,13 +295,13 @@ func (r *MuxReader) receivePingData(frame *http2.PingFrame) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update updates the computed values with a new measurement.
|
||||
// outgoingTime is the time that the probe was sent.
|
||||
// We assume that time.Now() is the time we received that probe.
|
||||
r.updateRTTChan <- &roundTripMeasurement{
|
||||
// Update the computed RTT aggregations with a new measurement.
|
||||
// `ts` is the time that the probe was sent.
|
||||
// We assume that `time.Now()` is the time we received that probe.
|
||||
r.metricsUpdater.updateRTT(&roundTripMeasurement{
|
||||
receiveTime: time.Now(),
|
||||
sendTime: time.Unix(0, ts),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Receive a GOAWAY from the peer. Gracefully shut down our connection.
|
||||
@@ -464,7 +462,7 @@ func (r *MuxReader) updateStreamWindow(frame *http2.WindowUpdateFrame) error {
|
||||
return nil
|
||||
}
|
||||
stream.replenishSendWindow(frame.Increment)
|
||||
r.updateSendWindowChan <- stream.getSendWindow()
|
||||
r.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+7
-10
@@ -40,14 +40,11 @@ type MuxWriter struct {
|
||||
headerEncoder *hpack.Encoder
|
||||
// headerBuffer is the temporary buffer used by headerEncoder.
|
||||
headerBuffer bytes.Buffer
|
||||
// updateReceiveWindowChan is the channel to update receiveWindow size to muxerMetricsUpdater
|
||||
updateReceiveWindowChan chan<- uint32
|
||||
// updateSendWindowChan is the channel to update sendWindow size to muxerMetricsUpdater
|
||||
updateSendWindowChan chan<- uint32
|
||||
// bytesWrote is the amount of bytes wrote to data frame since the last time we send bytes wrote to metrics
|
||||
|
||||
// metricsUpdater is used to report metrics
|
||||
metricsUpdater muxMetricsUpdater
|
||||
// bytesWrote is the amount of bytes written to data frames since the last time we called metricsUpdater.updateOutBoundBytes()
|
||||
bytesWrote *AtomicCounter
|
||||
// updateOutBoundBytesChan is the channel to send bytesWrote to muxerMetricsUpdater
|
||||
updateOutBoundBytesChan chan<- uint64
|
||||
|
||||
useDictChan <-chan useDictRequest
|
||||
}
|
||||
@@ -83,7 +80,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
case <-w.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
w.updateOutBoundBytesChan <- w.bytesWrote.Count()
|
||||
w.metricsUpdater.updateOutBoundBytes(w.bytesWrote.Count())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -172,8 +169,8 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
|
||||
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) error {
|
||||
logger.Debug("writable")
|
||||
chunk := stream.getChunk()
|
||||
w.updateReceiveWindowChan <- stream.getReceiveWindow()
|
||||
w.updateSendWindowChan <- stream.getSendWindow()
|
||||
w.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
|
||||
w.metricsUpdater.updateSendWindow(stream.getSendWindow())
|
||||
if chunk.sendHeadersFrame() {
|
||||
err := w.writeHeaders(chunk.streamID, chunk.headers)
|
||||
if err != nil {
|
||||
|
||||
+24
-13
@@ -1,15 +1,23 @@
|
||||
package h2mux
|
||||
|
||||
import "sync"
|
||||
|
||||
// ReadyList multiplexes several event signals onto a single channel.
|
||||
type ReadyList struct {
|
||||
// signalC is used to signal that a stream can be enqueued
|
||||
signalC chan uint32
|
||||
waitC chan uint32
|
||||
// waitC is used to signal the ID of the first ready descriptor
|
||||
waitC chan uint32
|
||||
// doneC is used to signal that run should terminate
|
||||
doneC chan struct{}
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewReadyList() *ReadyList {
|
||||
rl := &ReadyList{
|
||||
signalC: make(chan uint32),
|
||||
waitC: make(chan uint32),
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
go rl.run()
|
||||
return rl
|
||||
@@ -17,7 +25,11 @@ func NewReadyList() *ReadyList {
|
||||
|
||||
// ID is the stream ID
|
||||
func (r *ReadyList) Signal(ID uint32) {
|
||||
r.signalC <- ID
|
||||
select {
|
||||
case r.signalC <- ID:
|
||||
// ReadyList already closed
|
||||
case <-r.doneC:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReadyList) ReadyChannel() <-chan uint32 {
|
||||
@@ -25,7 +37,9 @@ func (r *ReadyList) ReadyChannel() <-chan uint32 {
|
||||
}
|
||||
|
||||
func (r *ReadyList) Close() {
|
||||
close(r.signalC)
|
||||
r.closeOnce.Do(func() {
|
||||
close(r.doneC)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *ReadyList) run() {
|
||||
@@ -35,28 +49,25 @@ func (r *ReadyList) run() {
|
||||
activeDescriptors := newReadyDescriptorMap()
|
||||
for {
|
||||
if firstReady == nil {
|
||||
// Wait for first ready descriptor
|
||||
i, ok := <-r.signalC
|
||||
if !ok {
|
||||
// closed
|
||||
select {
|
||||
case i := <-r.signalC:
|
||||
firstReady = activeDescriptors.SetIfMissing(i)
|
||||
case <-r.doneC:
|
||||
return
|
||||
}
|
||||
firstReady = activeDescriptors.SetIfMissing(i)
|
||||
}
|
||||
select {
|
||||
case r.waitC <- firstReady.ID:
|
||||
activeDescriptors.Delete(firstReady.ID)
|
||||
firstReady = queue.Dequeue()
|
||||
case i, ok := <-r.signalC:
|
||||
if !ok {
|
||||
// closed
|
||||
return
|
||||
}
|
||||
case i := <-r.signalC:
|
||||
newReady := activeDescriptors.SetIfMissing(i)
|
||||
if newReady != nil {
|
||||
// key doesn't exist
|
||||
queue.Enqueue(newReady)
|
||||
}
|
||||
case <-r.doneC:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-24
@@ -3,36 +3,59 @@ package h2mux
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReadyList(t *testing.T) {
|
||||
func assertEmpty(t *testing.T, rl *ReadyList) {
|
||||
select {
|
||||
case <-rl.ReadyChannel():
|
||||
t.Fatal("Spurious wakeup")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func assertClosed(t *testing.T, rl *ReadyList) {
|
||||
select {
|
||||
case _, ok := <-rl.ReadyChannel():
|
||||
assert.False(t, ok, "ReadyChannel was not closed")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func receiveWithTimeout(t *testing.T, rl *ReadyList) uint32 {
|
||||
select {
|
||||
case i := <-rl.ReadyChannel():
|
||||
return i
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyListEmpty(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
c := rl.ReadyChannel()
|
||||
// helper functions
|
||||
assertEmpty := func() {
|
||||
select {
|
||||
case <-c:
|
||||
t.Fatalf("Spurious wakeup")
|
||||
default:
|
||||
}
|
||||
}
|
||||
receiveWithTimeout := func() uint32 {
|
||||
select {
|
||||
case i := <-c:
|
||||
return i
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatalf("Timeout")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// no signals, receive should fail
|
||||
assertEmpty()
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
func TestReadyListSignal(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
assertEmpty(t, rl)
|
||||
|
||||
rl.Signal(0)
|
||||
if receiveWithTimeout() != 0 {
|
||||
if receiveWithTimeout(t, rl) != 0 {
|
||||
t.Fatalf("Received wrong ID of signalled event")
|
||||
}
|
||||
// no new signals, receive should fail
|
||||
assertEmpty()
|
||||
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
|
||||
func TestReadyListMultipleSignals(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
assertEmpty(t, rl)
|
||||
|
||||
// Signals should not block;
|
||||
// Duplicate unhandled signals should not cause multiple wakeups
|
||||
signalled := [5]bool{}
|
||||
@@ -42,12 +65,45 @@ func TestReadyList(t *testing.T) {
|
||||
}
|
||||
// All signals should be received once (in any order)
|
||||
for range signalled {
|
||||
i := receiveWithTimeout()
|
||||
i := receiveWithTimeout(t, rl)
|
||||
if signalled[i] {
|
||||
t.Fatalf("Received signal %d more than once", i)
|
||||
}
|
||||
signalled[i] = true
|
||||
}
|
||||
for i := range signalled {
|
||||
if !signalled[i] {
|
||||
t.Fatalf("Never received signal %d", i)
|
||||
}
|
||||
}
|
||||
assertEmpty(t, rl)
|
||||
}
|
||||
|
||||
func TestReadyListClose(t *testing.T) {
|
||||
rl := NewReadyList()
|
||||
rl.Close()
|
||||
|
||||
// readyList.run() occurs in a separate goroutine,
|
||||
// so there's no way to directly check that run() has terminated.
|
||||
// Perform an indirect check: is the ready channel closed?
|
||||
assertClosed(t, rl)
|
||||
|
||||
// a second rl.Close() shouldn't cause a panic
|
||||
rl.Close()
|
||||
|
||||
// Signal shouldn't block after Close()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
rl.Signal(uint32(i))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("Test timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyDescriptorQueue(t *testing.T) {
|
||||
|
||||
+1
-3
@@ -43,7 +43,7 @@ const indexTemplate = `
|
||||
<meta name="description" content="Argo Tunnel Connection">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}section{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent;-webkit-text-decoration-skip:objects}/* 1 */::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */a,body,dd,div,dl,dt,h1,h4,html,p,section{box-sizing:border-box}.bt{border-top-style:solid;border-top-width:1px}.bl{border-left-style:solid;border-left-width:1px}.b--orange{border-color:#f38020}.br1{border-radius:.125rem}.bw2{border-width:.25rem}.dib{display:inline-block}.sans-serif{font-family:open sans,-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.code{font-family:Consolas,monaco,monospace}.b{font-weight:700}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.mw-100{max-width:100%}.mw4{max-width:8rem}.mw7{max-width:48rem}.bg-light-gray{background-color:#f7f7f7}.link-hover:hover{background-color:#1f679e}.white{color:#fff}.bg-white{background-color:#fff}.bg-blue{background-color:#408bc9}.pb2{padding-bottom:.5rem}.pb6{padding-bottom:8rem}.pt3{padding-top:1rem}.pt5{padding-top:4rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ml0{margin-left:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mt5{margin-top:4rem}.ttu{text-transform:uppercase}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.center{margin-left:auto}.center{margin-right:auto}@media screen and (min-width:30em){.f2-ns{font-size:2.25rem}}@media screen and (min-width:30em) and (max-width:60em){.f5-m{font-size:1rem}}@media screen and (min-width:60em){.f4-l{font-size:1.25rem}}
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}section{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent;-webkit-text-decoration-skip:objects}/* 1 */::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */a,body,dd,div,dl,dt,h1,h4,html,p,section{box-sizing:border-box}.bt{border-top-style:solid;border-top-width:1px}.bl{border-left-style:solid;border-left-width:1px}.b--orange{border-color:#f38020}.br1{border-radius:.125rem}.bw2{border-width:.25rem}.dib{display:inline-block}.sans-serif{font-family:open sans,-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.overflow-x-auto{overflow-x:auto}.code{font-family:Consolas,monaco,monospace}.b{font-weight:700}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.mw-100{max-width:100%}.mw4{max-width:8rem}.mw7{max-width:48rem}.bg-light-gray{background-color:#f7f7f7}.link-hover:hover{background-color:#1f679e}.white{color:#fff}.bg-white{background-color:#fff}.bg-blue{background-color:#408bc9}.pb2{padding-bottom:.5rem}.pb6{padding-bottom:8rem}.pt3{padding-top:1rem}.pt5{padding-top:4rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ml0{margin-left:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mt5{margin-top:4rem}.ttu{text-transform:uppercase}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.center{margin-left:auto}.center{margin-right:auto}@media screen and (min-width:30em){.f2-ns{font-size:2.25rem}}@media screen and (min-width:30em) and (max-width:60em){.f5-m{font-size:1rem}}@media screen and (min-width:60em){.f4-l{font-size:1.25rem}}
|
||||
.st0{fill:#FFF}.st1{fill:#f48120}.st2{fill:#faad3f}.st3{fill:#404041}
|
||||
</style>
|
||||
</head>
|
||||
@@ -91,7 +91,6 @@ const indexTemplate = `
|
||||
</html>
|
||||
`
|
||||
|
||||
|
||||
func StartHelloWorldServer(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}) error {
|
||||
logger.Infof("Starting Hello World server at %s", listener.Addr())
|
||||
serverName := defaultServerName
|
||||
@@ -132,7 +131,6 @@ func CreateTLSListener(address string) (net.Listener, error) {
|
||||
return listener, err
|
||||
}
|
||||
|
||||
|
||||
func uptimeHandler(startTime time.Time) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Note that if autoupdate is enabled, the uptime is reset when a new client
|
||||
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/trace"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Redeclare time functions so they can be overridden in tests.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func immediateTimeAfter(time.Duration) <-chan time.Time {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/golibs/lrucache"
|
||||
)
|
||||
|
||||
// StatusPage.io API docs:
|
||||
// https://www.cloudflarestatus.com/api/v2/#incidents-unresolved
|
||||
const (
|
||||
activeIncidentsURL = "https://yh6f0r4529hb.statuspage.io/api/v2/incidents/unresolved.json"
|
||||
argoTunnelKeyword = "argo tunnel"
|
||||
incidentDetailsPrefix = "https://www.cloudflarestatus.com/incidents/"
|
||||
)
|
||||
|
||||
// IncidentLookup is an object that checks for active incidents in
|
||||
// the Cloudflare infrastructure.
|
||||
type IncidentLookup interface {
|
||||
ActiveIncidents() []Incident
|
||||
}
|
||||
|
||||
// NewIncidentLookup returns a new IncidentLookup instance that caches its
|
||||
// results with a 1-minute TTL.
|
||||
func NewIncidentLookup() IncidentLookup {
|
||||
return newCachedIncidentLookup(fetchActiveIncidents)
|
||||
}
|
||||
|
||||
type IncidentUpdate struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
type Incident struct {
|
||||
Name string
|
||||
ID string `json:"id"`
|
||||
Updates []IncidentUpdate `json:"incident_updates"`
|
||||
}
|
||||
|
||||
type StatusPage struct {
|
||||
Incidents []Incident
|
||||
}
|
||||
|
||||
func (i Incident) URL() string {
|
||||
return incidentDetailsPrefix + i.ID
|
||||
}
|
||||
|
||||
func parseStatusPage(data []byte) (*StatusPage, error) {
|
||||
var result StatusPage
|
||||
err := json.Unmarshal(data, &result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func isArgoTunnelIncident(i Incident) bool {
|
||||
if strings.Contains(strings.ToLower(i.Name), argoTunnelKeyword) {
|
||||
return true
|
||||
}
|
||||
for _, u := range i.Updates {
|
||||
if strings.Contains(strings.ToLower(u.Body), argoTunnelKeyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchActiveIncidents() (incidents []Incident) {
|
||||
resp, err := http.Get(activeIncidentsURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
statusPage, err := parseStatusPage(body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, i := range statusPage.Incidents {
|
||||
if isArgoTunnelIncident(i) {
|
||||
incidents = append(incidents, i)
|
||||
}
|
||||
}
|
||||
return incidents
|
||||
}
|
||||
|
||||
type cachedIncidentLookup struct {
|
||||
cache *lrucache.LRUCache
|
||||
ttl time.Duration
|
||||
uncachedLookup func() []Incident
|
||||
}
|
||||
|
||||
func newCachedIncidentLookup(uncachedLookup func() []Incident) *cachedIncidentLookup {
|
||||
return &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: time.Minute,
|
||||
uncachedLookup: uncachedLookup,
|
||||
}
|
||||
}
|
||||
|
||||
// We only need one cache entry. Always use the empty string as its key.
|
||||
const cacheKey = ""
|
||||
|
||||
func (c *cachedIncidentLookup) ActiveIncidents() []Incident {
|
||||
if cached, ok := c.cache.GetNotStale(cacheKey); ok {
|
||||
if incidents, ok := cached.([]Incident); ok {
|
||||
return incidents
|
||||
}
|
||||
}
|
||||
incidents := c.uncachedLookup()
|
||||
c.cache.Set(cacheKey, incidents, time.Now().Add(c.ttl))
|
||||
return incidents
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/golibs/lrucache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseStatusPage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input []byte
|
||||
output *StatusPage
|
||||
fail bool
|
||||
}{
|
||||
{
|
||||
input: []byte(`<html>
|
||||
<head><title>504 Gateway Time-out</title></head>
|
||||
<body><center><h1>504 Gateway Time-out</h1></center></body>
|
||||
</html>`),
|
||||
output: nil,
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
input: []byte(`{
|
||||
"page": {
|
||||
"id": "yh6f0r4529hb",
|
||||
"name": "Cloudflare",
|
||||
"url": "https://www.cloudflarestatus.com",
|
||||
"time_zone": "Etc/UTC",
|
||||
"updated_at": "2019-01-10T20:11:38.750Z"
|
||||
},
|
||||
"incidents": [
|
||||
{
|
||||
"name": "Cloudflare API service issues",
|
||||
"status": "resolved",
|
||||
"created_at": "2018-09-17T19:29:21.132Z",
|
||||
"updated_at": "2018-09-18T07:45:41.313Z",
|
||||
"monitoring_at": "2018-09-17T21:35:06.492Z",
|
||||
"resolved_at": "2018-09-18T07:45:41.290Z",
|
||||
"shortlink": "http://stspg.io/7f079791e",
|
||||
"id": "q746ybtyb6q0",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"incident_updates": [
|
||||
{
|
||||
"status": "resolved",
|
||||
"body": "Cloudflare has resolved the issue and the service have resumed normal operation.",
|
||||
"created_at": "2018-09-18T07:45:41.290Z",
|
||||
"updated_at": "2018-09-18T07:45:41.290Z",
|
||||
"display_at": "2018-09-18T07:45:41.290Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": true,
|
||||
"tweet_id": null,
|
||||
"id": "zl5g2pl5zhfs",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "monitoring",
|
||||
"body": "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.",
|
||||
"created_at": "2018-09-17T21:35:06.492Z",
|
||||
"updated_at": "2018-09-17T21:35:06.492Z",
|
||||
"display_at": "2018-09-17T21:35:06.492Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "degraded_performance",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "0001sv3chdnx",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "We are continuing to investigate this issue.",
|
||||
"created_at": "2018-09-17T19:30:08.049Z",
|
||||
"updated_at": "2018-09-17T19:30:08.049Z",
|
||||
"display_at": "2018-09-17T19:30:08.049Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "degraded_performance"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "qdr164tfpq7m",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.",
|
||||
"created_at": "2018-09-17T19:29:21.201Z",
|
||||
"updated_at": "2018-09-17T19:29:21.201Z",
|
||||
"display_at": "2018-09-17T19:29:21.201Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "g4tb35rs9yw7",
|
||||
"name": "Cloudflare customer dashboard and APIs - Cloudflare APIs",
|
||||
"old_status": "operational",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "qzl2n0q8tskg",
|
||||
"incident_id": "q746ybtyb6q0",
|
||||
"custom_tweet": null
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"status": "operational",
|
||||
"name": "Cloudflare APIs",
|
||||
"created_at": "2014-10-09T03:32:07.158Z",
|
||||
"updated_at": "2019-01-01T22:58:30.846Z",
|
||||
"position": 2,
|
||||
"description": null,
|
||||
"showcase": false,
|
||||
"id": "g4tb35rs9yw7",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"group_id": "1km35smx8p41",
|
||||
"group": false,
|
||||
"only_show_if_degraded": false,
|
||||
"automation_email": "component+g4tb35rs9yw7@notifications.statuspage.io"
|
||||
}
|
||||
],
|
||||
"impact": "minor"
|
||||
},
|
||||
{
|
||||
"name": "Web Analytics Delays",
|
||||
"status": "resolved",
|
||||
"created_at": "2018-09-17T18:05:39.907Z",
|
||||
"updated_at": "2018-09-17T22:53:05.078Z",
|
||||
"monitoring_at": null,
|
||||
"resolved_at": "2018-09-17T22:53:05.057Z",
|
||||
"shortlink": "http://stspg.io/cb208928c",
|
||||
"id": "wqfk9mzs5qt1",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"incident_updates": [
|
||||
{
|
||||
"status": "resolved",
|
||||
"body": "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.",
|
||||
"created_at": "2018-09-17T22:53:05.057Z",
|
||||
"updated_at": "2018-09-17T22:53:05.057Z",
|
||||
"display_at": "2018-09-17T22:53:05.057Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "4c231tkdlpcl",
|
||||
"name": "Cloudflare customer dashboard and APIs - Analytics",
|
||||
"old_status": "degraded_performance",
|
||||
"new_status": "operational"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "93y1w00yqzk4",
|
||||
"incident_id": "wqfk9mzs5qt1",
|
||||
"custom_tweet": null
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"body": "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.",
|
||||
"created_at": "2018-09-17T18:05:40.033Z",
|
||||
"updated_at": "2018-09-17T18:05:40.033Z",
|
||||
"display_at": "2018-09-17T18:05:40.033Z",
|
||||
"affected_components": [
|
||||
{
|
||||
"code": "4c231tkdlpcl",
|
||||
"name": "Cloudflare customer dashboard and APIs - Analytics",
|
||||
"old_status": "operational",
|
||||
"new_status": "degraded_performance"
|
||||
}
|
||||
],
|
||||
"deliver_notifications": false,
|
||||
"tweet_id": null,
|
||||
"id": "362t6lv0vrpk",
|
||||
"incident_id": "wqfk9mzs5qt1",
|
||||
"custom_tweet": null
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"status": "operational",
|
||||
"name": "Analytics",
|
||||
"created_at": "2014-11-13T11:54:10.191Z",
|
||||
"updated_at": "2018-12-31T08:20:52.349Z",
|
||||
"position": 3,
|
||||
"description": "Customer data",
|
||||
"showcase": false,
|
||||
"id": "4c231tkdlpcl",
|
||||
"page_id": "yh6f0r4529hb",
|
||||
"group_id": "1km35smx8p41",
|
||||
"group": false,
|
||||
"only_show_if_degraded": false,
|
||||
"automation_email": "component+4c231tkdlpcl@notifications.statuspage.io"
|
||||
}
|
||||
],
|
||||
"impact": "minor"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
output: &StatusPage{
|
||||
Incidents: []Incident{
|
||||
Incident{
|
||||
Name: "Cloudflare API service issues",
|
||||
ID: "q746ybtyb6q0",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has resolved the issue and the service have resumed normal operation.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has implemented a fix for this issue and is currently monitoring the results.\r\n\r\nWe will update the status once the issue is resolved.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "We are continuing to investigate this issue.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare is investigating issues with APIs and Page Rule delays for Page Rule updates. Cloudflare Page Rule service delivery is unaffected and is operating normally. Also, these issues do not affect the Cloudflare CDN and therefore, do not impact customer websites.",
|
||||
},
|
||||
},
|
||||
},
|
||||
Incident{
|
||||
Name: "Web Analytics Delays",
|
||||
ID: "wqfk9mzs5qt1",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{
|
||||
Body: "Cloudflare has resolved the issue and Web Analytics have resumed normal operation.",
|
||||
},
|
||||
IncidentUpdate{
|
||||
Body: "There is a delay in processing Cloudflare Web Analytics. This affects timely delivery of customer data.\n\nThese delays do not impact analytics for DNS and Rate Limiting.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
output, err := parseStatusPage(testCase.input)
|
||||
if testCase.fail {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, testCase.output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArgoTunnelIncident(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input Incident
|
||||
output bool
|
||||
}{
|
||||
{
|
||||
input: Incident{},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "An Argo Tunnel incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an argo tunnel incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an aRgO TuNnEl incident"},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "an argotunnel incident"},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{Name: "irrelevant"},
|
||||
output: false,
|
||||
},
|
||||
{
|
||||
input: Incident{
|
||||
Name: "irrelevant",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "an Argo Tunnel incident"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
},
|
||||
},
|
||||
output: true,
|
||||
},
|
||||
{
|
||||
input: Incident{
|
||||
Name: "an Argo Tunnel incident",
|
||||
Updates: []IncidentUpdate{
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
IncidentUpdate{Body: "irrelevant"},
|
||||
},
|
||||
},
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
actual := isArgoTunnelIncident(testCase.input)
|
||||
assert.Equal(t, testCase.output, actual, "Test case failed: %v", testCase.input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentURL(t *testing.T) {
|
||||
incident := Incident{
|
||||
ID: "s6k0dnn5347b",
|
||||
}
|
||||
assert.Equal(t, "https://www.cloudflarestatus.com/incidents/s6k0dnn5347b", incident.URL())
|
||||
}
|
||||
|
||||
func TestNewCachedIncidentLookup(t *testing.T) {
|
||||
c := newCachedIncidentLookup(func() []Incident { return nil })
|
||||
assert.Equal(t, time.Minute, c.ttl)
|
||||
assert.Equal(t, 1, c.cache.Capacity())
|
||||
}
|
||||
|
||||
func TestCachedIncidentLookup(t *testing.T) {
|
||||
expected := []Incident{
|
||||
Incident{
|
||||
Name: "An incident",
|
||||
ID: "incidentID",
|
||||
},
|
||||
}
|
||||
|
||||
var shouldCallUncachedLookup bool
|
||||
c := &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: 50 * time.Millisecond,
|
||||
uncachedLookup: func() []Incident {
|
||||
if !shouldCallUncachedLookup {
|
||||
t.Fatal("uncachedLookup shouldn't have been called")
|
||||
}
|
||||
return expected
|
||||
},
|
||||
}
|
||||
|
||||
shouldCallUncachedLookup = true
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
|
||||
shouldCallUncachedLookup = false
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
shouldCallUncachedLookup = true
|
||||
assert.Equal(t, expected, c.ActiveIncidents())
|
||||
}
|
||||
|
||||
func TestCachedIncidentLookupDoesntPanic(t *testing.T) {
|
||||
expected := []Incident{
|
||||
Incident{
|
||||
Name: "An incident",
|
||||
ID: "incidentID",
|
||||
},
|
||||
}
|
||||
c := &cachedIncidentLookup{
|
||||
cache: lrucache.NewLRUCache(1),
|
||||
ttl: 50 * time.Millisecond,
|
||||
uncachedLookup: func() []Incident { return expected },
|
||||
}
|
||||
c.cache.Set(cacheKey, 42, time.Now().Add(30*time.Minute))
|
||||
actual := c.ActiveIncidents()
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used to discover HA Warp servers
|
||||
srvService = "warp"
|
||||
srvProto = "tcp"
|
||||
srvName = "cloudflarewarp.com"
|
||||
)
|
||||
|
||||
func ResolveEdgeIPs(addresses []string) ([]*net.TCPAddr, error) {
|
||||
if len(addresses) > 0 {
|
||||
var tcpAddrs []*net.TCPAddr
|
||||
for _, address := range addresses {
|
||||
// Addresses specified (for testing, usually)
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpAddrs = append(tcpAddrs, tcpAddr)
|
||||
}
|
||||
return tcpAddrs, nil
|
||||
}
|
||||
// HA service discovery lookup
|
||||
_, addrs, err := net.LookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var lookupErr error
|
||||
for _, addr := range addrs {
|
||||
ips, err := ResolveSRVToTCP(addr)
|
||||
if err != nil || len(ips) == 0 {
|
||||
// don't return early, we might be able to resolve other addresses
|
||||
lookupErr = err
|
||||
continue
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
}
|
||||
ips := FlattenServiceIPs(resolvedIPsPerCNAME)
|
||||
if lookupErr == nil && len(ips) == 0 {
|
||||
return nil, fmt.Errorf("Unknown service discovery error")
|
||||
}
|
||||
return ips, lookupErr
|
||||
}
|
||||
|
||||
func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
ips, err := net.LookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// FlattenServiceIPs transposes and flattens the input slices such that the
|
||||
// first element of the n inner slices are the first n elements of the result.
|
||||
func FlattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr {
|
||||
var result []*net.TCPAddr
|
||||
for len(ipsByService) > 0 {
|
||||
filtered := ipsByService[:0]
|
||||
for _, ips := range ipsByService {
|
||||
if len(ips) == 0 {
|
||||
// sanity check
|
||||
continue
|
||||
}
|
||||
result = append(result, ips[0])
|
||||
if len(ips) > 1 {
|
||||
filtered = append(filtered, ips[1:])
|
||||
}
|
||||
}
|
||||
ipsByService = filtered
|
||||
}
|
||||
return result
|
||||
}
|
||||
+48
-3
@@ -51,7 +51,13 @@ type TunnelMetrics struct {
|
||||
// oldServerLocations stores the last server the tunnel was connected to
|
||||
oldServerLocations map[string]string
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
regSuccess prometheus.Counter
|
||||
regFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
tunnelsHA tunnelsForHA
|
||||
userHostnamesCounts *prometheus.CounterVec
|
||||
}
|
||||
|
||||
func newMuxerMetrics() *muxerMetrics {
|
||||
@@ -341,6 +347,40 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
)
|
||||
prometheus.MustRegister(serverLocations)
|
||||
|
||||
rpcFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_rpc_fail",
|
||||
Help: "Count of RPC connection errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
prometheus.MustRegister(rpcFail)
|
||||
|
||||
registerFail := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_register_fail",
|
||||
Help: "Count of tunnel registration errors by type",
|
||||
},
|
||||
[]string{"error"},
|
||||
)
|
||||
prometheus.MustRegister(registerFail)
|
||||
|
||||
userHostnamesCounts := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "user_hostnames_counts",
|
||||
Help: "Which user hostnames cloudflared is serving",
|
||||
},
|
||||
[]string{"userHostname"},
|
||||
)
|
||||
prometheus.MustRegister(userHostnamesCounts)
|
||||
|
||||
registerSuccess := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "tunnel_register_success",
|
||||
Help: "Count of successful tunnel registrations",
|
||||
})
|
||||
prometheus.MustRegister(registerSuccess)
|
||||
|
||||
return &TunnelMetrics{
|
||||
haConnections: haConnections,
|
||||
totalRequests: totalRequests,
|
||||
@@ -355,6 +395,11 @@ func NewTunnelMetrics() *TunnelMetrics {
|
||||
serverLocations: serverLocations,
|
||||
oldServerLocations: make(map[string]string),
|
||||
muxerMetrics: newMuxerMetrics(),
|
||||
tunnelsHA: NewTunnelsForHA(),
|
||||
regSuccess: registerSuccess,
|
||||
regFail: registerFail,
|
||||
rpcFail: rpcFail,
|
||||
userHostnamesCounts: userHostnamesCounts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +420,7 @@ func (t *TunnelMetrics) incrementRequests(connectionID string) {
|
||||
var concurrentRequests uint64
|
||||
var ok bool
|
||||
if concurrentRequests, ok = t.concurrentRequests[connectionID]; ok {
|
||||
t.concurrentRequests[connectionID] += 1
|
||||
t.concurrentRequests[connectionID]++
|
||||
concurrentRequests++
|
||||
} else {
|
||||
t.concurrentRequests[connectionID] = 1
|
||||
@@ -395,7 +440,7 @@ func (t *TunnelMetrics) incrementRequests(connectionID string) {
|
||||
func (t *TunnelMetrics) decrementConcurrentRequests(connectionID string) {
|
||||
t.concurrentRequestsLock.Lock()
|
||||
if _, ok := t.concurrentRequests[connectionID]; ok {
|
||||
t.concurrentRequests[connectionID] -= 1
|
||||
t.concurrentRequests[connectionID]--
|
||||
}
|
||||
t.concurrentRequestsLock.Unlock()
|
||||
|
||||
|
||||
+25
-20
@@ -1,11 +1,15 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,9 +54,9 @@ func NewSupervisor(config *TunnelConfig) *Supervisor {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) error {
|
||||
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
logger := s.config.Logger
|
||||
if err := s.initialize(ctx, connectedSignal); err != nil {
|
||||
if err := s.initialize(ctx, connectedSignal, u); err != nil {
|
||||
return err
|
||||
}
|
||||
var tunnelsWaiting []int
|
||||
@@ -95,7 +99,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) err
|
||||
case <-backoffTimer:
|
||||
backoffTimer = nil
|
||||
for _, index := range tunnelsWaiting {
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index))
|
||||
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), u)
|
||||
}
|
||||
tunnelsActive += len(tunnelsWaiting)
|
||||
tunnelsWaiting = nil
|
||||
@@ -119,9 +123,9 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal chan struct{}) err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct{}) error {
|
||||
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error {
|
||||
logger := s.config.Logger
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.EdgeAddrs)
|
||||
edgeIPs, err := connection.ResolveEdgeIPs(logger, s.config.EdgeAddrs)
|
||||
if err != nil {
|
||||
logger.Infof("ResolveEdgeIPs err")
|
||||
return err
|
||||
@@ -134,7 +138,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
s.lastResolve = time.Now()
|
||||
// check entitlement and version too old error before attempting to register more tunnels
|
||||
s.nextUnusedEdgeIP = s.config.HAConnections
|
||||
go s.startFirstTunnel(ctx, connectedSignal)
|
||||
go s.startFirstTunnel(ctx, connectedSignal, u)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-s.tunnelErrors
|
||||
@@ -142,11 +146,12 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
return fmt.Errorf("context was canceled")
|
||||
case tunnelError := <-s.tunnelErrors:
|
||||
return tunnelError.err
|
||||
case <-connectedSignal:
|
||||
case <-connectedSignal.Wait():
|
||||
}
|
||||
// At least one successful connection, so start the rest
|
||||
for i := 1; i < s.config.HAConnections; i++ {
|
||||
go s.startTunnel(ctx, i, make(chan struct{}))
|
||||
ch := signal.New(make(chan struct{}))
|
||||
go s.startTunnel(ctx, i, ch, u)
|
||||
time.Sleep(registrationInterval)
|
||||
}
|
||||
return nil
|
||||
@@ -154,8 +159,8 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal chan struct
|
||||
|
||||
// startTunnel starts the first tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors. It will send a signal via connectedSignal if registration succeed
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal chan struct{}) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal)
|
||||
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
defer func() {
|
||||
s.tunnelErrors <- tunnelError{index: 0, err: err}
|
||||
}()
|
||||
@@ -176,23 +181,23 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal chan
|
||||
default:
|
||||
return
|
||||
}
|
||||
err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal)
|
||||
err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u)
|
||||
}
|
||||
}
|
||||
|
||||
// startTunnel starts a new tunnel connection. The resulting error will be sent on
|
||||
// s.tunnelErrors.
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal chan struct{}) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal)
|
||||
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, u uuid.UUID) {
|
||||
err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal, u)
|
||||
s.tunnelErrors <- tunnelError{index: index, err: err}
|
||||
}
|
||||
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) chan struct{} {
|
||||
signal := make(chan struct{})
|
||||
s.tunnelsConnecting[index] = signal
|
||||
s.nextConnectedSignal = signal
|
||||
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
|
||||
sig := make(chan struct{})
|
||||
s.tunnelsConnecting[index] = sig
|
||||
s.nextConnectedSignal = sig
|
||||
s.nextConnectedIndex = index
|
||||
return signal
|
||||
return signal.New(sig)
|
||||
}
|
||||
|
||||
func (s *Supervisor) waitForNextTunnel(index int) bool {
|
||||
@@ -219,7 +224,7 @@ func (s *Supervisor) refreshEdgeIPs() {
|
||||
}
|
||||
s.resolverC = make(chan resolveResult)
|
||||
go func() {
|
||||
edgeIPs, err := ResolveEdgeIPs(s.config.EdgeAddrs)
|
||||
edgeIPs, err := connection.ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs)
|
||||
s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
+253
-128
@@ -2,67 +2,76 @@ package origin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
raven "github.com/getsentry/raven-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
_ "github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
rpc "zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 15 * time.Second
|
||||
openStreamTimeout = 30 * time.Second
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
TagHeaderNamePrefix = "Cf-Warp-Tag-"
|
||||
DuplicateConnectionError = "EDUPCONN"
|
||||
)
|
||||
|
||||
type TunnelConfig struct {
|
||||
EdgeAddrs []string
|
||||
OriginUrl string
|
||||
Hostname string
|
||||
OriginCert []byte
|
||||
TlsConfig *tls.Config
|
||||
ClientTlsConfig *tls.Config
|
||||
Retries uint
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
ClientID string
|
||||
BuildInfo *BuildInfo
|
||||
ReportedVersion string
|
||||
LBPool string
|
||||
Tags []tunnelpogs.Tag
|
||||
HAConnections int
|
||||
HTTPTransport http.RoundTripper
|
||||
Metrics *TunnelMetrics
|
||||
MetricsUpdateFreq time.Duration
|
||||
ProtocolLogger *log.Logger
|
||||
Logger *log.Logger
|
||||
IsAutoupdated bool
|
||||
GracePeriod time.Duration
|
||||
RunFromTerminal bool
|
||||
NoChunkedEncoding bool
|
||||
WSGI bool
|
||||
CompressionQuality uint64
|
||||
BuildInfo *BuildInfo
|
||||
ClientID string
|
||||
ClientTlsConfig *tls.Config
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
CompressionQuality uint64
|
||||
EdgeAddrs []string
|
||||
GracePeriod time.Duration
|
||||
HAConnections int
|
||||
HTTPTransport http.RoundTripper
|
||||
HeartbeatInterval time.Duration
|
||||
Hostname string
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
LBPool string
|
||||
Logger *log.Logger
|
||||
MaxHeartbeats uint64
|
||||
Metrics *TunnelMetrics
|
||||
MetricsUpdateFreq time.Duration
|
||||
NoChunkedEncoding bool
|
||||
OriginCert []byte
|
||||
ReportedVersion string
|
||||
Retries uint
|
||||
RunFromTerminal bool
|
||||
Tags []tunnelpogs.Tag
|
||||
TlsConfig *tls.Config
|
||||
TransportLogger *log.Logger
|
||||
UseDeclarativeTunnel bool
|
||||
WSGI bool
|
||||
// OriginUrl may not be used if a user specifies a unix socket.
|
||||
OriginUrl string
|
||||
}
|
||||
|
||||
type dialError struct {
|
||||
@@ -100,6 +109,11 @@ type clientRegisterTunnelError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec) clientRegisterTunnelError {
|
||||
counter.WithLabelValues(cause.Error()).Inc()
|
||||
return clientRegisterTunnelError{cause: cause}
|
||||
}
|
||||
|
||||
func (e clientRegisterTunnelError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
@@ -125,21 +139,43 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
}
|
||||
}
|
||||
|
||||
func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal chan struct{}) error {
|
||||
func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal *signal.Signal) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
<-shutdownC
|
||||
cancel()
|
||||
}()
|
||||
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a user specified negative HAConnections, we will treat it as requesting 1 connection
|
||||
if config.HAConnections > 1 {
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal)
|
||||
if config.UseDeclarativeTunnel {
|
||||
return connection.NewSupervisor(&connection.CloudflaredConfig{
|
||||
ConnectionConfig: &connection.ConnectionConfig{
|
||||
TLSConfig: config.TlsConfig,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.Logger.WithField("subsystem", "connection_supervisor"),
|
||||
},
|
||||
OriginCert: config.OriginCert,
|
||||
Tags: config.Tags,
|
||||
EdgeAddrs: config.EdgeAddrs,
|
||||
HAConnections: uint(config.HAConnections),
|
||||
Logger: config.Logger,
|
||||
CloudflaredVersion: config.ReportedVersion,
|
||||
}).Run(ctx)
|
||||
}
|
||||
return NewSupervisor(config).Run(ctx, connectedSignal, u)
|
||||
} else {
|
||||
addrs, err := ResolveEdgeIPs(config.EdgeAddrs)
|
||||
addrs, err := connection.ResolveEdgeIPs(config.Logger, config.EdgeAddrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal)
|
||||
return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal, u)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,23 +183,23 @@ func ServeTunnelLoop(ctx context.Context,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
connectionID uint8,
|
||||
connectedSignal chan struct{},
|
||||
connectedSignal *signal.Signal,
|
||||
u uuid.UUID,
|
||||
) error {
|
||||
logger := config.Logger
|
||||
config.Metrics.incrementHaConnections()
|
||||
defer config.Metrics.decrementHaConnections()
|
||||
backoff := BackoffHandler{MaxRetries: config.Retries}
|
||||
// Used to close connectedSignal no more than once
|
||||
connectedFuse := h2mux.NewBooleanFuse()
|
||||
go func() {
|
||||
if connectedFuse.Await() {
|
||||
close(connectedSignal)
|
||||
connectedSignal.Notify()
|
||||
}
|
||||
}()
|
||||
// Ensure the above goroutine will terminate if we return without connecting
|
||||
defer connectedFuse.Fuse(false)
|
||||
for {
|
||||
err, recoverable := ServeTunnel(ctx, config, addr, connectionID, connectedFuse, &backoff)
|
||||
err, recoverable := ServeTunnel(ctx, config, addr, connectionID, connectedFuse, &backoff, u)
|
||||
if recoverable {
|
||||
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
|
||||
logger.Infof("Retrying in %s seconds", duration)
|
||||
@@ -182,6 +218,7 @@ func ServeTunnel(
|
||||
connectionID uint8,
|
||||
connectedFuse *h2mux.BooleanFuse,
|
||||
backoff *BackoffHandler,
|
||||
u uuid.UUID,
|
||||
) (err error, recoverable bool) {
|
||||
// Treat panics as recoverable errors
|
||||
defer func() {
|
||||
@@ -221,7 +258,7 @@ func ServeTunnel(
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := RegisterTunnel(serveCtx, handler.muxer, config, connectionID, originLocalIP)
|
||||
err := RegisterTunnel(serveCtx, handler.muxer, config, connectionID, originLocalIP, u)
|
||||
if err == nil {
|
||||
connectedFuse.Fuse(true)
|
||||
backoff.SetGracePeriod()
|
||||
@@ -235,7 +272,7 @@ func ServeTunnel(
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
// UnregisterTunnel blocks until the RPC call returns
|
||||
err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.Logger)
|
||||
err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
|
||||
handler.muxer.Shutdown()
|
||||
return err
|
||||
case <-updateMetricsTickC:
|
||||
@@ -265,6 +302,9 @@ func ServeTunnel(
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error from server side")
|
||||
// Don't send registration error return from server to Sentry. They are
|
||||
// logged on server side
|
||||
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
|
||||
logger.Error(activeIncidentsMsg(incidents))
|
||||
}
|
||||
return castedErr.cause, !castedErr.permanent
|
||||
case clientRegisterTunnelError:
|
||||
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
|
||||
@@ -292,24 +332,27 @@ func IsRPCStreamResponse(headers []h2mux.Header) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfig, connectionID uint8, originLocalIP string) error {
|
||||
config.Logger.Debug("initiating RPC stream to register")
|
||||
stream, err := muxer.OpenStream([]h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
func RegisterTunnel(
|
||||
ctx context.Context,
|
||||
muxer *h2mux.Muxer,
|
||||
config *TunnelConfig,
|
||||
connectionID uint8,
|
||||
originLocalIP string,
|
||||
uuid uuid.UUID,
|
||||
) error {
|
||||
config.TransportLogger.Debug("initiating RPC stream to register")
|
||||
stream, err := openStream(ctx, muxer)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
}
|
||||
if !IsRPCStreamResponse(stream.Headers) {
|
||||
// stream response error
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
|
||||
}
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(config.Logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(config.Logger.WithField("subsystem", "rpc-transport")),
|
||||
tunnelrpc.NewTransportLogger(config.TransportLogger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(config.TransportLogger.WithField("subsystem", "rpc-transport")),
|
||||
)
|
||||
defer conn.Close()
|
||||
ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
|
||||
@@ -318,10 +361,6 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi
|
||||
serverInfoPromise := tsClient.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
return nil
|
||||
})
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
}
|
||||
registration, err := ts.RegisterTunnel(
|
||||
ctx,
|
||||
config.OriginCert,
|
||||
@@ -331,22 +370,19 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi
|
||||
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, config.Logger)
|
||||
if err != nil {
|
||||
// RegisterTunnel RPC failure
|
||||
return clientRegisterTunnelError{cause: err}
|
||||
return newClientRegisterTunnelError(err, config.Metrics.regFail)
|
||||
}
|
||||
for _, logLine := range registration.LogLines {
|
||||
config.Logger.Info(logLine)
|
||||
}
|
||||
if registration.Err == DuplicateConnectionError {
|
||||
return dupConnRegisterTunnelError{}
|
||||
} else if registration.Err != "" {
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", registration.Err),
|
||||
permanent: registration.PermanentFailure,
|
||||
}
|
||||
|
||||
if regErr := processRegisterTunnelError(registration.Err, registration.PermanentFailure, config.Metrics); regErr != nil {
|
||||
return regErr
|
||||
}
|
||||
|
||||
if registration.TunnelID != "" {
|
||||
config.Logger.Info("Tunnel ID: " + registration.TunnelID)
|
||||
config.Metrics.tunnelsHA.AddTunnelID(connectionID, registration.TunnelID)
|
||||
config.Logger.Infof("Each HA connection's tunnel IDs: %v", config.Metrics.tunnelsHA.String())
|
||||
}
|
||||
|
||||
// Print out the user's trial zone URL in a nice box (if they requested and got one)
|
||||
@@ -361,17 +397,32 @@ func RegisterTunnel(ctx context.Context, muxer *h2mux.Muxer, config *TunnelConfi
|
||||
}
|
||||
}
|
||||
|
||||
config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
config.Logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
return nil
|
||||
}
|
||||
|
||||
func processRegisterTunnelError(err string, permanentFailure bool, metrics *TunnelMetrics) error {
|
||||
if err == "" {
|
||||
metrics.regSuccess.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
metrics.regFail.WithLabelValues(err).Inc()
|
||||
if err == DuplicateConnectionError {
|
||||
return dupConnRegisterTunnelError{}
|
||||
}
|
||||
return serverRegisterTunnelError{
|
||||
cause: fmt.Errorf("Server error: %s", err),
|
||||
permanent: permanentFailure,
|
||||
}
|
||||
}
|
||||
|
||||
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
|
||||
logger.Debug("initiating RPC stream to unregister")
|
||||
stream, err := muxer.OpenStream([]h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
ctx := context.Background()
|
||||
stream, err := openStream(ctx, muxer)
|
||||
if err != nil {
|
||||
// RPC stream open error
|
||||
return err
|
||||
@@ -380,7 +431,6 @@ func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log
|
||||
// stream response error
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn := rpc.NewConn(
|
||||
tunnelrpc.NewTransportLogger(logger.WithField("subsystem", "rpc-unregister"), rpc.StreamTransport(stream)),
|
||||
tunnelrpc.ConnLog(logger.WithField("subsystem", "rpc-transport")),
|
||||
@@ -391,6 +441,16 @@ func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log
|
||||
return ts.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
|
||||
}
|
||||
|
||||
func openStream(ctx context.Context, muxer *h2mux.Muxer) (*h2mux.MuxedStream, error) {
|
||||
openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer cancel()
|
||||
return muxer.OpenStream(openStreamCtx, []h2mux.Header{
|
||||
{Name: ":method", Value: "RPC"},
|
||||
{Name: ":scheme", Value: "capnp"},
|
||||
{Name: ":path", Value: "*"},
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func LogServerInfo(
|
||||
promise tunnelrpc.ServerInfo_Promise,
|
||||
connectionID uint8,
|
||||
@@ -431,6 +491,12 @@ func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
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)
|
||||
}
|
||||
@@ -514,7 +580,7 @@ func NewTunnelHandler(ctx context.Context,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: config.HeartbeatInterval,
|
||||
MaxHeartbeats: config.MaxHeartbeats,
|
||||
Logger: config.ProtocolLogger.WithFields(log.Fields{}),
|
||||
Logger: config.TransportLogger.WithFields(log.Fields{}),
|
||||
CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -531,65 +597,93 @@ func (h *TunnelHandler) AppendTagHeaders(r *http.Request) {
|
||||
|
||||
func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
h.metrics.incrementRequests(h.connectionID)
|
||||
req, err := http.NewRequest("GET", h.originUrl, h2mux.MuxedStreamReader{MuxedStream: stream})
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Panic("Unexpected error from http.NewRequest")
|
||||
defer h.metrics.decrementConcurrentRequests(h.connectionID)
|
||||
|
||||
req, reqErr := h.createRequest(stream)
|
||||
if reqErr != nil {
|
||||
h.logError(stream, reqErr)
|
||||
return reqErr
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
h.logger.WithError(err).Error("invalid request received")
|
||||
}
|
||||
h.AppendTagHeaders(req)
|
||||
|
||||
cfRay := FindCfRayHeader(req)
|
||||
lbProbe := isLBProbeRequest(req)
|
||||
h.logRequest(req, cfRay, lbProbe)
|
||||
|
||||
var resp *http.Response
|
||||
var respErr error
|
||||
if websocket.IsWebSocketUpgrade(req) {
|
||||
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
|
||||
if err != nil {
|
||||
h.logError(stream, err)
|
||||
} else {
|
||||
stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
defer conn.Close()
|
||||
// Copy to/from stream to the undelying connection. Use the underlying
|
||||
// connection because cloudflared doesn't operate on the message themselves
|
||||
websocket.Stream(conn.UnderlyingConn(), stream)
|
||||
h.metrics.incrementResponses(h.connectionID, "200")
|
||||
h.logResponse(response, cfRay, lbProbe)
|
||||
}
|
||||
resp, respErr = h.serveWebsocket(stream, req)
|
||||
} else {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if h.noChunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
req.ContentLength = int64(cLength)
|
||||
}
|
||||
}
|
||||
resp, respErr = h.serveHTTP(stream, req)
|
||||
}
|
||||
if respErr != nil {
|
||||
h.logError(stream, respErr)
|
||||
return respErr
|
||||
}
|
||||
h.logResponseOk(resp, cfRay, lbProbe)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
func (h *TunnelHandler) createRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
|
||||
req, err := http.NewRequest("GET", h.originUrl, h2mux.MuxedStreamReader{MuxedStream: stream})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
h.AppendTagHeaders(req)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
response, err := h.httpClient.RoundTrip(req)
|
||||
func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
// Copy to/from stream to the undelying connection. Use the underlying
|
||||
// connection because cloudflared doesn't operate on the message themselves
|
||||
websocket.Stream(conn.UnderlyingConn(), stream)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
h.logError(stream, err)
|
||||
} else {
|
||||
defer response.Body.Close()
|
||||
stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
if h.isEventStream(response) {
|
||||
h.writeEventStream(stream, response.Body)
|
||||
} else {
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
|
||||
}
|
||||
|
||||
h.metrics.incrementResponses(h.connectionID, "200")
|
||||
h.logResponse(response, cfRay, lbProbe)
|
||||
func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if h.noChunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
req.ContentLength = int64(cLength)
|
||||
}
|
||||
}
|
||||
h.metrics.decrementConcurrentRequests(h.connectionID)
|
||||
return nil
|
||||
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
response, err := h.httpClient.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error proxying request to origin")
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
err = stream.WriteHeaders(H1ResponseToH2Response(response))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
if h.isEventStream(response) {
|
||||
h.writeEventStream(stream, response.Body)
|
||||
} else {
|
||||
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
|
||||
// compression generates dictionary on first write
|
||||
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) writeEventStream(stream *h2mux.MuxedStream, responseBody io.ReadCloser) {
|
||||
@@ -619,25 +713,42 @@ func (h *TunnelHandler) logError(stream *h2mux.MuxedStream, err error) {
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool) {
|
||||
logger := log.NewEntry(h.logger)
|
||||
if cfRay != "" {
|
||||
h.logger.WithField("CF-RAY", cfRay).Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else if lbProbe {
|
||||
h.logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
|
||||
} else {
|
||||
h.logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
|
||||
logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
|
||||
}
|
||||
logger.Debugf("Request Headers %+v", req.Header)
|
||||
|
||||
if contentLen := req.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Request Content length unknown")
|
||||
} else {
|
||||
logger.Debugf("Request content length %d", contentLen)
|
||||
}
|
||||
h.logger.Debugf("Request Headers %+v", req.Header)
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) logResponse(r *http.Response, cfRay string, lbProbe bool) {
|
||||
func (h *TunnelHandler) logResponseOk(r *http.Response, cfRay string, lbProbe bool) {
|
||||
h.metrics.incrementResponses(h.connectionID, "200")
|
||||
logger := log.NewEntry(h.logger)
|
||||
if cfRay != "" {
|
||||
h.logger.WithField("CF-RAY", cfRay).Debugf("%s", r.Status)
|
||||
logger = logger.WithField("CF-RAY", cfRay)
|
||||
logger.Debugf("%s", r.Status)
|
||||
} else if lbProbe {
|
||||
h.logger.Debugf("Response to Load Balancer health check %s", r.Status)
|
||||
logger.Debugf("Response to Load Balancer health check %s", r.Status)
|
||||
} else {
|
||||
h.logger.Infof("%s", r.Status)
|
||||
logger.Infof("%s", r.Status)
|
||||
}
|
||||
logger.Debugf("Response Headers %+v", r.Header)
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
logger.Debugf("Response content length unknown")
|
||||
} else {
|
||||
logger.Debugf("Response content length %d", contentLen)
|
||||
}
|
||||
h.logger.Debugf("Response Headers %+v", r.Header)
|
||||
}
|
||||
|
||||
func (h *TunnelHandler) UpdateMetrics(connectionID string) {
|
||||
@@ -683,3 +794,17 @@ func trialZoneMsg(url string) []string {
|
||||
" " + url,
|
||||
}
|
||||
}
|
||||
|
||||
func activeIncidentsMsg(incidents []Incident) string {
|
||||
preamble := "There is an active Cloudflare incident that may be related:"
|
||||
if len(incidents) > 1 {
|
||||
preamble = "There are active Cloudflare incidents that may be related:"
|
||||
}
|
||||
incidentStrings := []string{}
|
||||
for _, incident := range incidents {
|
||||
incidentString := fmt.Sprintf("%s (%s)", incident.Name, incident.URL())
|
||||
incidentStrings = append(incidentStrings, incidentString)
|
||||
}
|
||||
return preamble + " " + strings.Join(incidentStrings, "; ")
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve.
|
||||
type tunnelsForHA struct {
|
||||
sync.Mutex
|
||||
metrics *prometheus.GaugeVec
|
||||
entries map[uint8]string
|
||||
}
|
||||
|
||||
// NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA.
|
||||
func NewTunnelsForHA() tunnelsForHA {
|
||||
metrics := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "tunnel_ids",
|
||||
Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.",
|
||||
},
|
||||
[]string{"tunnel_id", "ha_conn_id"},
|
||||
)
|
||||
prometheus.MustRegister(metrics)
|
||||
|
||||
return tunnelsForHA{
|
||||
metrics: metrics,
|
||||
entries: make(map[uint8]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics.
|
||||
func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
haStr := fmt.Sprintf("%v", haConn)
|
||||
if oldTunnelID, ok := t.entries[haConn]; ok {
|
||||
t.metrics.WithLabelValues(oldTunnelID, haStr).Dec()
|
||||
}
|
||||
t.entries[haConn] = tunnelID
|
||||
t.metrics.WithLabelValues(tunnelID, haStr).Inc()
|
||||
}
|
||||
|
||||
func (t *tunnelsForHA) String() string {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
return fmt.Sprintf("%v", t.entries)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Signal lets goroutines signal that some event has occurred. Other goroutines can wait for the signal.
|
||||
type Signal struct {
|
||||
ch chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// New wraps a channel and turns it into a signal for a one-time event.
|
||||
func New(ch chan struct{}) *Signal {
|
||||
return &Signal{
|
||||
ch: ch,
|
||||
once: sync.Once{},
|
||||
}
|
||||
}
|
||||
|
||||
// Notify alerts any goroutines waiting on this signal that the event has occurred.
|
||||
// After the first call to Notify(), future calls are no-op.
|
||||
func (s *Signal) Notify() {
|
||||
s.once.Do(func() {
|
||||
close(s.ch)
|
||||
})
|
||||
}
|
||||
|
||||
// Wait returns a channel which will be written to when Notify() is called for the first time.
|
||||
// This channel will never be written to a second time.
|
||||
func (s *Signal) Wait() <-chan struct{} {
|
||||
return s.ch
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiNotifyDoesntCrash(t *testing.T) {
|
||||
sig := New(make(chan struct{}))
|
||||
sig.Notify()
|
||||
sig.Notify()
|
||||
// If code has reached here without crashing, the test has passed.
|
||||
}
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
sig := New(make(chan struct{}))
|
||||
sig.Notify()
|
||||
select {
|
||||
case <-sig.Wait():
|
||||
// Test succeeds
|
||||
return
|
||||
default:
|
||||
// sig.Wait() should have been read from, because sig.Notify() wrote to it.
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package sshgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
signEndpoint = "/cdn-cgi/access/cert_sign"
|
||||
keyName = "cf_key"
|
||||
)
|
||||
|
||||
// signPayload represents the request body sent to the sign handler API
|
||||
type signPayload struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
JWT string `json:"jwt"`
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
// signResponse represents the response body from the sign handler API
|
||||
type signResponse struct {
|
||||
KeyID string `json:"id"`
|
||||
Certificate string `json:"certificate"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// ErrorResponse struct stores error information after any error-prone function
|
||||
type errorResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var mockRequest func(url, contentType string, body io.Reader) (*http.Response, error) = nil
|
||||
|
||||
// GenerateShortLivedCertificate generates and stores a keypair for short lived certs
|
||||
func GenerateShortLivedCertificate(appURL *url.URL, token string) error {
|
||||
fullName, err := cfpath.GenerateFilePathFromURL(appURL, keyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := handleCertificateGeneration(token, fullName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := fullName + "-cert.pub"
|
||||
if err := writeKey(name, []byte(cert)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCertificateGeneration takes a JWT and uses it build a signPayload
|
||||
// to send to the Sign endpoint with the public key from the keypair it generated
|
||||
func handleCertificateGeneration(token, fullName string) (string, error) {
|
||||
if token == "" {
|
||||
return "", errors.New("invalid token")
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
issuer, _, err := claims.StringClaim("iss")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pub, err := generateKeyPair(fullName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(&signPayload{
|
||||
PublicKey: string(pub),
|
||||
JWT: token,
|
||||
Issuer: issuer,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res *http.Response
|
||||
if mockRequest != nil {
|
||||
res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
} else {
|
||||
res, err = http.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
var errResponse errorResponse
|
||||
if err := decoder.Decode(&errResponse); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("%d: %s", errResponse.Status, errResponse.Message)
|
||||
}
|
||||
|
||||
var signRes signResponse
|
||||
if err := decoder.Decode(&signRes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signRes.Certificate, err
|
||||
}
|
||||
|
||||
// generateKeyPair creates a EC keypair (P256) and stores them in the homedir.
|
||||
// returns the generated public key from the successful keypair generation
|
||||
func generateKeyPair(fullName string) ([]byte, error) {
|
||||
pubKeyName := fullName + ".pub"
|
||||
|
||||
exist, err := config.FileExists(pubKeyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
return ioutil.ReadFile(pubKeyName)
|
||||
}
|
||||
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := writeKey(fullName, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: parsed,
|
||||
})); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pub, err := gossh.NewPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := gossh.MarshalAuthorizedKey(pub)
|
||||
|
||||
if err := writeKey(pubKeyName, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// writeKey will write a key to disk in DER format (it's a standard pem key)
|
||||
func writeKey(filename string, data []byte) error {
|
||||
filepath, err := homedir.Expand(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath, data, 0600)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package sshgen
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/cmd/cloudflared/path"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
audTest = "cf-test-aud"
|
||||
nonceTest = "asfd"
|
||||
)
|
||||
|
||||
type signingArguments struct {
|
||||
Principals []string `json:"principals"`
|
||||
ClientPubKey string `json:"public_key"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
func TestCertGenSuccess(t *testing.T) {
|
||||
url, _ := url.Parse("https://cf-test-access.com/testpath")
|
||||
token := tokenGenerator()
|
||||
|
||||
fullName, err := cfpath.GenerateFilePathFromURL(url, keyName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pubKeyName := fullName + ".pub"
|
||||
certKeyName := fullName + "-cert.pub"
|
||||
|
||||
defer func() {
|
||||
os.Remove(fullName)
|
||||
os.Remove(pubKeyName)
|
||||
os.Remove(certKeyName)
|
||||
}()
|
||||
|
||||
resp := signingArguments{
|
||||
Principals: []string{"dalton"},
|
||||
ClientPubKey: "ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg+0rYq4mNGIAHiH1xPOJXfmOpTEwFIcyXzGJieTOhRs8AAAAIbmlzdHAyNTYAAABBBJIcsq02e8ZaofJXOZKp7yQdKW/JIouJ90lybr76hHIRrZBL1t4JEimfLvNDphPrTW9VDQaIcBSKNaxRqHOS8jezoJbhFGWhqQAAAAEAAAAgZWU5OTliNGRkZmFmNjgxNDEwMTVhMDJiY2ZhMTdiN2UAAAAKAAAABmF1c3RpbgAAAABc1KFoAAAAAFzUohwAAAAAAAAARwAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEEeAuYR56XaxvH5Z1p0hDCTQ7wC4dbj0Gc+LOKu1f94og2ilZTv9tutg8cZrqAT97REmGH6j9KIOVLGsPVjajSKAAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAORY9ZO3TQsrUm6ajnVW+arbnVfTkxYBYFlVoeOEXKZuAAAAIG96A8nQnTuprWXLSemWL68RXC1NVKnBOIPD2Z7UIOB1",
|
||||
Duration: "3m",
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
respJson, err := json.Marshal(resp)
|
||||
assert.NoError(t, err)
|
||||
w.Write(respJson)
|
||||
mockRequest = func(url, contentType string, body io.Reader) (*http.Response, error) {
|
||||
assert.Contains(t, "/cdn-cgi/access/cert_sign", url)
|
||||
assert.Equal(t, "application/json", contentType)
|
||||
buf, err := ioutil.ReadAll(body)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, buf)
|
||||
return w.Result(), nil
|
||||
}
|
||||
|
||||
err = GenerateShortLivedCertificate(url, token)
|
||||
assert.NoError(t, err)
|
||||
|
||||
exist, err := config.FileExists(fullName)
|
||||
assert.NoError(t, err)
|
||||
if !exist {
|
||||
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", fullName), fullName)
|
||||
return
|
||||
}
|
||||
|
||||
exist, err = config.FileExists(pubKeyName)
|
||||
assert.NoError(t, err)
|
||||
if !exist {
|
||||
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", pubKeyName), pubKeyName)
|
||||
return
|
||||
}
|
||||
|
||||
exist, err = config.FileExists(certKeyName)
|
||||
assert.NoError(t, err)
|
||||
if !exist {
|
||||
assert.FailNow(t, fmt.Sprintf("key should exist at: %s", certKeyName), certKeyName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func tokenGenerator() string {
|
||||
iat := time.Now().Unix()
|
||||
exp := time.Now().Add(time.Minute * 5).Unix()
|
||||
claims := jose.Claims{}
|
||||
claims.Add("aud", audTest)
|
||||
claims.Add("iat", iat)
|
||||
claims.Add("nonce", nonceTest)
|
||||
claims.Add("exp", exp)
|
||||
|
||||
k, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
signer := jose.NewSignerRSA("asdf", *k)
|
||||
token, terr := jose.NewSignedJWT(claims, signer)
|
||||
if terr != nil {
|
||||
return ""
|
||||
}
|
||||
return token.Encode()
|
||||
}
|
||||
+10
-19
@@ -2,14 +2,10 @@ package tlsconfig
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
tunnellog "github.com/cloudflare/cloudflared/log"
|
||||
"github.com/getsentry/raven-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
)
|
||||
|
||||
// CertReloader can load and reload a TLS certificate from a particular filepath.
|
||||
@@ -21,18 +17,14 @@ type CertReloader struct {
|
||||
keyPath string
|
||||
}
|
||||
|
||||
// NewCertReloader makes a CertReloader, memorizing the filepaths in the context/flags.
|
||||
func NewCertReloader(c *cli.Context, f CLIFlags) (*CertReloader, error) {
|
||||
if !c.IsSet(f.Cert) {
|
||||
return nil, errors.New("CertReloader: cert not provided")
|
||||
}
|
||||
if !c.IsSet(f.Key) {
|
||||
return nil, errors.New("CertReloader: key not provided")
|
||||
}
|
||||
// NewCertReloader makes a CertReloader. It loads the cert during initialization to make sure certPath and keyPath are valid
|
||||
func NewCertReloader(certPath, keyPath string) (*CertReloader, error) {
|
||||
cr := new(CertReloader)
|
||||
cr.certPath = c.String(f.Cert)
|
||||
cr.keyPath = c.String(f.Key)
|
||||
cr.LoadCert()
|
||||
cr.certPath = certPath
|
||||
cr.keyPath = keyPath
|
||||
if err := cr.LoadCert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
@@ -45,18 +37,17 @@ func (cr *CertReloader) Cert(clientHello *tls.ClientHelloInfo) (*tls.Certificate
|
||||
|
||||
// LoadCert loads a TLS certificate from the CertReloader's specified filepath.
|
||||
// Call this after writing a new certificate to the disk (e.g. after renewing a certificate)
|
||||
func (cr *CertReloader) LoadCert() {
|
||||
func (cr *CertReloader) LoadCert() error {
|
||||
cr.Lock()
|
||||
defer cr.Unlock()
|
||||
|
||||
log.SetFormatter(&tunnellog.JSONFormatter{})
|
||||
log.Info("Reloading certificate")
|
||||
cert, err := tls.LoadX509KeyPair(cr.certPath, cr.keyPath)
|
||||
|
||||
// Keep the old certificate if there's a problem reading the new one.
|
||||
if err != nil {
|
||||
raven.CaptureError(fmt.Errorf("Error parsing X509 key pair: %v", err), nil)
|
||||
return
|
||||
return err
|
||||
}
|
||||
cr.certificate = &cert
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package tlsconfig
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
)
|
||||
|
||||
// TODO: remove the Origin CA root certs when migrated to Authenticated Origin Pull certs
|
||||
@@ -85,11 +86,26 @@ QzMmZpRpIBB321ZBlcnlxiTJvWxvbCPHKHj20VwwAz7LONF59s84ZsOqfoBv8gKM
|
||||
s0s5dsq5zpLeaw==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
func GetCloudflareRootCA() *x509.CertPool {
|
||||
ca := x509.NewCertPool()
|
||||
if !ca.AppendCertsFromPEM([]byte(cloudflareRootCA)) {
|
||||
// should never happen
|
||||
panic("failure loading Cloudflare origin CA pem")
|
||||
func GetCloudflareRootCA() ([]*x509.Certificate, error) {
|
||||
var certs []*x509.Certificate
|
||||
pemBlocks := cloudflareRootCA
|
||||
for len(pemBlocks) > 0 {
|
||||
var block *pem.Block
|
||||
block, pemBlocks = pem.Decode(pemBlocks)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return ca
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICBjCCAbCgAwIBAgIJAPKk4bYMrSFMMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
|
||||
BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRkwFwYDVQQK
|
||||
DBBDbG91ZGZsYXJlLCBJbmMuMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgxMTE1
|
||||
MjA1NzU3WhcNMjgxMTEyMjA1NzU3WjBdMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF
|
||||
VGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xvdWRmbGFyZSwgSW5j
|
||||
LjESMBAGA1UEAwwJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOQN
|
||||
pTRn5wLf8SSI5x2kpDvbdDy7lfhamJ2En4Q+wy1cSKp8bn8/oyhVF7QTsimDGTI4
|
||||
45pV9nDfNJPYB3IW0x0CAwEAAaNTMFEwHQYDVR0OBBYEFE4jIa97mIEiYFa02X++
|
||||
uu5mCEn+MB8GA1UdIwQYMBaAFE4jIa97mIEiYFa02X++uu5mCEn+MA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQAkE+pDee0o5cNcZRUszy8sTQzB1Wlp
|
||||
J6ucfmo16crqRaK7uGvhkMyibIc4D8z2Cxw3aI3IMMFoIIlYoYKiUcbd
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICBjCCAbCgAwIBAgIJAN6cXRTbJtFnMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
|
||||
BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRgwFgYDVQQK
|
||||
DA9DbG91ZGZsYXJlLCBJbmMxEzARBgNVBAMMCmxvY2FsaG9zdDIwHhcNMTgxMTE1
|
||||
MjExMTU4WhcNMjgxMTEyMjExMTU4WjBdMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF
|
||||
VGV4YXMxDzANBgNVBAcMBkF1c3RpbjEYMBYGA1UECgwPQ2xvdWRmbGFyZSwgSW5j
|
||||
MRMwEQYDVQQDDApsb2NhbGhvc3QyMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQx
|
||||
IMZ6QgXoul2ITF/7sly4fW2Ol+a/AYw42zCWhVqOXv8AhY21I0Q8lkRR6wOroQwZ
|
||||
O7jKKOcE5TnR/NRcZr8CAwEAAaNTMFEwHQYDVR0OBBYEFONKxLZc2RUD0KTHkAz4
|
||||
8nrb5688MB8GA1UdIwQYMBaAFONKxLZc2RUD0KTHkAz48nrb5688MA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQA56pwhvGpNPjyLcWfJHu/vI3ZjdoLB
|
||||
LnrkRaMjJmv0H0Beh4upJhoz8u6lhMACerKQrrdQhEPB2u+maFrEBtmN
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,10 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA5A2lNGfnAt/xJIjn
|
||||
HaSkO9t0PLuV+FqYnYSfhD7DLVxIqnxufz+jKFUXtBOyKYMZMjjjmlX2cN80k9gH
|
||||
chbTHQIDAQABAkAoeDtu91lJa1AxuZG58vOqI6GW/Xr5naojmdts7m5YaAhDa7DE
|
||||
zJUp4d8SP5cGBf1/PB3x6Cu9UviFNQ16wmzJAiEA8gUm4UYpWZD4Ze2l/xb+BK8D
|
||||
IglSUIy1VxW+X1G55wMCIQDxOfXiFzPqnv/e5avKGv6CU11Dhmbi1OpiyybZTjGz
|
||||
XwIhAM3bE/cJdqJ4bNBGE6umIupY8pFA3IMnLBempwbsvPOBAiEAgzJ+5OSxu92W
|
||||
VGidsmJUIhWtF9i1hJFAmVLcYjwBFAkCICtjP/vv0qOZWk4mAAn2zz9UVWp45DSR
|
||||
p/FA8V77ohXD
|
||||
-----END PRIVATE KEY-----
|
||||
+58
-122
@@ -6,150 +6,86 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
|
||||
"github.com/cloudflare/cloudflared/log"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var logger = log.CreateLogger()
|
||||
|
||||
// CLIFlags names the flags used to configure TLS for a command or subsystem.
|
||||
// The nil value for a field means the flag is ignored.
|
||||
type CLIFlags struct {
|
||||
Cert string
|
||||
Key string
|
||||
ClientCert string
|
||||
RootCA string
|
||||
// Config is the user provided parameters to create a tls.Config
|
||||
type TLSParameters struct {
|
||||
Cert string
|
||||
Key string
|
||||
GetCertificate *CertReloader
|
||||
ClientCAs []string
|
||||
RootCAs []string
|
||||
ServerName string
|
||||
CurvePreferences []tls.CurveID
|
||||
}
|
||||
|
||||
// GetConfig returns a TLS configuration according to the flags defined in f and
|
||||
// set by the user.
|
||||
func (f CLIFlags) GetConfig(c *cli.Context) *tls.Config {
|
||||
config := &tls.Config{}
|
||||
|
||||
if c.IsSet(f.Cert) && c.IsSet(f.Key) {
|
||||
cert, err := tls.LoadX509KeyPair(c.String(f.Cert), c.String(f.Key))
|
||||
// GetConfig returns a TLS configuration according to the Config set by the user.
|
||||
func GetConfig(p *TLSParameters) (*tls.Config, error) {
|
||||
tlsconfig := &tls.Config{}
|
||||
if p.Cert != "" && p.Key != "" {
|
||||
cert, err := tls.LoadX509KeyPair(p.Cert, p.Key)
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Error parsing X509 key pair")
|
||||
return nil, errors.Wrap(err, "Error parsing X509 key pair")
|
||||
}
|
||||
config.Certificates = []tls.Certificate{cert}
|
||||
config.BuildNameToCertificate()
|
||||
tlsconfig.Certificates = []tls.Certificate{cert}
|
||||
// BuildNameToCertificate parses Certificates and builds NameToCertificate from common name
|
||||
// and SAN fields of leaf certificates
|
||||
tlsconfig.BuildNameToCertificate()
|
||||
}
|
||||
return f.finishGettingConfig(c, config)
|
||||
}
|
||||
|
||||
func (f CLIFlags) GetConfigReloadableCert(c *cli.Context, cr *CertReloader) *tls.Config {
|
||||
config := &tls.Config{
|
||||
GetCertificate: cr.Cert,
|
||||
if p.GetCertificate != nil {
|
||||
// GetCertificate is called when client supplies SNI info or Certificates is empty.
|
||||
// Order of retrieving certificate is GetCertificate, NameToCertificate and lastly first element of Certificates
|
||||
tlsconfig.GetCertificate = p.GetCertificate.Cert
|
||||
}
|
||||
config.BuildNameToCertificate()
|
||||
return f.finishGettingConfig(c, config)
|
||||
}
|
||||
|
||||
func (f CLIFlags) finishGettingConfig(c *cli.Context, config *tls.Config) *tls.Config {
|
||||
if c.IsSet(f.ClientCert) {
|
||||
if len(p.ClientCAs) > 0 {
|
||||
// set of root certificate authorities that servers use if required to verify a client certificate
|
||||
// by the policy in ClientAuth
|
||||
config.ClientCAs = LoadCert(c.String(f.ClientCert))
|
||||
clientCAs, err := LoadCert(p.ClientCAs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error loading client CAs")
|
||||
}
|
||||
tlsconfig.ClientCAs = clientCAs
|
||||
// server's policy for TLS Client Authentication. Default is no client cert
|
||||
config.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
tlsconfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
// set of root certificate authorities that clients use when verifying server certificates
|
||||
if c.IsSet(f.RootCA) {
|
||||
config.RootCAs = LoadCert(c.String(f.RootCA))
|
||||
|
||||
if len(p.RootCAs) > 0 {
|
||||
rootCAs, err := LoadCert(p.RootCAs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error loading root CAs")
|
||||
}
|
||||
tlsconfig.RootCAs = rootCAs
|
||||
}
|
||||
// we optimize CurveP256
|
||||
config.CurvePreferences = []tls.CurveID{tls.CurveP256}
|
||||
return config
|
||||
|
||||
if p.ServerName != "" {
|
||||
tlsconfig.ServerName = p.ServerName
|
||||
}
|
||||
|
||||
if len(p.CurvePreferences) > 0 {
|
||||
tlsconfig.CurvePreferences = p.CurvePreferences
|
||||
} else {
|
||||
// Cloudflare optimize CurveP256
|
||||
tlsconfig.CurvePreferences = []tls.CurveID{tls.CurveP256}
|
||||
}
|
||||
|
||||
return tlsconfig, nil
|
||||
}
|
||||
|
||||
// LoadCert creates a CertPool containing all certificates in a PEM-format file.
|
||||
func LoadCert(certPath string) *x509.CertPool {
|
||||
caCert, err := ioutil.ReadFile(certPath)
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatalf("Error reading certificate %s", certPath)
|
||||
}
|
||||
func LoadCert(certPaths []string) (*x509.CertPool, error) {
|
||||
ca := x509.NewCertPool()
|
||||
if !ca.AppendCertsFromPEM(caCert) {
|
||||
logger.WithError(err).Fatalf("Error parsing certificate %s", certPath)
|
||||
}
|
||||
return ca
|
||||
}
|
||||
|
||||
func LoadGlobalCertPool() (*x509.CertPool, error) {
|
||||
success := false
|
||||
|
||||
// First, obtain the system certificate pool
|
||||
certPool, systemCertPoolErr := x509.SystemCertPool()
|
||||
if systemCertPoolErr != nil {
|
||||
if runtime.GOOS != "windows" {
|
||||
logger.Warnf("error obtaining the system certificates: %s", systemCertPoolErr)
|
||||
for _, certPath := range certPaths {
|
||||
caCert, err := ioutil.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Error reading certificate %s", certPath)
|
||||
}
|
||||
certPool = x509.NewCertPool()
|
||||
} else {
|
||||
success = true
|
||||
}
|
||||
|
||||
// Next, append the Cloudflare CA pool into the system pool
|
||||
if !certPool.AppendCertsFromPEM(cloudflareRootCA) {
|
||||
logger.Warn("could not append the CF certificate to the cloudflared certificate pool")
|
||||
} else {
|
||||
success = true
|
||||
}
|
||||
|
||||
if success != true { // Obtaining any of the CAs has failed; this is a fatal error
|
||||
return nil, errors.New("error loading any of the CAs into the global certificate pool")
|
||||
}
|
||||
|
||||
// Finally, add the Hello certificate into the pool (since it's self-signed)
|
||||
helloCertificate, err := GetHelloCertificateX509()
|
||||
if err != nil {
|
||||
logger.Warn("error obtaining the Hello server certificate")
|
||||
}
|
||||
|
||||
certPool.AddCert(helloCertificate)
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func LoadOriginCertPool(originCAPoolPEM []byte) (*x509.CertPool, error) {
|
||||
success := false
|
||||
|
||||
// Get the global pool
|
||||
certPool, globalPoolErr := LoadGlobalCertPool()
|
||||
if globalPoolErr != nil {
|
||||
certPool = x509.NewCertPool()
|
||||
} else {
|
||||
success = true
|
||||
}
|
||||
|
||||
// Then, add any custom origin CA pool the user may have passed
|
||||
if originCAPoolPEM != nil {
|
||||
if !certPool.AppendCertsFromPEM(originCAPoolPEM) {
|
||||
logger.Warn("could not append the provided origin CA to the cloudflared certificate pool")
|
||||
} else {
|
||||
success = true
|
||||
if !ca.AppendCertsFromPEM(caCert) {
|
||||
return nil, errors.Wrapf(err, "Error parsing certificate %s", certPath)
|
||||
}
|
||||
}
|
||||
|
||||
if success != true {
|
||||
return nil, errors.New("error loading any of the CAs into the origin certificate pool")
|
||||
}
|
||||
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
func CreateTunnelConfig(c *cli.Context, addrs []string) *tls.Config {
|
||||
tlsConfig := CLIFlags{RootCA: "cacert"}.GetConfig(c)
|
||||
if tlsConfig.RootCAs == nil {
|
||||
tlsConfig.RootCAs = GetCloudflareRootCA()
|
||||
tlsConfig.ServerName = "cftunnel.com"
|
||||
} else if len(addrs) > 0 {
|
||||
// Set for development environments and for testing specific origintunneld instances
|
||||
tlsConfig.ServerName, _, _ = net.SplitHostPort(addrs[0])
|
||||
}
|
||||
return tlsConfig
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
+58
-188
@@ -1,214 +1,84 @@
|
||||
// +build ignore
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tlsconfig
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"os"
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650`
|
||||
var samplePEM = []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQCb/H0EUrdXEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBPbmUwHhcNMTgwNDI2MTYxMDUxWhcNMjgwNDIzMTYxMDUxWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBPbmUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAwVQD
|
||||
K0SJ25UFLznm2pU3zhzMEvpDEofHVNnCjk4mlDrtVop7PkKZ8pDEmuQANltUrxC8
|
||||
yHBE2wXMv+GlH+bDtwIDAQABMA0GCSqGSIb3DQEBCwUAA0EAjVYQzozIFPkt/HRY
|
||||
uUoZ8zEHIDICb0syFf5VAjm9AgTwIPzUmD+c5vl6LWDnxq7L45nLCzhhQ6YmiwDz
|
||||
X7Wcyg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB4DCCAYoCCQDZfCdAJ+mwzDANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJV
|
||||
UzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcGA1UECgwQQ2xv
|
||||
dWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVneTERMA8GA1UE
|
||||
AwwIVGVzdCBUd28wHhcNMTgwNDI2MTYxMTIwWhcNMjgwNDIzMTYxMTIwWjB3MQsw
|
||||
CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxDzANBgNVBAcMBkF1c3RpbjEZMBcG
|
||||
A1UECgwQQ2xvdWRmbGFyZSwgSW5jLjEZMBcGA1UECwwQUHJvZHVjdCBTdHJhdGVn
|
||||
eTERMA8GA1UEAwwIVGVzdCBUd28wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAoHKp
|
||||
ROVK3zCSsH7ocYeyRAML4V7SFAbZcb4WIwDnE08oMBVRkQVcW5tqEkvG3RiClfzV
|
||||
wZIJ3CfqKIeSNSDU9wIDAQABMA0GCSqGSIb3DQEBCwUAA0EAJw2gUbnPiq4C2p5b
|
||||
iWzlA9Q7aKo+VQ4H7IZS7tTccr59nVjvH/TG3eWujpnocr4TOqW9M3CK1DF9mUGP
|
||||
3pQ3Jg==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
// testcert.pem and testcert2.pem are Generated using `openssl req -newkey rsa:512 -nodes -x509 -days 3650`
|
||||
const (
|
||||
testcertCommonName = "localhost"
|
||||
)
|
||||
|
||||
var systemCertPoolSubjects []*pkix.Name
|
||||
func TestGetFromEmptyConfig(t *testing.T) {
|
||||
c := &TLSParameters{}
|
||||
|
||||
type certificateFixture struct {
|
||||
ou string
|
||||
cn string
|
||||
tlsConfig, err := GetConfig(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, tlsConfig.Certificates)
|
||||
|
||||
assert.Empty(t, tlsConfig.NameToCertificate)
|
||||
|
||||
assert.Nil(t, tlsConfig.ClientCAs)
|
||||
assert.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth)
|
||||
|
||||
assert.Nil(t, tlsConfig.RootCAs)
|
||||
|
||||
assert.Len(t, tlsConfig.CurvePreferences, 1)
|
||||
assert.Equal(t, tls.CurveP256, tlsConfig.CurvePreferences[0])
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
systemCertPool, err := x509.SystemCertPool()
|
||||
if isUnrecoverableError(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
func TestGetConfig(t *testing.T) {
|
||||
cert, err := tls.LoadX509KeyPair("testcert.pem", "testkey.pem")
|
||||
assert.NoError(t, err)
|
||||
|
||||
if systemCertPool == nil {
|
||||
// On Windows, let's just assume the system cert pool was empty
|
||||
systemCertPool = x509.NewCertPool()
|
||||
c := &TLSParameters{
|
||||
Cert: "testcert.pem",
|
||||
Key: "testkey.pem",
|
||||
ClientCAs: []string{"testcert.pem", "testcert2.pem"},
|
||||
RootCAs: []string{"testcert.pem", "testcert2.pem"},
|
||||
ServerName: "test",
|
||||
CurvePreferences: []tls.CurveID{tls.CurveP384},
|
||||
}
|
||||
tlsConfig, err := GetConfig(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tlsConfig.Certificates, 1)
|
||||
assert.Equal(t, cert, tlsConfig.Certificates[0])
|
||||
|
||||
systemCertPoolSubjects, err = getCertPoolSubjects(systemCertPool)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
assert.Equal(t, cert, *tlsConfig.NameToCertificate[testcertCommonName])
|
||||
|
||||
os.Exit(m.Run())
|
||||
assert.NotNil(t, tlsConfig.ClientCAs)
|
||||
assert.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth)
|
||||
|
||||
assert.NotNil(t, tlsConfig.RootCAs)
|
||||
|
||||
assert.Len(t, tlsConfig.CurvePreferences, 1)
|
||||
assert.Equal(t, tls.CurveP384, tlsConfig.CurvePreferences[0])
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolJustSystemPool(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
func TestCertReloader(t *testing.T) {
|
||||
expectedCert, err := tls.LoadX509KeyPair("testcert.pem", "testkey.pem")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Remove extra subjects from the cert pool
|
||||
var filteredSystemCertPoolSubjects []*pkix.Name
|
||||
certReloader, err := NewCertReloader("testcert.pem", "testkey.pem")
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Log(extraSubjects)
|
||||
chi := &tls.ClientHelloInfo{ServerName: testcertCommonName}
|
||||
cert, err := certReloader.Cert(chi)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedCert, *cert)
|
||||
|
||||
OUTER:
|
||||
for _, subject := range certPoolSubjects {
|
||||
for _, extraSubject := range extraSubjects {
|
||||
if subject == extraSubject {
|
||||
t.Log(extraSubject)
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
filteredSystemCertPoolSubjects = append(filteredSystemCertPoolSubjects, subject)
|
||||
c := &TLSParameters{
|
||||
GetCertificate: certReloader,
|
||||
}
|
||||
tlsConfig, err := GetConfig(c)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, len(filteredSystemCertPoolSubjects), len(systemCertPoolSubjects))
|
||||
|
||||
difference := subjectSubtract(systemCertPoolSubjects, filteredSystemCertPoolSubjects)
|
||||
assert.Equal(t, 0, len(difference))
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolCFCertificates(t *testing.T) {
|
||||
certPoolSubjects := loadCertPoolSubjects(t, nil)
|
||||
|
||||
extraSubjects := subjectSubtract(systemCertPoolSubjects, certPoolSubjects)
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{ou: "CloudFlare Origin SSL ECC Certificate Authority"},
|
||||
{ou: "CloudFlare Origin SSL Certificate Authority"},
|
||||
{cn: "origin-pull.cloudflare.net"},
|
||||
{cn: "Argo Tunnel Sample Hello Server Certificate"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, extraSubjects)
|
||||
}
|
||||
|
||||
func TestLoadOriginCertPoolWithExtraPEMs(t *testing.T) {
|
||||
certPoolWithoutPEMSubjects := loadCertPoolSubjects(t, nil)
|
||||
certPoolWithPEMSubjects := loadCertPoolSubjects(t, samplePEM)
|
||||
|
||||
difference := subjectSubtract(certPoolWithoutPEMSubjects, certPoolWithPEMSubjects)
|
||||
|
||||
assert.Equal(t, 2, len(difference))
|
||||
|
||||
expected := []*certificateFixture{
|
||||
{cn: "Test One"},
|
||||
{cn: "Test Two"},
|
||||
}
|
||||
|
||||
assertFixturesMatchSubjects(t, expected, difference)
|
||||
}
|
||||
|
||||
func loadCertPoolSubjects(t *testing.T, originCAPoolPEM []byte) []*pkix.Name {
|
||||
certPool, err := LoadOriginCertPool(originCAPoolPEM)
|
||||
if isUnrecoverableError(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NotEmpty(t, certPool.Subjects())
|
||||
certPoolSubjects, err := getCertPoolSubjects(certPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return certPoolSubjects
|
||||
}
|
||||
|
||||
func assertFixturesMatchSubjects(t *testing.T, fixtures []*certificateFixture, subjects []*pkix.Name) {
|
||||
assert.Equal(t, len(fixtures), len(subjects))
|
||||
|
||||
for _, fixture := range fixtures {
|
||||
found := false
|
||||
for _, subject := range subjects {
|
||||
found = found || fixtureMatchesSubjectPredicate(fixture, subject)
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fixtureMatchesSubjectPredicate(fixture *certificateFixture, subject *pkix.Name) bool {
|
||||
cnMatch := true
|
||||
if fixture.cn != "" {
|
||||
cnMatch = fixture.cn == subject.CommonName
|
||||
}
|
||||
|
||||
ouMatch := true
|
||||
if fixture.ou != "" {
|
||||
ouMatch = len(subject.OrganizationalUnit) > 0 && fixture.ou == subject.OrganizationalUnit[0]
|
||||
}
|
||||
|
||||
return cnMatch && ouMatch
|
||||
}
|
||||
|
||||
func subjectSubtract(left []*pkix.Name, right []*pkix.Name) []*pkix.Name {
|
||||
var difference []*pkix.Name
|
||||
|
||||
var found bool
|
||||
for _, r := range right {
|
||||
found = false
|
||||
for _, l := range left {
|
||||
if (*l).String() == (*r).String() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
difference = append(difference, r)
|
||||
}
|
||||
}
|
||||
|
||||
return difference
|
||||
}
|
||||
|
||||
func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
|
||||
var subjects []*pkix.Name
|
||||
|
||||
for _, subject := range certPool.Subjects() {
|
||||
var sequence pkix.RDNSequence
|
||||
_, err := asn1.Unmarshal(subject, &sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := pkix.Name{}
|
||||
name.FillFromRDNSequence(&sequence)
|
||||
|
||||
subjects = append(subjects, &name)
|
||||
}
|
||||
|
||||
return subjects, nil
|
||||
}
|
||||
|
||||
func isUnrecoverableError(err error) bool {
|
||||
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
|
||||
cert, err = tlsConfig.GetCertificate(chi)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedCert, *cert)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Upstream is a simplified interface for proxy destination
|
||||
|
||||
@@ -2,6 +2,7 @@ package tunneldns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewUpstreamHTTPS(endpoint string) (Upstream, error) {
|
||||
TLSClientConfig: tls,
|
||||
DisableCompression: true,
|
||||
MaxIdleConns: 1,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
http2.ConfigureTransport(transport)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package tunneldns
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metrics/vars"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
@@ -8,7 +10,6 @@ import (
|
||||
"github.com/coredns/coredns/request"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// MetricsPlugin is an adapter for CoreDNS and built-in metrics
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
package tunnelrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/trace"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
@@ -3,9 +3,9 @@ package tunnelrpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
"zombiezen.com/go/capnproto2/encoding/text"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
rpccapnp "zombiezen.com/go/capnproto2/std/capnp/rpc"
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
)
|
||||
|
||||
///
|
||||
/// Structs
|
||||
///
|
||||
|
||||
type CloudflaredConfig struct {
|
||||
Timestamp time.Time
|
||||
AutoUpdateFrequency time.Duration
|
||||
MetricsUpdateFrequency time.Duration
|
||||
HeartbeatInterval time.Duration
|
||||
MaxFailedHeartbeats uint64
|
||||
GracePeriod time.Duration
|
||||
DoHProxyConfigs []*DoHProxyConfig
|
||||
ReverseProxyConfigs []*ReverseProxyConfig
|
||||
}
|
||||
|
||||
type UseConfigurationResult struct {
|
||||
Success bool
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
type DoHProxyConfig struct {
|
||||
ListenHost string
|
||||
ListenPort uint16
|
||||
Upstreams []string
|
||||
}
|
||||
|
||||
type ReverseProxyConfig struct {
|
||||
TunnelID string
|
||||
Origin OriginConfig
|
||||
Retries uint64
|
||||
ConnectionTimeout time.Duration
|
||||
ChunkedEncoding bool
|
||||
CompressionQuality uint64
|
||||
}
|
||||
|
||||
//go-sumtype:decl OriginConfig
|
||||
type OriginConfig interface {
|
||||
isOriginConfig()
|
||||
}
|
||||
|
||||
type HTTPOriginConfig struct {
|
||||
URL string `capnp:"url"`
|
||||
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"`
|
||||
DialDualStack bool
|
||||
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"`
|
||||
TLSVerify bool `capnp:"tlsVerify"`
|
||||
OriginCAPool string
|
||||
OriginServerName string
|
||||
MaxIdleConnections uint64
|
||||
IdleConnectionTimeout time.Duration
|
||||
}
|
||||
|
||||
func (_ *HTTPOriginConfig) isOriginConfig() {}
|
||||
|
||||
type UnixSocketOriginConfig struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (_ *UnixSocketOriginConfig) isOriginConfig() {}
|
||||
|
||||
type WebSocketOriginConfig struct {
|
||||
URL string `capnp:"url"`
|
||||
}
|
||||
|
||||
func (_ *WebSocketOriginConfig) isOriginConfig() {}
|
||||
|
||||
type HelloWorldOriginConfig struct{}
|
||||
|
||||
func (_ *HelloWorldOriginConfig) isOriginConfig() {}
|
||||
|
||||
///
|
||||
/// Boilerplate to convert between these structs and the primitive structs generated by capnp-go
|
||||
///
|
||||
|
||||
func MarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig, p *CloudflaredConfig) error {
|
||||
s.SetTimestamp(p.Timestamp.UnixNano())
|
||||
s.SetAutoUpdateFrequency(p.AutoUpdateFrequency.Nanoseconds())
|
||||
s.SetMetricsUpdateFrequency(p.MetricsUpdateFrequency.Nanoseconds())
|
||||
s.SetHeartbeatInterval(p.HeartbeatInterval.Nanoseconds())
|
||||
s.SetMaxFailedHeartbeats(p.MaxFailedHeartbeats)
|
||||
s.SetGracePeriod(p.GracePeriod.Nanoseconds())
|
||||
err := marshalDoHProxyConfigs(s, p.DoHProxyConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = marshalReverseProxyConfigs(s, p.ReverseProxyConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig, dohProxyConfigs []*DoHProxyConfig) error {
|
||||
capnpList, err := s.NewDohProxyConfigs(int32(len(dohProxyConfigs)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, unmarshalledConfig := range dohProxyConfigs {
|
||||
err := MarshalDoHProxyConfig(capnpList.At(i), unmarshalledConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig, reverseProxyConfigs []*ReverseProxyConfig) error {
|
||||
capnpList, err := s.NewReverseProxyConfigs(int32(len(reverseProxyConfigs)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, unmarshalledConfig := range reverseProxyConfigs {
|
||||
err := MarshalReverseProxyConfig(capnpList.At(i), unmarshalledConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig) (*CloudflaredConfig, error) {
|
||||
p := new(CloudflaredConfig)
|
||||
p.Timestamp = time.Unix(0, s.Timestamp()).UTC()
|
||||
p.AutoUpdateFrequency = time.Duration(s.AutoUpdateFrequency())
|
||||
p.MetricsUpdateFrequency = time.Duration(s.MetricsUpdateFrequency())
|
||||
p.HeartbeatInterval = time.Duration(s.HeartbeatInterval())
|
||||
p.MaxFailedHeartbeats = s.MaxFailedHeartbeats()
|
||||
p.GracePeriod = time.Duration(s.GracePeriod())
|
||||
dohProxyConfigs, err := unmarshalDoHProxyConfigs(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.DoHProxyConfigs = dohProxyConfigs
|
||||
reverseProxyConfigs, err := unmarshalReverseProxyConfigs(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ReverseProxyConfigs = reverseProxyConfigs
|
||||
return p, err
|
||||
}
|
||||
|
||||
func unmarshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*DoHProxyConfig, error) {
|
||||
var result []*DoHProxyConfig
|
||||
marshalledDoHProxyConfigs, err := s.DohProxyConfigs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < marshalledDoHProxyConfigs.Len(); i++ {
|
||||
ss := marshalledDoHProxyConfigs.At(i)
|
||||
dohProxyConfig, err := UnmarshalDoHProxyConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, dohProxyConfig)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func unmarshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*ReverseProxyConfig, error) {
|
||||
var result []*ReverseProxyConfig
|
||||
marshalledReverseProxyConfigs, err := s.ReverseProxyConfigs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < marshalledReverseProxyConfigs.Len(); i++ {
|
||||
ss := marshalledReverseProxyConfigs.At(i)
|
||||
reverseProxyConfig, err := UnmarshalReverseProxyConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, reverseProxyConfig)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func MarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult, p *UseConfigurationResult) error {
|
||||
return pogs.Insert(tunnelrpc.UseConfigurationResult_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult) (*UseConfigurationResult, error) {
|
||||
p := new(UseConfigurationResult)
|
||||
err := pogs.Extract(p, tunnelrpc.UseConfigurationResult_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig, p *DoHProxyConfig) error {
|
||||
return pogs.Insert(tunnelrpc.DoHProxyConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig) (*DoHProxyConfig, error) {
|
||||
p := new(DoHProxyConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.DoHProxyConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyConfig) error {
|
||||
s.SetTunnelID(p.TunnelID)
|
||||
switch config := p.Origin.(type) {
|
||||
case *HTTPOriginConfig:
|
||||
ss, err := s.Origin().NewHttp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalHTTPOriginConfig(ss, config)
|
||||
case *UnixSocketOriginConfig:
|
||||
ss, err := s.Origin().NewSocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalUnixSocketOriginConfig(ss, config)
|
||||
case *WebSocketOriginConfig:
|
||||
ss, err := s.Origin().NewWebsocket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalWebSocketOriginConfig(ss, config)
|
||||
case *HelloWorldOriginConfig:
|
||||
ss, err := s.Origin().NewHelloWorld()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MarshalHelloWorldOriginConfig(ss, config)
|
||||
default:
|
||||
return fmt.Errorf("Unknown type for config: %T", config)
|
||||
}
|
||||
s.SetRetries(p.Retries)
|
||||
s.SetConnectionTimeout(p.ConnectionTimeout.Nanoseconds())
|
||||
s.SetChunkedEncoding(p.ChunkedEncoding)
|
||||
s.SetCompressionQuality(p.CompressionQuality)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyConfig, error) {
|
||||
p := new(ReverseProxyConfig)
|
||||
tunnelID, err := s.TunnelID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.TunnelID = tunnelID
|
||||
switch s.Origin().Which() {
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_http:
|
||||
ss, err := s.Origin().Http()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := UnmarshalHTTPOriginConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_socket:
|
||||
ss, err := s.Origin().Socket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := UnmarshalUnixSocketOriginConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_websocket:
|
||||
ss, err := s.Origin().Websocket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := UnmarshalWebSocketOriginConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
case tunnelrpc.ReverseProxyConfig_origin_Which_helloWorld:
|
||||
ss, err := s.Origin().HelloWorld()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err := UnmarshalHelloWorldOriginConfig(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Origin = config
|
||||
}
|
||||
p.Retries = s.Retries()
|
||||
p.ConnectionTimeout = time.Duration(s.ConnectionTimeout())
|
||||
p.ChunkedEncoding = s.ChunkedEncoding()
|
||||
p.CompressionQuality = s.CompressionQuality()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func MarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig, p *HTTPOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.HTTPOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig, error) {
|
||||
p := new(HTTPOriginConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.HTTPOriginConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig, p *UnixSocketOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig) (*UnixSocketOriginConfig, error) {
|
||||
p := new(UnixSocketOriginConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig, p *WebSocketOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig) (*WebSocketOriginConfig, error) {
|
||||
p := new(WebSocketOriginConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func MarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig, p *HelloWorldOriginConfig) error {
|
||||
return pogs.Insert(tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig) (*HelloWorldOriginConfig, error) {
|
||||
p := new(HelloWorldOriginConfig)
|
||||
err := pogs.Extract(p, tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
type CloudflaredServer interface {
|
||||
UseConfiguration(ctx context.Context, config *CloudflaredConfig) (*CloudflaredConfig, error)
|
||||
GetConfiguration(ctx context.Context) (*CloudflaredConfig, error)
|
||||
}
|
||||
|
||||
type CloudflaredServer_PogsClient struct {
|
||||
Client capnp.Client
|
||||
Conn *rpc.Conn
|
||||
}
|
||||
|
||||
func (c *CloudflaredServer_PogsClient) Close() error {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *CloudflaredServer_PogsClient) UseConfiguration(
|
||||
ctx context.Context,
|
||||
config *CloudflaredConfig,
|
||||
) (*UseConfigurationResult, error) {
|
||||
client := tunnelrpc.CloudflaredServer{Client: c.Client}
|
||||
promise := client.UseConfiguration(ctx, func(p tunnelrpc.CloudflaredServer_useConfiguration_Params) error {
|
||||
cloudflaredConfig, err := p.NewCloudflaredConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalCloudflaredConfig(cloudflaredConfig, config)
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalUseConfigurationResult(retval)
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package pogs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
func TestCloudflaredConfig(t *testing.T) {
|
||||
addDoHProxyConfigs := func(c *CloudflaredConfig) {
|
||||
c.DoHProxyConfigs = []*DoHProxyConfig{
|
||||
sampleDoHProxyConfig(),
|
||||
}
|
||||
}
|
||||
addReverseProxyConfigs := func(c *CloudflaredConfig) {
|
||||
c.ReverseProxyConfigs = []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleHTTPOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleUnixSocketOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleWebSocketOriginConfig()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []*CloudflaredConfig{
|
||||
sampleCloudflaredConfig(),
|
||||
sampleCloudflaredConfig(addDoHProxyConfigs),
|
||||
sampleCloudflaredConfig(addReverseProxyConfigs),
|
||||
sampleCloudflaredConfig(addDoHProxyConfigs, addReverseProxyConfigs),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewCloudflaredConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalCloudflaredConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalCloudflaredConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseConfigurationResult(t *testing.T) {
|
||||
testCases := []*UseConfigurationResult{
|
||||
&UseConfigurationResult{
|
||||
Success: true,
|
||||
},
|
||||
&UseConfigurationResult{
|
||||
Success: false,
|
||||
ErrorMessage: "the quick brown fox jumped over the lazy dogs",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewUseConfigurationResult(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalUseConfigurationResult(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalUseConfigurationResult(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoHProxyConfig(t *testing.T) {
|
||||
testCases := []*DoHProxyConfig{
|
||||
sampleDoHProxyConfig(),
|
||||
sampleDoHProxyConfig(func(c *DoHProxyConfig) {
|
||||
c.Upstreams = nil
|
||||
}),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewDoHProxyConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalDoHProxyConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalDoHProxyConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseProxyConfig(t *testing.T) {
|
||||
testCases := []*ReverseProxyConfig{
|
||||
sampleReverseProxyConfig(),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleHTTPOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleUnixSocketOriginConfig()
|
||||
}),
|
||||
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
|
||||
c.Origin = sampleWebSocketOriginConfig()
|
||||
}),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewReverseProxyConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalReverseProxyConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalReverseProxyConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPOriginConfig(t *testing.T) {
|
||||
testCases := []*HTTPOriginConfig{
|
||||
sampleHTTPOriginConfig(),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewHTTPOriginConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalHTTPOriginConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalHTTPOriginConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixSocketOriginConfig(t *testing.T) {
|
||||
testCases := []*UnixSocketOriginConfig{
|
||||
sampleUnixSocketOriginConfig(),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewUnixSocketOriginConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalUnixSocketOriginConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalUnixSocketOriginConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSocketOriginConfig(t *testing.T) {
|
||||
testCases := []*WebSocketOriginConfig{
|
||||
sampleWebSocketOriginConfig(),
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
capnpEntity, err := tunnelrpc.NewWebSocketOriginConfig(seg)
|
||||
if !assert.NoError(t, err) {
|
||||
t.Fatal("Couldn't initialize a new message")
|
||||
}
|
||||
err = MarshalWebSocketOriginConfig(capnpEntity, testCase)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
|
||||
continue
|
||||
}
|
||||
result, err := UnmarshalWebSocketOriginConfig(capnpEntity)
|
||||
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Functions to generate sample data for ease of testing
|
||||
|
||||
func sampleCloudflaredConfig(overrides ...func(*CloudflaredConfig)) *CloudflaredConfig {
|
||||
// strip the location and monotonic clock reading so that assert.Equals()
|
||||
// will work correctly
|
||||
now := time.Now().UTC().Round(0)
|
||||
sample := &CloudflaredConfig{
|
||||
Timestamp: now,
|
||||
AutoUpdateFrequency: 21 * time.Hour,
|
||||
MetricsUpdateFrequency: 11 * time.Minute,
|
||||
HeartbeatInterval: 5 * time.Second,
|
||||
MaxFailedHeartbeats: 9001,
|
||||
GracePeriod: 31 * time.Second,
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleDoHProxyConfig(overrides ...func(*DoHProxyConfig)) *DoHProxyConfig {
|
||||
sample := &DoHProxyConfig{
|
||||
ListenHost: "127.0.0.1",
|
||||
ListenPort: 53,
|
||||
Upstreams: []string{"https://1.example.com", "https://2.example.com"},
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReverseProxyConfig {
|
||||
sample := &ReverseProxyConfig{
|
||||
TunnelID: "hijk",
|
||||
Origin: &HelloWorldOriginConfig{},
|
||||
Retries: 18,
|
||||
ConnectionTimeout: 5 * time.Second,
|
||||
ChunkedEncoding: false,
|
||||
CompressionQuality: 4,
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
|
||||
sample := &HTTPOriginConfig{
|
||||
URL: "https://example.com",
|
||||
TCPKeepAlive: 7 * time.Second,
|
||||
DialDualStack: true,
|
||||
TLSHandshakeTimeout: 11 * time.Second,
|
||||
TLSVerify: true,
|
||||
OriginCAPool: "/etc/cert.pem",
|
||||
OriginServerName: "secure.example.com",
|
||||
MaxIdleConnections: 19,
|
||||
IdleConnectionTimeout: 17 * time.Second,
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleUnixSocketOriginConfig(overrides ...func(*UnixSocketOriginConfig)) *UnixSocketOriginConfig {
|
||||
sample := &UnixSocketOriginConfig{
|
||||
Path: "/var/lib/file.sock",
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig {
|
||||
sample := &WebSocketOriginConfig{
|
||||
URL: "ssh://example.com",
|
||||
}
|
||||
for _, f := range overrides {
|
||||
f(sample)
|
||||
}
|
||||
return sample
|
||||
}
|
||||
+130
-1
@@ -2,11 +2,13 @@ package pogs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/google/uuid"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"zombiezen.com/go/capnproto2"
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
"zombiezen.com/go/capnproto2/rpc"
|
||||
"zombiezen.com/go/capnproto2/server"
|
||||
@@ -71,6 +73,41 @@ func UnmarshalRegistrationOptions(s tunnelrpc.RegistrationOptions) (*Registratio
|
||||
return p, err
|
||||
}
|
||||
|
||||
type ConnectResult struct {
|
||||
Err *ConnectError
|
||||
ServerInfo ServerInfo
|
||||
}
|
||||
|
||||
func MarshalConnectResult(s tunnelrpc.ConnectResult, p *ConnectResult) error {
|
||||
return pogs.Insert(tunnelrpc.ConnectResult_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalConnectResult(s tunnelrpc.ConnectResult) (*ConnectResult, error) {
|
||||
p := new(ConnectResult)
|
||||
err := pogs.Extract(p, tunnelrpc.ConnectResult_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
type ConnectError struct {
|
||||
Cause string
|
||||
RetryAfter time.Duration
|
||||
ShouldRetry bool
|
||||
}
|
||||
|
||||
func MarshalConnectError(s tunnelrpc.ConnectError, p *ConnectError) error {
|
||||
return pogs.Insert(tunnelrpc.ConnectError_TypeID, s.Struct, p)
|
||||
}
|
||||
|
||||
func UnmarshalConnectError(s tunnelrpc.ConnectError) (*ConnectError, error) {
|
||||
p := new(ConnectError)
|
||||
err := pogs.Extract(p, tunnelrpc.ConnectError_TypeID, s.Struct)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (e *ConnectError) Error() string {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
@@ -90,10 +127,60 @@ func UnmarshalServerInfo(s tunnelrpc.ServerInfo) (*ServerInfo, error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
type ConnectParameters struct {
|
||||
OriginCert []byte
|
||||
CloudflaredID uuid.UUID
|
||||
NumPreviousAttempts uint8
|
||||
Tags []Tag
|
||||
CloudflaredVersion string
|
||||
}
|
||||
|
||||
// CapnpConnectParameters is ConnectParameters represented in Cap'n Proto build-in types
|
||||
type CapnpConnectParameters struct {
|
||||
OriginCert []byte
|
||||
CloudflaredID []byte
|
||||
NumPreviousAttempts uint8
|
||||
Tags []Tag
|
||||
CloudflaredVersion string
|
||||
}
|
||||
|
||||
func MarshalConnectParameters(s tunnelrpc.CapnpConnectParameters, p *ConnectParameters) error {
|
||||
cloudflaredIDBytes, err := p.CloudflaredID.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
capnpConnectParameters := &CapnpConnectParameters{
|
||||
OriginCert: p.OriginCert,
|
||||
CloudflaredID: cloudflaredIDBytes,
|
||||
NumPreviousAttempts: p.NumPreviousAttempts,
|
||||
CloudflaredVersion: p.CloudflaredVersion,
|
||||
}
|
||||
return pogs.Insert(tunnelrpc.CapnpConnectParameters_TypeID, s.Struct, capnpConnectParameters)
|
||||
}
|
||||
|
||||
func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectParameters, error) {
|
||||
p := new(CapnpConnectParameters)
|
||||
err := pogs.Extract(p, tunnelrpc.CapnpConnectParameters_TypeID, s.Struct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cloudflaredID, err := uuid.FromBytes(p.CloudflaredID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ConnectParameters{
|
||||
OriginCert: p.OriginCert,
|
||||
CloudflaredID: cloudflaredID,
|
||||
NumPreviousAttempts: p.NumPreviousAttempts,
|
||||
CloudflaredVersion: p.CloudflaredVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TunnelServer interface {
|
||||
RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error)
|
||||
GetServerInfo(ctx context.Context) (*ServerInfo, error)
|
||||
UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error
|
||||
Connect(ctx context.Context, paramaters *ConnectParameters) (*ConnectResult, error)
|
||||
}
|
||||
|
||||
func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer {
|
||||
@@ -151,7 +238,27 @@ func (i TunnelServer_PogsImpl) UnregisterTunnel(p tunnelrpc.TunnelServer_unregis
|
||||
gracePeriodNanoSec := p.Params.GracePeriodNanoSec()
|
||||
server.Ack(p.Options)
|
||||
return i.impl.UnregisterTunnel(p.Ctx, gracePeriodNanoSec)
|
||||
}
|
||||
|
||||
func (i TunnelServer_PogsImpl) Connect(p tunnelrpc.TunnelServer_connect) error {
|
||||
paramaters, err := p.Params.Parameters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pogsParameters, err := UnmarshalConnectParameters(paramaters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server.Ack(p.Options)
|
||||
connectResult, err := i.impl.Connect(p.Ctx, pogsParameters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := p.Results.NewResult()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalConnectResult(result, connectResult)
|
||||
}
|
||||
|
||||
type TunnelServer_PogsClient struct {
|
||||
@@ -212,3 +319,25 @@ func (c TunnelServer_PogsClient) UnregisterTunnel(ctx context.Context, gracePeri
|
||||
_, err := promise.Struct()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c TunnelServer_PogsClient) Connect(ctx context.Context,
|
||||
parameters *ConnectParameters,
|
||||
) (*ConnectResult, error) {
|
||||
client := tunnelrpc.TunnelServer{Client: c.Client}
|
||||
promise := client.Connect(ctx, func(p tunnelrpc.TunnelServer_connect_Params) error {
|
||||
connectParameters, err := p.NewParameters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MarshalConnectParameters(connectParameters, parameters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
retval, err := promise.Result().Struct()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalConnectResult(retval)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,163 @@ struct RegistrationOptions {
|
||||
uuid @11 :Text;
|
||||
}
|
||||
|
||||
struct CapnpConnectParameters {
|
||||
# certificate and token to prove ownership of a zone
|
||||
originCert @0 :Data;
|
||||
# UUID assigned to this cloudflared obtained from Hello
|
||||
cloudflaredID @1 :Data;
|
||||
# number of previous attempts to send Connect
|
||||
numPreviousAttempts @2 :UInt8;
|
||||
# user defined labels for this cloudflared
|
||||
tags @3 :List(Tag);
|
||||
# release version of cloudflared
|
||||
cloudflaredVersion @4 :Text;
|
||||
}
|
||||
|
||||
struct ConnectResult {
|
||||
err @0 :ConnectError;
|
||||
# Information about the server this connection is established with
|
||||
serverInfo @1 :ServerInfo;
|
||||
}
|
||||
|
||||
struct ConnectError {
|
||||
cause @0 :Text;
|
||||
# How long should this connection wait to retry in ns
|
||||
retryAfter @1 :Int64;
|
||||
shouldRetry @2 :Bool;
|
||||
}
|
||||
|
||||
struct CloudflaredConfig {
|
||||
# Timestamp (in ns) of this configuration. Any configuration supplied to
|
||||
# useConfiguration() with an older timestamp should be ignored.
|
||||
timestamp @0 :Int64;
|
||||
# Frequency (in ns) to check Equinox for updates.
|
||||
# Zero means auto-update is disabled.
|
||||
# cloudflared CLI option: `autoupdate-freq`
|
||||
autoUpdateFrequency @1 :Int64;
|
||||
# Frequency (in ns) to update connection-based metrics.
|
||||
# cloudflared CLI option: `metrics-update-freq`
|
||||
metricsUpdateFrequency @2 :Int64;
|
||||
# interval (in ns) between heartbeats with the Cloudflare edge
|
||||
# cloudflared CLI option: `heartbeat-interval`
|
||||
heartbeatInterval @3 :Int64;
|
||||
# Minimum number of unacked heartbeats for cloudflared to send before
|
||||
# closing the connection to the edge.
|
||||
# cloudflared CLI option: `heartbeat-count`
|
||||
maxFailedHeartbeats @4 :UInt64;
|
||||
# Time (in ns) to continue serving requests after cloudflared receives its
|
||||
# first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to
|
||||
# shutdown immediately. For example, this field can be used to gracefully
|
||||
# transition traffic to another cloudflared instance.
|
||||
# cloudflared CLI option: `grace-period`
|
||||
gracePeriod @5 :Int64;
|
||||
# Configuration for cloudflared to run as a DNS-over-HTTPS proxy.
|
||||
# cloudflared CLI option: `proxy-dns`
|
||||
dohProxyConfigs @6 :List(DoHProxyConfig);
|
||||
# Configuration for cloudflared to run as an HTTP reverse proxy.
|
||||
reverseProxyConfigs @7 :List(ReverseProxyConfig);
|
||||
}
|
||||
|
||||
struct ReverseProxyConfig {
|
||||
tunnelID @0 :Text;
|
||||
origin :union {
|
||||
http @1 :HTTPOriginConfig;
|
||||
socket @2 :UnixSocketOriginConfig;
|
||||
websocket @3 :WebSocketOriginConfig;
|
||||
helloWorld @4 :HelloWorldOriginConfig;
|
||||
}
|
||||
# Maximum number of retries for connection/protocol errors.
|
||||
# cloudflared CLI option: `retries`
|
||||
retries @5 :UInt64;
|
||||
# maximum time (in ns) for cloudflared to wait to establish a connection
|
||||
# to the origin. Zero means no timeout.
|
||||
# cloudflared CLI option: `proxy-connect-timeout`
|
||||
connectionTimeout @6 :Int64;
|
||||
# Whether cloudflared should allow chunked transfer encoding to the
|
||||
# origin. (This should be disabled for WSGI origins, for example.)
|
||||
# negation of cloudflared CLI option: `no-chunked-encoding`
|
||||
chunkedEncoding @7 :Bool;
|
||||
# (beta) Use cross-stream compression instead of HTTP compression.
|
||||
# 0=off, 1=low, 2=medium, 3=high.
|
||||
# For more context see the mapping here: https://github.com/cloudflare/cloudflared/blob/2019.3.2/h2mux/h2_dictionaries.go#L62
|
||||
# cloudflared CLI option: `compression-quality`
|
||||
compressionQuality @8 :UInt64;
|
||||
}
|
||||
|
||||
struct UnixSocketOriginConfig {
|
||||
# path to the socket file.
|
||||
# cloudflared will send data to this socket via a Unix socket connection.
|
||||
# cloudflared CLI option: `unix-socket`
|
||||
path @0 :Text;
|
||||
}
|
||||
|
||||
#
|
||||
struct WebSocketOriginConfig {
|
||||
# URI of the origin service.
|
||||
# cloudflared will start a websocket server that forwards data to this URI
|
||||
# cloudflared CLI option: `url`
|
||||
# cloudflared logic: https://github.com/cloudflare/cloudflared/blob/2019.3.2/cmd/cloudflared/tunnel/cmd.go#L304
|
||||
url @0 :Text;
|
||||
}
|
||||
|
||||
struct HTTPOriginConfig {
|
||||
# HTTP(S) URL of the origin service.
|
||||
# cloudflared CLI option: `url`
|
||||
url @0 :Text;
|
||||
# the TCP keep-alive period (in ns) for an active network connection.
|
||||
# Zero means keep-alives are not enabled.
|
||||
# cloudflared CLI option: `proxy-tcp-keepalive`
|
||||
tcpKeepAlive @1 :Int64;
|
||||
# whether cloudflared should use a "happy eyeballs"-compliant procedure
|
||||
# to connect to origins that resolve to both IPv4 and IPv6 addresses
|
||||
# negation of cloudflared CLI option: `proxy-no-happy-eyeballs`
|
||||
dialDualStack @2 :Bool;
|
||||
# maximum time (in ns) for cloudflared to wait for a TLS handshake
|
||||
# with the origin. Zero means no timeout.
|
||||
# cloudflared CLI option: `proxy-tls-timeout`
|
||||
tlsHandshakeTimeout @3 :Int64;
|
||||
# Whether cloudflared should verify TLS connections to the origin.
|
||||
# negation of cloudflared CLI option: `no-tls-verify`
|
||||
tlsVerify @4 :Bool;
|
||||
# originCAPool specifies the root CA that cloudflared should use when
|
||||
# verifying TLS connections to the origin.
|
||||
# - if tlsVerify is false, originCAPool will be ignored.
|
||||
# - if tlsVerify is true and originCAPool is empty, the system CA pool
|
||||
# will be loaded if possible.
|
||||
# - if tlsVerify is true and originCAPool is non-empty, cloudflared will
|
||||
# treat it as the filepath to the root CA.
|
||||
# cloudflared CLI option: `origin-ca-pool`
|
||||
originCAPool @5 :Text;
|
||||
# Hostname to use when verifying TLS connections to the origin.
|
||||
# cloudflared CLI option: `origin-server-name`
|
||||
originServerName @6 :Text;
|
||||
# maximum number of idle (keep-alive) connections for cloudflared to
|
||||
# keep open with the origin. Zero means no limit.
|
||||
# cloudflared CLI option: `proxy-keepalive-connections`
|
||||
maxIdleConnections @7 :UInt64;
|
||||
# maximum time (in ns) for an idle (keep-alive) connection to remain
|
||||
# idle before closing itself. Zero means no timeout.
|
||||
# cloudflared CLI option: `proxy-keepalive-timeout`
|
||||
idleConnectionTimeout @8 :Int64;
|
||||
}
|
||||
|
||||
# configuration for cloudflared to provide a DNS over HTTPS proxy server
|
||||
struct DoHProxyConfig {
|
||||
# The hostname for the DoH proxy server to listen on.
|
||||
# cloudflared CLI option: `proxy-dns-address`
|
||||
listenHost @0 :Text;
|
||||
# The port for the DoH proxy server to listen on.
|
||||
# cloudflared CLI option: `proxy-dns-port`
|
||||
listenPort @1 :UInt16;
|
||||
# Upstream endpoint URLs for the DoH proxy server.
|
||||
# cloudflared CLI option: `proxy-dns-upstream`
|
||||
upstreams @2 :List(Text);
|
||||
}
|
||||
|
||||
struct HelloWorldOriginConfig {
|
||||
# nothing to configure
|
||||
}
|
||||
|
||||
struct Tag {
|
||||
name @0 :Text;
|
||||
value @1 :Text;
|
||||
@@ -61,8 +218,18 @@ struct ServerInfo {
|
||||
locationName @0 :Text;
|
||||
}
|
||||
|
||||
struct UseConfigurationResult {
|
||||
success @0 :Bool;
|
||||
errorMessage @1 :Text;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
interface CloudflaredServer {
|
||||
useConfiguration @0 (cloudflaredConfig :CloudflaredConfig) -> (result :UseConfigurationResult);
|
||||
}
|
||||
|
||||
+1894
-145
File diff suppressed because it is too large
Load Diff
+31
-11
@@ -5,15 +5,20 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/idna"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const defaultScheme = "http"
|
||||
|
||||
var supportedProtocol = [2]string{"http", "https"}
|
||||
var (
|
||||
supportedProtocols = []string{"http", "https", "rdp"}
|
||||
validationTimeout = time.Duration(30 * time.Second)
|
||||
)
|
||||
|
||||
func ValidateHostname(hostname string) (string, error) {
|
||||
if hostname == "" {
|
||||
@@ -116,7 +121,7 @@ func ValidateUrl(originUrl string) (string, error) {
|
||||
}
|
||||
|
||||
func validateScheme(scheme string) error {
|
||||
for _, protocol := range supportedProtocol {
|
||||
for _, protocol := range supportedProtocols {
|
||||
if scheme == protocol {
|
||||
return nil
|
||||
}
|
||||
@@ -137,26 +142,41 @@ func validateIP(scheme, host, port string) (string, error) {
|
||||
return fmt.Sprintf("%s://%s", scheme, host), nil
|
||||
}
|
||||
|
||||
func ValidateHTTPService(originURL string, transport http.RoundTripper) error {
|
||||
func ValidateHTTPService(originURL string, hostname string, transport http.RoundTripper) error {
|
||||
parsedURL, err := url.Parse(originURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Timeout: validationTimeout,
|
||||
}
|
||||
|
||||
initialResponse, initialErr := client.Get(parsedURL.String())
|
||||
if initialErr != nil || initialResponse.StatusCode != http.StatusOK {
|
||||
initialRequest, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initialRequest.Host = hostname
|
||||
_, initialErr := client.Do(initialRequest)
|
||||
if initialErr != nil {
|
||||
// Attempt the same endpoint via the other protocol (http/https); maybe we have better luck?
|
||||
oldScheme := parsedURL.Scheme
|
||||
parsedURL.Scheme = toggleProtocol(parsedURL.Scheme)
|
||||
|
||||
secondResponse, _ := client.Get(parsedURL.String())
|
||||
|
||||
if secondResponse != nil && secondResponse.StatusCode == http.StatusOK { // Worked this time--advise the user to switch protocols
|
||||
secondRequest, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secondRequest.Host = hostname
|
||||
_, 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",
|
||||
parsedURL.Hostname(),
|
||||
parsedURL.Host,
|
||||
oldScheme,
|
||||
parsedURL.Scheme,
|
||||
parsedURL,
|
||||
|
||||
+251
-27
@@ -1,18 +1,22 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateHostname(t *testing.T) {
|
||||
@@ -150,58 +154,278 @@ func TestToggleProtocol(t *testing.T) {
|
||||
assert.Equal(t, "", toggleProtocol(""))
|
||||
}
|
||||
|
||||
// Happy path 1: originURL is HTTP, and HTTP connections work
|
||||
func TestValidateHTTPService_HTTP2HTTP(t *testing.T) {
|
||||
originURL := "http://127.0.0.1/"
|
||||
hostname := "example.com"
|
||||
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return emptyResponse(200), nil
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
t.Fatal("http works, shouldn't have tried with https")
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return emptyResponse(503), nil
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
t.Fatal("http works, shouldn't have tried with https")
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
assert.Equal(t, nil, ValidateHTTPService("http://example.com/", client.Transport))
|
||||
}
|
||||
|
||||
func TestValidateHTTPService_ServerNonOKResponse(t *testing.T) {
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(400)
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
defer redirectServer.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
|
||||
assert.Equal(t, nil, ValidateHTTPService("http://example.com/", client.Transport))
|
||||
}
|
||||
|
||||
func TestValidateHTTPService_HTTPS2HTTP(t *testing.T) {
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
assert.Equal(t,
|
||||
"example.com doesn't seem to work over https, but does seem to work over http. Consider changing the origin URL to http://example.com:1234/",
|
||||
ValidateHTTPService("https://example.com:1234/", client.Transport).Error())
|
||||
}
|
||||
|
||||
// Happy path 2: originURL is HTTPS, and HTTPS connections work
|
||||
func TestValidateHTTPService_HTTPS2HTTPS(t *testing.T) {
|
||||
originURL := "https://127.0.0.1/"
|
||||
hostname := "example.com"
|
||||
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
t.Fatal("https works, shouldn't have tried with http")
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return emptyResponse(200), nil
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
t.Fatal("https works, shouldn't have tried with http")
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return emptyResponse(503), nil
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
assert.Equal(t, nil, ValidateHTTPService("https://example.com/", client.Transport))
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Nil(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
}
|
||||
|
||||
func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Error path 1: originURL is HTTPS, but HTTP connections work
|
||||
func TestValidateHTTPService_HTTPS2HTTP(t *testing.T) {
|
||||
originURL := "https://127.0.0.1:1234/"
|
||||
hostname := "example.com"
|
||||
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return emptyResponse(200), nil
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return emptyResponse(503), nil
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
assert.Equal(t,
|
||||
"example.com doesn't seem to work over http, but does seem to work over https. Consider changing the origin URL to https://example.com:1234/",
|
||||
ValidateHTTPService("http://example.com:1234/", client.Transport).Error())
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
|
||||
}
|
||||
|
||||
// Error path 2: originURL is HTTP, but HTTPS connections work
|
||||
func TestValidateHTTPService_HTTP2HTTPS(t *testing.T) {
|
||||
originURL := "http://127.0.0.1:1234/"
|
||||
hostname := "example.com"
|
||||
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return emptyResponse(200), nil
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, testRoundTripper(func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, req.Host, hostname)
|
||||
if req.URL.Scheme == "http" {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
if req.URL.Scheme == "https" {
|
||||
return emptyResponse(503), nil
|
||||
}
|
||||
panic("Shouldn't reach here")
|
||||
})))
|
||||
|
||||
// Integration-style test with a mock server
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:1234", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer server.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, client.Transport))
|
||||
|
||||
// this will fail if the client follows the 302
|
||||
redirectServer, redirectClient, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/followedRedirect" {
|
||||
t.Fatal("shouldn't have followed the 302")
|
||||
}
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
w.Header().Set("Location", "/followedRedirect")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
defer redirectServer.Close()
|
||||
assert.Error(t, ValidateHTTPService(originURL, hostname, redirectClient.Transport))
|
||||
}
|
||||
|
||||
// error path 3: origin URL is nonresponsive
|
||||
func TestValidateHTTPService_NonResponsiveOrigin(t *testing.T) {
|
||||
originURL := "https://127.0.0.1/"
|
||||
hostname := "example.com"
|
||||
oldValidationTimeout := validationTimeout
|
||||
defer func() {
|
||||
validationTimeout = oldValidationTimeout
|
||||
}()
|
||||
validationTimeout = 500 * time.Millisecond
|
||||
|
||||
server, client, err := createSecureMockServerAndClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "CONNECT" {
|
||||
assert.Equal(t, "127.0.0.1:443", r.Host)
|
||||
} else {
|
||||
assert.Equal(t, hostname, r.Host)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
err = ValidateHTTPService(originURL, hostname, client.Transport)
|
||||
if err, ok := err.(net.Error); assert.True(t, ok) {
|
||||
assert.True(t, err.Timeout())
|
||||
}
|
||||
}
|
||||
|
||||
type testRoundTripper func(req *http.Request) (*http.Response, error)
|
||||
|
||||
func (f testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func emptyResponse(statusCode int) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(nil)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func createMockServerAndClient(handler http.Handler) (*httptest.Server, *http.Client, error) {
|
||||
|
||||
+17
-10
@@ -1,14 +1,21 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
+17
-10
@@ -1,14 +1,21 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
+17
-10
@@ -1,14 +1,21 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
+17
-10
@@ -1,14 +1,21 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
Copyright (c) 2013 TOML authors
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
+1
-1
@@ -775,7 +775,7 @@ func lexDatetime(lx *lexer) stateFn {
|
||||
return lexDatetime
|
||||
}
|
||||
switch r {
|
||||
case '-', 'T', ':', '.', 'Z':
|
||||
case '-', 'T', ':', '.', 'Z', '+':
|
||||
return lexDatetime
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+5
-5
@@ -1,16 +1,16 @@
|
||||
Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
|
||||
Copyright (c) 2013 CloudFlare, Inc. 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
|
||||
* 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
|
||||
* 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.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
* Neither the name of the CloudFlare, Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
HOLDER OR CONTRIBUTORS 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
|
||||
@@ -0,0 +1,3 @@
|
||||
cover.out~
|
||||
benchmark/benchmark
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
RACE+=--race
|
||||
|
||||
PKGNAME=github.com/cloudflare/golibs/lrucache
|
||||
SKIPCOVER=list.go|list_extension.go|priorityqueue.go
|
||||
|
||||
.PHONY: all test bench cover clean
|
||||
|
||||
all:
|
||||
@echo "Targets:"
|
||||
@echo " test: run tests with race detector"
|
||||
@echo " cover: print test coverage"
|
||||
@echo " bench: run basic benchmarks"
|
||||
|
||||
test:
|
||||
@go test $(RACE) -bench=. -v $(PKGNAME)
|
||||
|
||||
COVEROUT=cover.out
|
||||
cover:
|
||||
@go test -coverprofile=$(COVEROUT) -v $(PKGNAME)
|
||||
@cat $(COVEROUT) | egrep -v '$(SKIPCOVER)' > $(COVEROUT)~
|
||||
@go tool cover -func=$(COVEROUT)~|sed 's|^.*/\([^/]*/[^/]*/[^/]*\)$$|\1|g'
|
||||
|
||||
bench:
|
||||
@echo "[*] Scalability of cache/lrucache"
|
||||
@echo "[ ] Operations in shared cache using one core"
|
||||
@GOMAXPROCS=1 go test -run=- -bench='.*LRUCache.*' $(PKGNAME) \
|
||||
| egrep -v "^PASS|^ok"
|
||||
|
||||
@echo "[*] Scalability of cache/multilru"
|
||||
@echo "[ ] Operations in four caches using four cores "
|
||||
@GOMAXPROCS=4 go test -run=- -bench='.*MultiLRU.*' $(PKGNAME) \
|
||||
| egrep -v "^PASS|^ok"
|
||||
|
||||
|
||||
@(cd benchmark; go build $(PKGNAME)/benchmark)
|
||||
@./benchmark/benchmark
|
||||
|
||||
clean:
|
||||
rm -rf $(COVEROUT) $(COVEROUT)~ benchmark/benchmark
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
LRU Cache
|
||||
---------
|
||||
|
||||
A `golang` implementation of last recently used cache data structure.
|
||||
|
||||
To install:
|
||||
|
||||
go get github.com/cloudflare/golibs/lrucache
|
||||
|
||||
To test:
|
||||
|
||||
cd $GOPATH/src/github.com/cloudflare/golibs/lrucache
|
||||
make test
|
||||
|
||||
For coverage:
|
||||
|
||||
make cover
|
||||
|
||||
Basic benchmarks:
|
||||
|
||||
$ make bench # As tested on my two core i5
|
||||
[*] Scalability of cache/lrucache
|
||||
[ ] Operations in shared cache using one core
|
||||
BenchmarkConcurrentGetLRUCache 5000000 450 ns/op
|
||||
BenchmarkConcurrentSetLRUCache 2000000 821 ns/op
|
||||
BenchmarkConcurrentSetNXLRUCache 5000000 664 ns/op
|
||||
|
||||
[*] Scalability of cache/multilru
|
||||
[ ] Operations in four caches using four cores
|
||||
BenchmarkConcurrentGetMultiLRU-4 5000000 475 ns/op
|
||||
BenchmarkConcurrentSetMultiLRU-4 2000000 809 ns/op
|
||||
BenchmarkConcurrentSetNXMultiLRU-4 5000000 643 ns/op
|
||||
|
||||
[*] Capacity=4096 Keys=30000 KeySpace=15625
|
||||
vitess LRUCache MultiLRUCache-4
|
||||
create 1.709us 1.626374ms 343.54us
|
||||
Get (miss) 144.266083ms 132.470397ms 177.277193ms
|
||||
SetNX #1 338.637977ms 380.733302ms 411.709204ms
|
||||
Get (hit) 195.896066ms 173.252112ms 234.109494ms
|
||||
SetNX #2 349.785951ms 367.255624ms 419.129127ms
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
// Package lrucache implements a last recently used cache data structure.
|
||||
//
|
||||
// This code tries to avoid dynamic memory allocations - all required
|
||||
// memory is allocated on creation. Access to the data structure is
|
||||
// O(1). Modification O(log(n)) if expiry is used, O(1)
|
||||
// otherwise.
|
||||
//
|
||||
// This package exports three things:
|
||||
// LRUCache: is the main implementation. It supports multithreading by
|
||||
// using guarding mutex lock.
|
||||
//
|
||||
// MultiLRUCache: is a sharded implementation. It supports the same
|
||||
// API as LRUCache and uses it internally, but is not limited to
|
||||
// a single CPU as every shard is separately locked. Use this
|
||||
// data structure instead of LRUCache if you have have lock
|
||||
// contention issues.
|
||||
//
|
||||
// Cache interface: Both implementations fulfill it.
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache interface is fulfilled by the LRUCache and MultiLRUCache
|
||||
// implementations.
|
||||
type Cache interface {
|
||||
// Methods not needing to know current time.
|
||||
//
|
||||
// Get a key from the cache, possibly stale. Update its LRU
|
||||
// score.
|
||||
Get(key string) (value interface{}, ok bool)
|
||||
// Get a key from the cache, possibly stale. Don't modify its LRU score. O(1)
|
||||
GetQuiet(key string) (value interface{}, ok bool)
|
||||
// Get and remove a key from the cache.
|
||||
Del(key string) (value interface{}, ok bool)
|
||||
// Evict all items from the cache.
|
||||
Clear() int
|
||||
// Number of entries used in the LRU
|
||||
Len() int
|
||||
// Get the total capacity of the LRU
|
||||
Capacity() int
|
||||
|
||||
// Methods use time.Now() when neccessary to determine expiry.
|
||||
//
|
||||
// Add an item to the cache overwriting existing one if it
|
||||
// exists.
|
||||
Set(key string, value interface{}, expire time.Time)
|
||||
// Get a key from the cache, make sure it's not stale. Update
|
||||
// its LRU score.
|
||||
GetNotStale(key string) (value interface{}, ok bool)
|
||||
// Evict all the expired items.
|
||||
Expire() int
|
||||
|
||||
// Methods allowing to explicitly specify time used to
|
||||
// determine if items are expired.
|
||||
//
|
||||
// Add an item to the cache overwriting existing one if it
|
||||
// exists. Allows specifing current time required to expire an
|
||||
// item when no more slots are used.
|
||||
SetNow(key string, value interface{}, expire time.Time, now time.Time)
|
||||
// Get a key from the cache, make sure it's not stale. Update
|
||||
// its LRU score.
|
||||
GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool)
|
||||
// Evict items that expire before Now.
|
||||
ExpireNow(now time.Time) int
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/* This file is a slightly modified file from the go package sources
|
||||
and is released on the following license:
|
||||
|
||||
Copyright (c) 2012 The Go Authors. 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.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may 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 THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS 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.
|
||||
*/
|
||||
|
||||
// Package list implements a doubly linked list.
|
||||
//
|
||||
// To iterate over a list (where l is a *List):
|
||||
// for e := l.Front(); e != nil; e = e.Next() {
|
||||
// // do something with e.Value
|
||||
// }
|
||||
//
|
||||
|
||||
package lrucache
|
||||
|
||||
// Element is an element of a linked list.
|
||||
type element struct {
|
||||
// Next and previous pointers in the doubly-linked list of elements.
|
||||
// To simplify the implementation, internally a list l is implemented
|
||||
// as a ring, such that &l.root is both the next element of the last
|
||||
// list element (l.Back()) and the previous element of the first list
|
||||
// element (l.Front()).
|
||||
next, prev *element
|
||||
|
||||
// The list to which this element belongs.
|
||||
list *list
|
||||
|
||||
// The value stored with this element.
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// Next returns the next list element or nil.
|
||||
func (e *element) Next() *element {
|
||||
if p := e.next; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prev returns the previous list element or nil.
|
||||
func (e *element) Prev() *element {
|
||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List represents a doubly linked list.
|
||||
// The zero value for List is an empty list ready to use.
|
||||
type list struct {
|
||||
root element // sentinel list element, only &root, root.prev, and root.next are used
|
||||
len int // current list length excluding (this) sentinel element
|
||||
}
|
||||
|
||||
// Init initializes or clears list l.
|
||||
func (l *list) Init() *list {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
return l
|
||||
}
|
||||
|
||||
// New returns an initialized list.
|
||||
// func New() *list { return new(list).Init() }
|
||||
|
||||
// Len returns the number of elements of list l.
|
||||
// The complexity is O(1).
|
||||
func (l *list) Len() int { return l.len }
|
||||
|
||||
// Front returns the first element of list l or nil
|
||||
func (l *list) Front() *element {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.next
|
||||
}
|
||||
|
||||
// Back returns the last element of list l or nil.
|
||||
func (l *list) Back() *element {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *list) insert(e, at *element) *element {
|
||||
n := at.next
|
||||
at.next = e
|
||||
e.prev = at
|
||||
e.next = n
|
||||
n.prev = e
|
||||
e.list = l
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
|
||||
func (l *list) insertValue(v interface{}, at *element) *element {
|
||||
return l.insert(&element{Value: v}, at)
|
||||
}
|
||||
|
||||
// remove removes e from its list, decrements l.len, and returns e.
|
||||
func (l *list) remove(e *element) *element {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
e.list = nil
|
||||
l.len--
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove removes e from l if e is an element of list l.
|
||||
// It returns the element value e.Value.
|
||||
func (l *list) Remove(e *element) interface{} {
|
||||
if e.list == l {
|
||||
// if e.list == l, l must have been initialized when e was inserted
|
||||
// in l or l == nil (e is a zero Element) and l.remove will crash
|
||||
l.remove(e)
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *list) PushFront(v interface{}) *element {
|
||||
return l.insertValue(v, &l.root)
|
||||
}
|
||||
|
||||
// PushBack inserts a new element e with value v at the back of list l and returns e.
|
||||
func (l *list) PushBack(v interface{}) *element {
|
||||
return l.insertValue(v, l.root.prev)
|
||||
}
|
||||
|
||||
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
func (l *list) InsertBefore(v interface{}, mark *element) *element {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark.prev)
|
||||
}
|
||||
|
||||
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
|
||||
// If mark is not an element of l, the list is not modified.
|
||||
func (l *list) InsertAfter(v interface{}, mark *element) *element {
|
||||
if mark.list != l {
|
||||
return nil
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
return l.insertValue(v, mark)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
func (l *list) MoveToFront(e *element) {
|
||||
if e.list != l || l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.insert(l.remove(e), &l.root)
|
||||
}
|
||||
|
||||
// MoveToBack moves element e to the back of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
func (l *list) MoveToBack(e *element) {
|
||||
if e.list != l || l.root.prev == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.insert(l.remove(e), l.root.prev)
|
||||
}
|
||||
|
||||
// MoveBefore moves element e to its new position before mark.
|
||||
// If e is not an element of l, or e == mark, the list is not modified.
|
||||
func (l *list) MoveBefore(e, mark *element) {
|
||||
if e.list != l || e == mark {
|
||||
return
|
||||
}
|
||||
l.insert(l.remove(e), mark.prev)
|
||||
}
|
||||
|
||||
// MoveAfter moves element e to its new position after mark.
|
||||
// If e is not an element of l, or e == mark, the list is not modified.
|
||||
func (l *list) MoveAfter(e, mark *element) {
|
||||
if e.list != l || e == mark {
|
||||
return
|
||||
}
|
||||
l.insert(l.remove(e), mark)
|
||||
}
|
||||
|
||||
// PushBackList inserts a copy of an other list at the back of list l.
|
||||
// The lists l and other may be the same.
|
||||
func (l *list) PushBackList(other *list) {
|
||||
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
|
||||
l.insertValue(e.Value, l.root.prev)
|
||||
}
|
||||
}
|
||||
|
||||
// PushFrontList inserts a copy of an other list at the front of list l.
|
||||
// The lists l and other may be the same.
|
||||
func (l *list) PushFrontList(other *list) {
|
||||
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
|
||||
l.insertValue(e.Value, &l.root)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
// Extensions to "container/list" that allowing reuse of Elements.
|
||||
|
||||
package lrucache
|
||||
|
||||
func (l *list) PushElementFront(e *element) *element {
|
||||
return l.insert(e, &l.root)
|
||||
}
|
||||
|
||||
func (l *list) PushElementBack(e *element) *element {
|
||||
return l.insert(e, l.root.prev)
|
||||
}
|
||||
|
||||
func (l *list) PopElementFront() *element {
|
||||
el := l.Front()
|
||||
l.Remove(el)
|
||||
return el
|
||||
}
|
||||
|
||||
func (l *list) PopFront() interface{} {
|
||||
el := l.Front()
|
||||
l.Remove(el)
|
||||
return el.Value
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
// Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Every element in the cache is linked to three data structures:
|
||||
// Table map, PriorityQueue heap ordered by expiry and a LruList list
|
||||
// ordered by decreasing popularity.
|
||||
type entry struct {
|
||||
element element // list element. value is a pointer to this entry
|
||||
key string // key is a key!
|
||||
value interface{} //
|
||||
expire time.Time // time when the item is expired. it's okay to be stale.
|
||||
index int // index for priority queue needs. -1 if entry is free
|
||||
}
|
||||
|
||||
// LRUCache data structure. Never dereference it or copy it by
|
||||
// value. Always use it through a pointer.
|
||||
type LRUCache struct {
|
||||
lock sync.Mutex
|
||||
table map[string]*entry // all entries in table must be in lruList
|
||||
priorityQueue priorityQueue // some elements from table may be in priorityQueue
|
||||
lruList list // every entry is either used and resides in lruList
|
||||
freeList list // or free and is linked to freeList
|
||||
|
||||
ExpireGracePeriod time.Duration // time after an expired entry is purged from cache (unless pushed out of LRU)
|
||||
}
|
||||
|
||||
// Initialize the LRU cache instance. O(capacity)
|
||||
func (b *LRUCache) Init(capacity uint) {
|
||||
b.table = make(map[string]*entry, capacity)
|
||||
b.priorityQueue = make([]*entry, 0, capacity)
|
||||
b.lruList.Init()
|
||||
b.freeList.Init()
|
||||
heap.Init(&b.priorityQueue)
|
||||
|
||||
// Reserve all the entries in one giant continous block of memory
|
||||
arrayOfEntries := make([]entry, capacity)
|
||||
for i := uint(0); i < capacity; i++ {
|
||||
e := &arrayOfEntries[i]
|
||||
e.element.Value = e
|
||||
e.index = -1
|
||||
b.freeList.PushElementBack(&e.element)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new LRU cache instance. Allocate all the needed memory. O(capacity)
|
||||
func NewLRUCache(capacity uint) *LRUCache {
|
||||
b := &LRUCache{}
|
||||
b.Init(capacity)
|
||||
return b
|
||||
}
|
||||
|
||||
// Give me the entry with lowest expiry field if it's before now.
|
||||
func (b *LRUCache) expiredEntry(now time.Time) *entry {
|
||||
if len(b.priorityQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if now.IsZero() {
|
||||
// Fill it only when actually used.
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
if e := b.priorityQueue[0]; e.expire.Before(now) {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Give me the least used entry.
|
||||
func (b *LRUCache) leastUsedEntry() *entry {
|
||||
return b.lruList.Back().Value.(*entry)
|
||||
}
|
||||
|
||||
func (b *LRUCache) freeSomeEntry(now time.Time) (e *entry, used bool) {
|
||||
if b.freeList.Len() > 0 {
|
||||
return b.freeList.Front().Value.(*entry), false
|
||||
}
|
||||
|
||||
e = b.expiredEntry(now)
|
||||
if e != nil {
|
||||
return e, true
|
||||
}
|
||||
|
||||
if b.lruList.Len() == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return b.leastUsedEntry(), true
|
||||
}
|
||||
|
||||
// Move entry from used/lru list to a free list. Clear the entry as well.
|
||||
func (b *LRUCache) removeEntry(e *entry) {
|
||||
if e.element.list != &b.lruList {
|
||||
panic("list lruList")
|
||||
}
|
||||
|
||||
if e.index != -1 {
|
||||
heap.Remove(&b.priorityQueue, e.index)
|
||||
}
|
||||
b.lruList.Remove(&e.element)
|
||||
b.freeList.PushElementFront(&e.element)
|
||||
delete(b.table, e.key)
|
||||
e.key = ""
|
||||
e.value = nil
|
||||
}
|
||||
|
||||
func (b *LRUCache) insertEntry(e *entry) {
|
||||
if e.element.list != &b.freeList {
|
||||
panic("list freeList")
|
||||
}
|
||||
|
||||
if !e.expire.IsZero() {
|
||||
heap.Push(&b.priorityQueue, e)
|
||||
}
|
||||
b.freeList.Remove(&e.element)
|
||||
b.lruList.PushElementFront(&e.element)
|
||||
b.table[e.key] = e
|
||||
}
|
||||
|
||||
func (b *LRUCache) touchEntry(e *entry) {
|
||||
b.lruList.MoveToFront(&e.element)
|
||||
}
|
||||
|
||||
// Add an item to the cache overwriting existing one if it
|
||||
// exists. Allows specifing current time required to expire an item
|
||||
// when no more slots are used. O(log(n)) if expiry is set, O(1) when
|
||||
// clear.
|
||||
func (b *LRUCache) SetNow(key string, value interface{}, expire time.Time, now time.Time) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
var used bool
|
||||
|
||||
e := b.table[key]
|
||||
if e != nil {
|
||||
used = true
|
||||
} else {
|
||||
e, used = b.freeSomeEntry(now)
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if used {
|
||||
b.removeEntry(e)
|
||||
}
|
||||
|
||||
e.key = key
|
||||
e.value = value
|
||||
e.expire = expire
|
||||
b.insertEntry(e)
|
||||
}
|
||||
|
||||
// Add an item to the cache overwriting existing one if it
|
||||
// exists. O(log(n)) if expiry is set, O(1) when clear.
|
||||
func (b *LRUCache) Set(key string, value interface{}, expire time.Time) {
|
||||
b.SetNow(key, value, expire, time.Time{})
|
||||
}
|
||||
|
||||
// Get a key from the cache, possibly stale. Update its LRU score. O(1)
|
||||
func (b *LRUCache) Get(key string) (v interface{}, ok bool) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
e := b.table[key]
|
||||
if e == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
b.touchEntry(e)
|
||||
return e.value, true
|
||||
}
|
||||
|
||||
// Get a key from the cache, possibly stale. Don't modify its LRU score. O(1)
|
||||
func (b *LRUCache) GetQuiet(key string) (v interface{}, ok bool) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
e := b.table[key]
|
||||
if e == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return e.value, true
|
||||
}
|
||||
|
||||
// Get a key from the cache, make sure it's not stale. Update its
|
||||
// LRU score. O(log(n)) if the item is expired.
|
||||
func (b *LRUCache) GetNotStale(key string) (value interface{}, ok bool) {
|
||||
return b.GetNotStaleNow(key, time.Now())
|
||||
}
|
||||
|
||||
// Get a key from the cache, make sure it's not stale. Update its
|
||||
// LRU score. O(log(n)) if the item is expired.
|
||||
func (b *LRUCache) GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
e := b.table[key]
|
||||
if e == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if e.expire.Before(now) {
|
||||
// Remove entries expired for more than a graceful period
|
||||
if b.ExpireGracePeriod == 0 || e.expire.Sub(now) > b.ExpireGracePeriod {
|
||||
b.removeEntry(e)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
b.touchEntry(e)
|
||||
return e.value, true
|
||||
}
|
||||
|
||||
// Get a key from the cache, possibly stale. Update its LRU
|
||||
// score. O(1) always.
|
||||
func (b *LRUCache) GetStale(key string) (value interface{}, ok, expired bool) {
|
||||
return b.GetStaleNow(key, time.Now())
|
||||
}
|
||||
|
||||
// Get a key from the cache, possibly stale. Update its LRU
|
||||
// score. O(1) always.
|
||||
func (b *LRUCache) GetStaleNow(key string, now time.Time) (value interface{}, ok, expired bool) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
e := b.table[key]
|
||||
if e == nil {
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
b.touchEntry(e)
|
||||
return e.value, true, e.expire.Before(now)
|
||||
}
|
||||
|
||||
// Get and remove a key from the cache. O(log(n)) if the item is using expiry, O(1) otherwise.
|
||||
func (b *LRUCache) Del(key string) (v interface{}, ok bool) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
e := b.table[key]
|
||||
if e == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value := e.value
|
||||
b.removeEntry(e)
|
||||
return value, true
|
||||
}
|
||||
|
||||
// Evict all items from the cache. O(n*log(n))
|
||||
func (b *LRUCache) Clear() int {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
// First, remove entries that have expiry set
|
||||
l := len(b.priorityQueue)
|
||||
for i := 0; i < l; i++ {
|
||||
// This could be reduced to O(n).
|
||||
b.removeEntry(b.priorityQueue[0])
|
||||
}
|
||||
|
||||
// Second, remove all remaining entries
|
||||
r := b.lruList.Len()
|
||||
for i := 0; i < r; i++ {
|
||||
b.removeEntry(b.leastUsedEntry())
|
||||
}
|
||||
return l + r
|
||||
}
|
||||
|
||||
// Evict all the expired items. O(n*log(n))
|
||||
func (b *LRUCache) Expire() int {
|
||||
return b.ExpireNow(time.Now())
|
||||
}
|
||||
|
||||
// Evict items that expire before `now`. O(n*log(n))
|
||||
func (b *LRUCache) ExpireNow(now time.Time) int {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
i := 0
|
||||
for {
|
||||
e := b.expiredEntry(now)
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
b.removeEntry(e)
|
||||
i += 1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Number of entries used in the LRU
|
||||
func (b *LRUCache) Len() int {
|
||||
// yes. this stupid thing requires locking
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
return b.lruList.Len()
|
||||
}
|
||||
|
||||
// Get the total capacity of the LRU
|
||||
func (b *LRUCache) Capacity() int {
|
||||
// yes. this stupid thing requires locking
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
return b.lruList.Len() + b.freeList.Len()
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
package lrucache
|
||||
|
||||
import (
|
||||
"hash/crc32"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MultiLRUCache data structure. Never dereference it or copy it by
|
||||
// value. Always use it through a pointer.
|
||||
type MultiLRUCache struct {
|
||||
buckets uint
|
||||
cache []*LRUCache
|
||||
}
|
||||
|
||||
// Using this constructor is almost always wrong. Use NewMultiLRUCache instead.
|
||||
func (m *MultiLRUCache) Init(buckets, bucket_capacity uint) {
|
||||
m.buckets = buckets
|
||||
m.cache = make([]*LRUCache, buckets)
|
||||
for i := uint(0); i < buckets; i++ {
|
||||
m.cache[i] = NewLRUCache(bucket_capacity)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the stale expiry grace period for each cache in the multicache instance.
|
||||
func (m *MultiLRUCache) SetExpireGracePeriod(p time.Duration) {
|
||||
for _, c := range m.cache {
|
||||
c.ExpireGracePeriod = p
|
||||
}
|
||||
}
|
||||
|
||||
func NewMultiLRUCache(buckets, bucket_capacity uint) *MultiLRUCache {
|
||||
m := &MultiLRUCache{}
|
||||
m.Init(buckets, bucket_capacity)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) bucketNo(key string) uint {
|
||||
// Arbitrary choice. Any fast hash will do.
|
||||
return uint(crc32.ChecksumIEEE([]byte(key))) % m.buckets
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Set(key string, value interface{}, expire time.Time) {
|
||||
m.cache[m.bucketNo(key)].Set(key, value, expire)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) SetNow(key string, value interface{}, expire time.Time, now time.Time) {
|
||||
m.cache[m.bucketNo(key)].SetNow(key, value, expire, now)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Get(key string) (value interface{}, ok bool) {
|
||||
return m.cache[m.bucketNo(key)].Get(key)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) GetQuiet(key string) (value interface{}, ok bool) {
|
||||
return m.cache[m.bucketNo(key)].Get(key)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) GetNotStale(key string) (value interface{}, ok bool) {
|
||||
return m.cache[m.bucketNo(key)].GetNotStale(key)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) GetNotStaleNow(key string, now time.Time) (value interface{}, ok bool) {
|
||||
return m.cache[m.bucketNo(key)].GetNotStaleNow(key, now)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) GetStale(key string) (value interface{}, ok, expired bool) {
|
||||
return m.cache[m.bucketNo(key)].GetStale(key)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) GetStaleNow(key string, now time.Time) (value interface{}, ok, expired bool) {
|
||||
return m.cache[m.bucketNo(key)].GetStaleNow(key, now)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Del(key string) (value interface{}, ok bool) {
|
||||
return m.cache[m.bucketNo(key)].Del(key)
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Clear() int {
|
||||
var s int
|
||||
for _, c := range m.cache {
|
||||
s += c.Clear()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Len() int {
|
||||
var s int
|
||||
for _, c := range m.cache {
|
||||
s += c.Len()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Capacity() int {
|
||||
var s int
|
||||
for _, c := range m.cache {
|
||||
s += c.Capacity()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) Expire() int {
|
||||
var s int
|
||||
for _, c := range m.cache {
|
||||
s += c.Expire()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *MultiLRUCache) ExpireNow(now time.Time) int {
|
||||
var s int
|
||||
for _, c := range m.cache {
|
||||
s += c.ExpireNow(now)
|
||||
}
|
||||
return s
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2013 CloudFlare, Inc.
|
||||
|
||||
// This code is based on golang example from "container/heap" package.
|
||||
|
||||
package lrucache
|
||||
|
||||
type priorityQueue []*entry
|
||||
|
||||
func (pq priorityQueue) Len() int {
|
||||
return len(pq)
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Less(i, j int) bool {
|
||||
return pq[i].expire.Before(pq[j].expire)
|
||||
}
|
||||
|
||||
func (pq priorityQueue) Swap(i, j int) {
|
||||
pq[i], pq[j] = pq[j], pq[i]
|
||||
pq[i].index = i
|
||||
pq[j].index = j
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Push(e interface{}) {
|
||||
n := len(*pq)
|
||||
item := e.(*entry)
|
||||
item.index = n
|
||||
*pq = append(*pq, item)
|
||||
}
|
||||
|
||||
func (pq *priorityQueue) Pop() interface{} {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
item.index = -1
|
||||
*pq = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
-1
@@ -39,7 +39,6 @@ var Directives = []string{
|
||||
"auto",
|
||||
"secondary",
|
||||
"etcd",
|
||||
"loop",
|
||||
"forward",
|
||||
"proxy",
|
||||
"erratic",
|
||||
|
||||
+1
-15
@@ -10,9 +10,7 @@ package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
golog "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// D controls whether we should output debug logs. If true, we do.
|
||||
@@ -63,21 +61,9 @@ func Error(v ...interface{}) { log(err, v...) }
|
||||
// Errorf is equivalent to log.Printf, but prefixed with "[ERROR] ".
|
||||
func Errorf(format string, v ...interface{}) { logf(err, format, v...) }
|
||||
|
||||
// Fatal is equivalent to log.Print, but prefixed with "[FATAL] ", and calling
|
||||
// os.Exit(1).
|
||||
func Fatal(v ...interface{}) { log(fatal, v...); os.Exit(1) }
|
||||
|
||||
// Fatalf is equivalent to log.Printf, but prefixed with "[FATAL] ", and calling
|
||||
// os.Exit(1)
|
||||
func Fatalf(format string, v ...interface{}) { logf(fatal, format, v...); os.Exit(1) }
|
||||
|
||||
// Discard sets the log output to /dev/null.
|
||||
func Discard() { golog.SetOutput(ioutil.Discard) }
|
||||
|
||||
const (
|
||||
debug = "[DEBUG] "
|
||||
err = "[ERROR] "
|
||||
fatal = "[FATAL] "
|
||||
info = "[INFO] "
|
||||
warning = "[WARNING] "
|
||||
info = "[INFO] "
|
||||
)
|
||||
|
||||
-7
@@ -3,7 +3,6 @@ package log
|
||||
import (
|
||||
"fmt"
|
||||
golog "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// P is a logger that includes the plugin doing the logging.
|
||||
@@ -59,10 +58,4 @@ func (p P) Error(v ...interface{}) { p.log(err, v...) }
|
||||
// Errorf logs as log.Errorf.
|
||||
func (p P) Errorf(format string, v ...interface{}) { p.logf(err, format, v...) }
|
||||
|
||||
// Fatal logs as log.Fatal and calls os.Exit(1).
|
||||
func (p P) Fatal(v ...interface{}) { p.log(fatal, v...); os.Exit(1) }
|
||||
|
||||
// Fatalf logs as log.Fatalf and calls os.Exit(1).
|
||||
func (p P) Fatalf(format string, v ...interface{}) { p.logf(fatal, format, v...); os.Exit(1) }
|
||||
|
||||
func pFormat(s string) string { return "plugin/" + s + ": " }
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ ISC License
|
||||
|
||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
|
||||
+90
-97
@@ -16,7 +16,9 @@
|
||||
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// +build !js,!appengine,!safe,!disableunsafe
|
||||
// Go versions prior to 1.4 are disabled because they use a different layout
|
||||
// for interfaces which make the implementation of unsafeReflectValue more complex.
|
||||
// +build !js,!appengine,!safe,!disableunsafe,go1.4
|
||||
|
||||
package spew
|
||||
|
||||
@@ -34,80 +36,49 @@ const (
|
||||
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||
)
|
||||
|
||||
var (
|
||||
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
||||
// internal reflect.Value fields. These values are valid before golang
|
||||
// commit ecccf07e7f9d which changed the format. The are also valid
|
||||
// after commit 82f48826c6c7 which changed the format again to mirror
|
||||
// the original format. Code in the init function updates these offsets
|
||||
// as necessary.
|
||||
offsetPtr = uintptr(ptrSize)
|
||||
offsetScalar = uintptr(0)
|
||||
offsetFlag = uintptr(ptrSize * 2)
|
||||
type flag uintptr
|
||||
|
||||
// flagKindWidth and flagKindShift indicate various bits that the
|
||||
// reflect package uses internally to track kind information.
|
||||
//
|
||||
// flagRO indicates whether or not the value field of a reflect.Value is
|
||||
// read-only.
|
||||
//
|
||||
// flagIndir indicates whether the value field of a reflect.Value is
|
||||
// the actual data or a pointer to the data.
|
||||
//
|
||||
// These values are valid before golang commit 90a7c3c86944 which
|
||||
// changed their positions. Code in the init function updates these
|
||||
// flags as necessary.
|
||||
flagKindWidth = uintptr(5)
|
||||
flagKindShift = uintptr(flagKindWidth - 1)
|
||||
flagRO = uintptr(1 << 0)
|
||||
flagIndir = uintptr(1 << 1)
|
||||
var (
|
||||
// flagRO indicates whether the value field of a reflect.Value
|
||||
// is read-only.
|
||||
flagRO flag
|
||||
|
||||
// flagAddr indicates whether the address of the reflect.Value's
|
||||
// value may be taken.
|
||||
flagAddr flag
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Older versions of reflect.Value stored small integers directly in the
|
||||
// ptr field (which is named val in the older versions). Versions
|
||||
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
||||
// scalar for this purpose which unfortunately came before the flag
|
||||
// field, so the offset of the flag field is different for those
|
||||
// versions.
|
||||
//
|
||||
// This code constructs a new reflect.Value from a known small integer
|
||||
// and checks if the size of the reflect.Value struct indicates it has
|
||||
// the scalar field. When it does, the offsets are updated accordingly.
|
||||
vv := reflect.ValueOf(0xf00)
|
||||
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
||||
offsetScalar = ptrSize * 2
|
||||
offsetFlag = ptrSize * 3
|
||||
}
|
||||
// flagKindMask holds the bits that make up the kind
|
||||
// part of the flags field. In all the supported versions,
|
||||
// it is in the lower 5 bits.
|
||||
const flagKindMask = flag(0x1f)
|
||||
|
||||
// Commit 90a7c3c86944 changed the flag positions such that the low
|
||||
// order bits are the kind. This code extracts the kind from the flags
|
||||
// field and ensures it's the correct type. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are updated
|
||||
// accordingly.
|
||||
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
||||
upfv := *(*uintptr)(upf)
|
||||
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
||||
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
||||
flagKindShift = 0
|
||||
flagRO = 1 << 5
|
||||
flagIndir = 1 << 6
|
||||
// Different versions of Go have used different
|
||||
// bit layouts for the flags type. This table
|
||||
// records the known combinations.
|
||||
var okFlags = []struct {
|
||||
ro, addr flag
|
||||
}{{
|
||||
// From Go 1.4 to 1.5
|
||||
ro: 1 << 5,
|
||||
addr: 1 << 7,
|
||||
}, {
|
||||
// Up to Go tip.
|
||||
ro: 1<<5 | 1<<6,
|
||||
addr: 1 << 8,
|
||||
}}
|
||||
|
||||
// Commit adf9b30e5594 modified the flags to separate the
|
||||
// flagRO flag into two bits which specifies whether or not the
|
||||
// field is embedded. This causes flagIndir to move over a bit
|
||||
// and means that flagRO is the combination of either of the
|
||||
// original flagRO bit and the new bit.
|
||||
//
|
||||
// This code detects the change by extracting what used to be
|
||||
// the indirect bit to ensure it's set. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are
|
||||
// updated accordingly.
|
||||
if upfv&flagIndir == 0 {
|
||||
flagRO = 3 << 5
|
||||
flagIndir = 1 << 7
|
||||
}
|
||||
var flagValOffset = func() uintptr {
|
||||
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||
if !ok {
|
||||
panic("reflect.Value has no flag field")
|
||||
}
|
||||
return field.Offset
|
||||
}()
|
||||
|
||||
// flagField returns a pointer to the flag field of a reflect.Value.
|
||||
func flagField(v *reflect.Value) *flag {
|
||||
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
|
||||
}
|
||||
|
||||
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||
@@ -119,34 +90,56 @@ func init() {
|
||||
// This allows us to check for implementations of the Stringer and error
|
||||
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||
// inaccessible values such as unexported struct fields.
|
||||
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
||||
indirects := 1
|
||||
vt := v.Type()
|
||||
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
||||
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
||||
if rvf&flagIndir != 0 {
|
||||
vt = reflect.PtrTo(v.Type())
|
||||
indirects++
|
||||
} else if offsetScalar != 0 {
|
||||
// The value is in the scalar field when it's not one of the
|
||||
// reference types.
|
||||
switch vt.Kind() {
|
||||
case reflect.Uintptr:
|
||||
case reflect.Chan:
|
||||
case reflect.Func:
|
||||
case reflect.Map:
|
||||
case reflect.Ptr:
|
||||
case reflect.UnsafePointer:
|
||||
default:
|
||||
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
||||
offsetScalar)
|
||||
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
|
||||
return v
|
||||
}
|
||||
flagFieldPtr := flagField(&v)
|
||||
*flagFieldPtr &^= flagRO
|
||||
*flagFieldPtr |= flagAddr
|
||||
return v
|
||||
}
|
||||
|
||||
// Sanity checks against future reflect package changes
|
||||
// to the type or semantics of the Value.flag field.
|
||||
func init() {
|
||||
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
|
||||
if !ok {
|
||||
panic("reflect.Value has no flag field")
|
||||
}
|
||||
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
|
||||
panic("reflect.Value flag field has changed kind")
|
||||
}
|
||||
type t0 int
|
||||
var t struct {
|
||||
A t0
|
||||
// t0 will have flagEmbedRO set.
|
||||
t0
|
||||
// a will have flagStickyRO set
|
||||
a t0
|
||||
}
|
||||
vA := reflect.ValueOf(t).FieldByName("A")
|
||||
va := reflect.ValueOf(t).FieldByName("a")
|
||||
vt0 := reflect.ValueOf(t).FieldByName("t0")
|
||||
|
||||
// Infer flagRO from the difference between the flags
|
||||
// for the (otherwise identical) fields in t.
|
||||
flagPublic := *flagField(&vA)
|
||||
flagWithRO := *flagField(&va) | *flagField(&vt0)
|
||||
flagRO = flagPublic ^ flagWithRO
|
||||
|
||||
// Infer flagAddr from the difference between a value
|
||||
// taken from a pointer and not.
|
||||
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
|
||||
flagNoPtr := *flagField(&vA)
|
||||
flagPtr := *flagField(&vPtrA)
|
||||
flagAddr = flagNoPtr ^ flagPtr
|
||||
|
||||
// Check that the inferred flags tally with one of the known versions.
|
||||
for _, f := range okFlags {
|
||||
if flagRO == f.ro && flagAddr == f.addr {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pv := reflect.NewAt(vt, upv)
|
||||
rv = pv
|
||||
for i := 0; i < indirects; i++ {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
return rv
|
||||
panic("reflect.Value read-only flag has changed semantics")
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// +build js appengine safe disableunsafe
|
||||
// +build js appengine safe disableunsafe !go1.4
|
||||
|
||||
package spew
|
||||
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||
w.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
||||
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
|
||||
// prefix to Writer w.
|
||||
func printHexPtr(w io.Writer, p uintptr) {
|
||||
// Null pointer.
|
||||
|
||||
+5
-5
@@ -35,16 +35,16 @@ var (
|
||||
|
||||
// cCharRE is a regular expression that matches a cgo char.
|
||||
// It is used to detect character arrays to hexdump them.
|
||||
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
||||
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
|
||||
|
||||
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||
// char. It is used to detect unsigned character arrays to hexdump
|
||||
// them.
|
||||
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
||||
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
|
||||
|
||||
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||
// It is used to detect uint8_t arrays to hexdump them.
|
||||
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
||||
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
|
||||
)
|
||||
|
||||
// dumpState contains information about the state of a dump operation.
|
||||
@@ -143,10 +143,10 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||
// Display dereferenced value.
|
||||
d.w.Write(openParenBytes)
|
||||
switch {
|
||||
case nilFound == true:
|
||||
case nilFound:
|
||||
d.w.Write(nilAngleBytes)
|
||||
|
||||
case cycleFound == true:
|
||||
case cycleFound:
|
||||
d.w.Write(circularBytes)
|
||||
|
||||
default:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user