From 2e4818db123035e26721201c32dd88e7bbf723ae Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Thu, 31 May 2018 16:40:21 -0700 Subject: [PATCH] feat: initial transfer of Rust autopush code Issue #1 --- .travis.yml | 6 + Cargo.lock | 2237 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 70 ++ src/client.rs | 1054 ++++++++++++++++++ src/db/commands.rs | 416 +++++++ src/db/macros.rs | 109 ++ src/db/mod.rs | 405 +++++++ src/db/models.rs | 275 +++++ src/db/util.rs | 15 + src/errors.rs | 87 ++ src/http.rs | 83 ++ src/lib.rs | 129 +++ src/logging.rs | 50 + src/main.rs | 53 + src/protocol.rs | 158 +++ src/server/dispatch.rs | 92 ++ src/server/metrics.rs | 23 + src/server/mod.rs | 988 +++++++++++++++++ src/server/tls.rs | 138 +++ src/server/webpush_io.rs | 66 ++ src/settings.rs | 131 +++ src/util/megaphone.rs | 359 ++++++ src/util/mod.rs | 42 + src/util/rc.rs | 53 + src/util/send_all.rs | 91 ++ src/util/timing.rs | 18 + src/util/user_agent.rs | 92 ++ 27 files changed, 7240 insertions(+) create mode 100644 .travis.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/client.rs create mode 100644 src/db/commands.rs create mode 100644 src/db/macros.rs create mode 100644 src/db/mod.rs create mode 100644 src/db/models.rs create mode 100644 src/db/util.rs create mode 100644 src/errors.rs create mode 100644 src/http.rs create mode 100644 src/lib.rs create mode 100644 src/logging.rs create mode 100644 src/main.rs create mode 100644 src/protocol.rs create mode 100644 src/server/dispatch.rs create mode 100644 src/server/metrics.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/tls.rs create mode 100644 src/server/webpush_io.rs create mode 100644 src/settings.rs create mode 100644 src/util/megaphone.rs create mode 100644 src/util/mod.rs create mode 100644 src/util/rc.rs create mode 100644 src/util/send_all.rs create mode 100644 src/util/timing.rs create mode 100644 src/util/user_agent.rs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a2a3d9dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: rust + +rust: + - stable + +cache: cargo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..1ce4ab75 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2237 @@ +[[package]] +name = "adler32" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "aho-corasick" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aho-corasick" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "arrayref" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "arrayvec" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autopush" +version = "0.1.0" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cadence 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chan-signal 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "config 0.8.0 (git+https://github.com/mehcode/config-rs?rev=e8fa9fee96185ddd18ebcef8a925c75459111edb)", + "docopt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", + "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fernet 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-backoff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mozsvc-common 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_credential 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_dynamodb 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sentry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_dynamodb 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-mozlog-json 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-scope 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-stdlog 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-term 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "state_machine_future 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-openssl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tungstenite 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "woothee 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bit-set" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bit-vec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bit-vec" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cadence" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chan" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chan-signal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chan 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chrono" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "config" +version = "0.8.0" +source = "git+https://github.com/mehcode/config-rs?rev=e8fa9fee96185ddd18ebcef8a925c75459111edb#e8fa9fee96185ddd18ebcef8a925c75459111edb" +dependencies = [ + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde-hjson 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "core-foundation" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "core-foundation-sys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crossbeam-deque" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-utils" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crypto-mac" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "darling" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "darling_macro 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "darling_core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ident_case 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "darling_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "derive_state_machine_future" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "darling 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "petgraph 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "docopt" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "encoding_rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "erased-serde" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "error-chain" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "error-chain" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fernet" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fixedbitset" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-backoff" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-timer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "futures-timer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "heck" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "hmac" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crypto-mac 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hostname" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "httparse" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "humantime" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-tls" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ident_case" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "idna" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "input_buffer" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "isatty" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazycell" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libflate" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "linked-hash-map" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_test 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memoffset" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "mime" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mime_guess" +version = "2.0.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mime 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mozsvc-common" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "native-tls" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nom" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl" +version = "0.9.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-sys" +version = "0.9.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ordermap" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "petgraph" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "ordermap 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_codegen" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_shared" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_syscall" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "relay" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rent_to_own" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "reqwest" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding_rs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rusoto_core" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_credential 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rusoto_credential" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rusoto_dynamodb" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "schannel" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "security-framework" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "security-framework-sys" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sentry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde-hjson" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_dynamodb" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rusoto_dynamodb 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_test" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_urlencoded" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sha2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slog" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "erased-serde 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slog-async" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slog-mozlog-json" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slog-scope" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slog-stdlog" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "slog-scope 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "slog-term" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "isatty 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "smallvec" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "state_machine_future" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "derive_state_machine_future 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "rent_to_own 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "take" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread-id" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-fs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-udp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-executor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-fs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-io" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-openssl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-proto" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-service" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-timer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tls" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tungstenite 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-udp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "try-lock" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "tungstenite" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "input_buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ucd-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicase" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-segmentation" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf-8" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "utf8-ranges" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "uuid" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vcpkg" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "version_check" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "want" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wincolor" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winutil" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "woothee" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "xml-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "yaml-rust" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6cbd0b9af8587c72beadc9f72d35b9fbb070982c9e6203e46e93f10df25f8f45" +"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" +"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" +"checksum arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd1479b7c29641adbd35ff3b5c293922d696a92f25c8c975da3e0acbc87258f" +"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef" +"checksum atty 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" +"checksum backtrace 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dbdd17cd962b570302f5297aea8648d5923e22e555c2ed2d8b2e34eca646bf6d" +"checksum backtrace-sys 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "bff67d0c06556c0b8e6b5f090f0eac52d950d9dfd1d35ba04e4ca3543eaf6a7e" +"checksum base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "85415d2594767338a74a30c1d370b2f3262ec1b4ed2d7bba5b3faf4de40467d9" +"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" +"checksum bit-vec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b4ff8b16e6076c3e14220b39fbc1fabb6737522281a388998046859400895f" +"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +"checksum bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" +"checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" +"checksum byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "74c0b906e9446b0a2e4f760cdb3fa4b2c48cdc6db8766a845c54b6ff063fd2e9" +"checksum bytes 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7dd32989a66957d3f0cba6588f15d4281a733f4e9ffc43fcd2385f57d3bf99ff" +"checksum cadence 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f844988bbe2f2a0c67b9982d1788ba800bb1b2910a69141247c94e5667a4a0a0" +"checksum cc 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "49ec142f5768efb5b7622aebc3fdbdbb8950a4b9ba996393cb76ef7466e8747d" +"checksum cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "405216fd8fe65f718daa7102ea808a946b6ce40c742998fbfd3463645552de18" +"checksum chan 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "9af7c487bb99c929ba2715b1a3a7bf45f5062bf5b6eae5d32b292a96c5865172" +"checksum chan-signal 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f1f1e11f6e1c14c9e805a87c622cb8fcb636283b3119a2150af390cc6702d7fe" +"checksum chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum config 0.8.0 (git+https://github.com/mehcode/config-rs?rev=e8fa9fee96185ddd18ebcef8a925c75459111edb)" = "" +"checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" +"checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" +"checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" +"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +"checksum crossbeam 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "bd66663db5a988098a89599d4857919b3acf7f61402e61365acfd3919857b9be" +"checksum crossbeam-deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fe8153ef04a7594ded05b427ffad46ddeaf22e63fd48d42b3e1e3bb4db07cae7" +"checksum crossbeam-epoch 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9b4e2817eb773f770dcb294127c011e22771899c21d18fce7dd739c0b9832e81" +"checksum crossbeam-utils 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d636a8b3bcc1b409d7ffd3facef8f21dcb4009626adbd0c5e6c4305c07253c7b" +"checksum crypto-mac 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0999b4ff4d3446d4ddb19a63e9e00c1876e75cd7000d20e57a693b4b3f08d958" +"checksum darling 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1630fdbe3554154a50624487c79b0140a424e87dc08061db1a2211359792acab" +"checksum darling_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d12d2eeb837786ace70b6bca9adfeaef4352cc68d6a42e8e3d0c4159bbca7ab2" +"checksum darling_macro 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "01581bdeabb86f69970dbd9e6ee3c61963f9a7321169589e3dffa16033c0928c" +"checksum derive_state_machine_future 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54e84dd4e2e6b94edda02aaae8fd8d02f68404817c89183e16d217bb380d08e8" +"checksum digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "00a49051fef47a72c9623101b19bd71924a45cca838826caae3eaa4d00772603" +"checksum docopt 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e67fb750c36fc6fffbd3575cf8f2b46790fc0b05096ae3c03a36cf71b55e1e2b" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" +"checksum encoding_rs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98fd0f24d1fb71a4a6b9330c8ca04cbd4e7cc5d846b54ca74ff376bc7c9f798d" +"checksum env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0e6e40ebb0e66918a37b38c7acab4e10d299e0463fe2af5d29b9cc86710cfd2a" +"checksum erased-serde 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c564e32677839f1c551664c478e079c9b128a1a2d223180bffb2ddfabeded0be" +"checksum error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" +"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum fernet 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b001fae1d5ef9a63cb117462e0c6d76eca42891db7d6140ed12e8b1792277028" +"checksum fixedbitset 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" +"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "1a70b146671de62ec8c8ed572219ca5d594d9b06c0b364d5e67b722fc559b48c" +"checksum futures-backoff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "79f590345ee8cedf1f36068db74ca81c7e7bf753a3e59ea58b0826243a13971b" +"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum futures-timer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a5cedfe9b6dc756220782cc1ba5bcb1fa091cdcba155e40d3556159c3db58043" +"checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea04fa3ead4e05e51a7c806fc07271fdbde4e246a6c6d1efd52e72230b771b82" +"checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" +"checksum hmac 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44f3bdb08579d99d7dc761c0e266f13b5f2ab8c8c703b9fc9ef333cd8f48f55e" +"checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" +"checksum httparse 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c2f407128745b78abc95c0ffbe4e5d37427fdc0d45470710cfef8c44522a2e37" +"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e" +"checksum hyper 0.11.27 (registry+https://github.com/rust-lang/crates.io-index)" = "34a590ca09d341e94cddf8e5af0bbccde205d5fbc2fa3c09dd67c7f85cea59d7" +"checksum hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a5aa51f6ae9842239b0fac14af5f22123b8432b4cc774a44ff059fcba0f675ca" +"checksum ident_case 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9826188e666f2ed92071d2dadef6edc430b11b158b5b2b3f4babbcc891eaaa" +"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum input_buffer 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "64fc52dd2f15e7ce28663e4eada58f457aa8c220044d531c3b8d56a8781af9b1" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum isatty 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6c324313540cd4d7ba008d43dc6606a32a5579f13cc17b2804c13096f0a5c522" +"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" +"checksum lazy_static 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e6412c5e2ad9584b0b8e979393122026cdd6d2a80b933f890dcd694ddbe73739" +"checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef" +"checksum libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)" = "ac8ebf8343a981e2fa97042b14768f02ed3e1d602eac06cae6166df3c8ced206" +"checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e" +"checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" +"checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" +"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2" +"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" +"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +"checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" +"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" +"checksum mime 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0b28683d0b09bbc20be1c9b3f6f24854efb1356ffcffee08ea3f6e65596e85fa" +"checksum mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130ea3c9c1b65dba905ab5a4d9ac59234a9585c24d135f264e187fe7336febbd" +"checksum mio 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "6d771e3ef92d58a8da8df7d6976bfca9371ed1de6619d9d5a5ce5b1f29b85bfe" +"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum mozsvc-common 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "732386d576b01d7e011eedcf3d6373322ee4699bf5c46585ef416959a7f567fa" +"checksum native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f74dbadc8b43df7864539cedb7bc91345e532fdd913cfdc23ad94f4d2d40fbc0" +"checksum net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "9044faf1413a1057267be51b5afba8eb1090bd2231c693664aa1db716fe1eae0" +"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" +"checksum nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" +"checksum num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac0ea58d64a89d9d6b7688031b3be9358d6c919badcf7fbb0527ccfd891ee45" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +"checksum num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "775393e285254d2f5004596d69bb8bc1149754570dcc08cf30cabeba67955e28" +"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30" +"checksum openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)" = "736898acffb0e00a14d86c5b836aee2ca1c502efcf1c1b0d17a936dfc49ec47f" +"checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985" +"checksum openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)" = "a4d6a27d108b29befe1822d40e2e22f85518dac59acbf7f30fdc532f48fd0a77" +"checksum ordermap 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum petgraph 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8b30dc85588cd02b9b76f5e386535db546d21dc68506cff2abebee0b6445e8e4" +"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2" +"checksum phf_codegen 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "4e4048fe7dd7a06b8127ecd6d3803149126e9b33c7558879846da3a63f734f2b" +"checksum phf_generator 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "05a079dd052e7b674d21cb31cbb6c05efd56a2cd2827db7692e2f1a507ebd998" +"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930" +"checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f" +"checksum proc-macro2 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1fa93823f53cfd0f5ac117b189aed6cfdfb2cfc0a9d82e956dd7927595ed7d46" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" +"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" +"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" +"checksum rand 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a89abf8d34faf9783692392dca7bcdc6e82fa84eca86ccb6301ec87f3497185" +"checksum rand_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1b7a5f27547c49e5ccf8a586db3f3782fd93cf849780b21853b9d981db203302" +"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" +"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +"checksum regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75ecf88252dce580404a22444fc7d626c01815debba56a7f4f536772a5ff19d3" +"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" +"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +"checksum regex-syntax 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8f1ac0f60d675cc6cf13a20ec076568254472551051ad5dd050364d70671bf6b" +"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" +"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" +"checksum rent_to_own 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05a51ad2b1c5c710fa89e6b1631068dab84ed687bc6a5fe061ad65da3d0c25b2" +"checksum reqwest 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2abe46f8e00792693a2488e296c593d1f4ea39bb1178cfce081d6793657575e4" +"checksum rusoto_core 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "12daaa6d62d64f6447bf0299ce775f4e05f8e75e5418e817da094b9de04ad22d" +"checksum rusoto_credential 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "53199d09fd1b7d4f5ac50f4d23106577624238ea77cae2b44eb1d1fc4cd956a4" +"checksum rusoto_dynamodb 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "221fb3362d86a9e6a064cf5f71044cb1cc67a43e7d151008d9ca2a899104c39a" +"checksum rustc-demangle 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "76d7ba1feafada44f2d38eed812bd2489a03c0f5abb975799251518b68848649" +"checksum rustc_version 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a54aa04a10c68c1c4eacb4337fd883b435997ede17a9385784b990777686b09a" +"checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" +"checksum schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "85fd9df495640643ad2d00443b3d78aae69802ad488debab4f1dd52fc1806ade" +"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" +"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" +"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332" +"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum sentry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c931969579f133c35280ccc1969a4786984449bd8adad937ef9f76cef3bdfbc" +"checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" +"checksum serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)" = "fba5be06346c5200249c8c8ca4ccba4a09e8747c71c16e420bd359a0db4d8f91" +"checksum serde-hjson 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a2376ebb8976138927f48b49588ef73cde2f6591b8b3df22f4063e0f27b9bec" +"checksum serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)" = "79e4620ba6fbe051fc7506fab6f84205823564d55da18d55b695160fb3479cd8" +"checksum serde_dynamodb 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe982f1146e7134af153b2d1fdcab083f09c184600b232cd7a120ec191a4e1b" +"checksum serde_json 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "93aee34bb692dde91e602871bc792dd319e489c7308cdbbe5f27cf27c64280f5" +"checksum serde_test 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" +"checksum serde_urlencoded 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e703cef904312097cfceab9ce131ff6bbe09e8c964a0703345a5f49238757bc1" +"checksum sha1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "933ed2cffa70bb0e1a2c1bf1174d0f39dd3b81bbf5597d882d886710c8729924" +"checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" +"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" +"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" +"checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d" +"checksum slog 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2f7bfce6405155042d42ec0e645efe43eddedd7be280063ce0623b120014e7f9" +"checksum slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e544d16c6b230d84c866662fe55e31aacfca6ae71e6fc49ae9a311cb379bfc2f" +"checksum slog-mozlog-json 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f400f1c5db96f1f52065e8931ca0c524cceb029f7537c9e6d5424488ca137ca0" +"checksum slog-scope 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "053344c94c0e2b22da6305efddb698d7c485809427cf40555dc936085f67a9df" +"checksum slog-stdlog 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ac42f8254ae996cc7d640f9410d3b048dcdf8887a10df4d5d4c44966de24c4a8" +"checksum slog-term 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5951a808c40f419922ee014c15b6ae1cd34d963538b57d8a4778b9ca3fff1e0b" +"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013" +"checksum state_machine_future 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eaafbb574dda413e09727f3a534af6837756c9edb69691c120a3240fa30179da" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum syn 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6dfd71b2be5a58ee30a6f8ea355ba8290d397131c00dfa55c3d34e6e13db5101" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" +"checksum take_mut 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +"checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561" +"checksum termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "adc4587ead41bf016f11af03e55a624c06568b5a19db4e90fde573d805074f83" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" +"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" +"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" +"checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" +"checksum tokio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d00555353b013e170ed8bc4e13f648a317d1fd12157dbcae13f7013f6cf29f5" +"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71" +"checksum tokio-executor 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cac2a7883ff3567e9d66bb09100d09b33d90311feca0206c7ca034bc0c55113" +"checksum tokio-fs 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "76766830bbf9a2d5bfb50c95350d56a2e79e2c80f675967fff448bc615899708" +"checksum tokio-io 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6af9eb326f64b2d6b68438e1953341e00ab3cf54de7e35d92bfc73af8555313a" +"checksum tokio-openssl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7e88cd8a247335be936e713ca68a1cb5227df649e22e975b9a71b4e862169e82" +"checksum tokio-proto 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fbb47ae81353c63c487030659494b295f6cb6576242f907f203473b191b0389" +"checksum tokio-reactor 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3cedc8e5af5131dc3423ffa4f877cce78ad25259a9a62de0613735a13ebc64b" +"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" +"checksum tokio-tcp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ec9b094851aadd2caf83ba3ad8e8c4ce65a42104f7b94d9e6550023f0407853f" +"checksum tokio-threadpool 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5783254b10c7c84a56f62c74766ef7e5b83d1f13053218c7cab8d3f2c826fa0e" +"checksum tokio-timer 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535fed0ccee189f3d48447587697ba3fd234b3dbbb091f0ec4613ddfec0a7c4c" +"checksum tokio-tls 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "772f4b04e560117fe3b0a53e490c16ddc8ba6ec437015d91fa385564996ed913" +"checksum tokio-tungstenite 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3cedf5e2d459171cb08aa6126572a06d827de4208d35281a4cc98081182d5d1a" +"checksum tokio-udp 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "137bda266504893ac4774e0ec4c2108f7ccdbcb7ac8dced6305fe9e4e0b5041a" +"checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" +"checksum try-lock 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee2aa4715743892880f70885373966c83d73ef1b0838a664ef0c76fffd35e7c2" +"checksum tungstenite 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b3904357c86319d331cf9430bc7379a669f1bde1e20be51115c0fc96c0b9c9de" +"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" +"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d" +"checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +"checksum unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284b6d3db520d67fbe88fd778c21510d1b0ba4a551e5d0fbb023d33405f6de8a" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25" +"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7" +"checksum utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1262dfab4c30d5cb7c07026be00ee343a6cf5027fdc0104a9160f354e5db75c" +"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" +"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" +"checksum uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" +"checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380" +"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum want 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a05d9d966753fa4b5c8db73fcab5eed4549cfe0e1e4e66911e5564a0085c35d1" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767" +"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" +"checksum woothee 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e7c2cece51be2a2f25518a9efdd303d5ca8dfa619272f091e7dedbba95d1873" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c1cb601d29fe2c2ac60a2b2e5e293994d87a1f6fa9687a31a15270f909be9c2" +"checksum yaml-rust 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57ab38ee1a4a266ed033496cf9af1828d8d6e6c1cfa5f643a2809effcae4d628" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..a5464363 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "autopush" +version = "0.1.0" +authors = [ + "Ben Bangert ", + "JR Conlin ", + "Alex Crichton ", + "Phil Jenvey ", +] + +[lib] +name = "autopush" + +[[bin]] +name = "autopush_rs" +path = "src/main.rs" + +[dependencies] +base64 = "0.9.1" +bytes = "0.4.8" +cadence = "0.14.0" +chan-signal = "0.3.1" +chrono = "0.4.2" +docopt = "1.0.0" +env_logger = { version = "0.5.10", default-features = false } +error-chain = "0.11.0" +fernet = "0.1.0" +futures = "0.1.21" +futures-backoff = "0.1.0" +hex = "0.3.2" +httparse = "1.2.4" +hyper = "0.11.27" +lazy_static = "1.0.1" +libc = "0.2.41" +log = { version = "0.4.1", features = ["max_level_info", "release_max_level_info"] } +matches = "0.1.6" +mozsvc-common = "0.1.0" +openssl = "0.10.8" +rand = "0.5.0" +regex = "1.0.0" +reqwest = { version = "0.8.5", features = ["unstable"] } +rusoto_core = "0.32.0" +rusoto_credential = "0.11.0" +rusoto_dynamodb = "0.32.0" +sentry = "0.2.0" +serde = "1.0.63" +serde_derive = "1.0.63" +serde_dynamodb = "0.1.2" +serde_json = "1.0.18" +slog = { version = "2.2.3" , features = ["max_level_trace", "release_max_level_info"] } +slog-async = "2.3.0" +slog-term = "2.4.0" +slog-mozlog-json = "0.1" +slog-scope = "4.0.1" +slog-stdlog = "3.0.2" +# state_machine_future = { version = "0.1.6", features = ["debug_code_generation"] } +state_machine_future = "0.1.6" +time = "0.1.40" +tokio-core = "0.1.17" +tokio-io = "0.1.6" +tokio-openssl = "0.2.0" +tokio-service = "0.1.0" +tokio-tungstenite = { version = "0.5.1", default-features = false } +tungstenite = { version = "0.5.3", default-features = false } +uuid = { version = "0.6.5", features = ["serde", "v4"] } +woothee = "0.7.3" + +[dependencies.config] +git = "https://github.com/mehcode/config-rs" +rev = "e8fa9fee96185ddd18ebcef8a925c75459111edb" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 00000000..7b560075 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,1054 @@ +//! Management of connected clients to a WebPush server +//! +//! This module is a pretty heavy work in progress. The intention is that +//! this'll house all the various state machine transitions and state management +//! of connected clients. Note that it's expected there'll be a lot of connected +//! clients, so this may appears relatively heavily optimized! +use std::cell::RefCell; +use std::mem; +use std::rc::Rc; + +use cadence::prelude::*; +use futures::AsyncSink; +use futures::future::Either; +use futures::sync::mpsc; +use futures::sync::oneshot::Receiver; +use futures::{Async, Future, Poll, Sink, Stream}; +use rusoto_dynamodb::UpdateItemOutput; +use state_machine_future::RentToOwn; +use tokio_core::reactor::Timeout; +use uuid::Uuid; +use woothee::parser::Parser; + +use errors::*; +use protocol::{ClientMessage, Notification, ServerMessage, ServerNotification}; +use server::Server; +use db::{CheckStorageResponse, HelloResponse, RegisterResponse}; +use util::megaphone::{ClientServices, Service, ServiceClientInit}; +use util::{ms_since_epoch, parse_user_agent, sec_since_epoch}; + +// Created and handed to the AutopushServer +pub struct RegisteredClient { + pub uaid: Uuid, + pub uid: Uuid, + pub tx: mpsc::UnboundedSender, +} + +pub struct Client + where + T: Stream + + Sink + + 'static, +{ + state_machine: UnAuthClientStateFuture, + srv: Rc, + broadcast_services: Rc>, + tx: mpsc::UnboundedSender, +} + +impl Client +where + T: Stream + + Sink + + 'static, +{ + /// Spins up a new client communicating over the websocket `ws` specified. + /// + /// The `ws` specified already has ping/pong parts of the websocket + /// protocol managed elsewhere, and this struct is only expected to deal + /// with webpush-specific messages. + /// + /// The `srv` argument is the server that this client is attached to and + /// the various state behind the server. This provides transitive access to + /// various configuration options of the server as well as the ability to + /// call back into Python. + pub fn new(ws: T, srv: &Rc, mut uarx: Receiver, host: String) -> Client { + let srv = srv.clone(); + let timeout = Timeout::new(srv.opts.open_handshake_timeout.unwrap(), &srv.handle).unwrap(); + let (tx, rx) = mpsc::unbounded(); + + // Pull out the user-agent, which we should have by now + let uastr = match uarx.poll() { + Ok(Async::Ready(ua)) => ua, + Ok(Async::NotReady) => { + error!("Failed to parse the user-agent"); + String::from("") + } + Err(_) => { + error!("Failed to receive a value"); + String::from("") + } + }; + + let broadcast_services = Rc::new(RefCell::new(Default::default())); + let sm = UnAuthClientState::start( + UnAuthClientData { + srv: srv.clone(), + ws, + user_agent: uastr, + host, + broadcast_services: broadcast_services.clone(), + }, + timeout, + tx.clone(), + rx, + ); + + Self { + state_machine: sm, + srv: srv.clone(), + broadcast_services, + tx, + } + } + + pub fn broadcast_delta(&mut self) -> Option> { + let mut broadcast_services = self.broadcast_services.borrow_mut(); + self.srv.broadcast_delta(&mut broadcast_services) + } + + pub fn shutdown(&mut self) { + let _result = self.tx.unbounded_send(ServerNotification::Disconnect); + } +} + +impl Future for Client +where + T: Stream + + Sink + + 'static, +{ + type Item = (); + type Error = Error; + + fn poll(&mut self) -> Poll<(), Error> { + self.state_machine.poll() + } +} + +// Websocket session statistics +#[derive(Clone, Default)] +struct SessionStatistics { + // User data + uaid: String, + uaid_reset: bool, + existing_uaid: bool, + connection_type: String, + host: String, + + // Usage data + direct_acked: i32, + direct_storage: i32, + stored_retrieved: i32, + stored_acked: i32, + nacks: i32, + unregisters: i32, + registers: i32, +} + +// Represent the state for a valid WebPush client that is authenticated +pub struct WebPushClient { + uaid: Uuid, + uid: Uuid, + rx: mpsc::UnboundedReceiver, + flags: ClientFlags, + message_month: String, + unacked_direct_notifs: Vec, + unacked_stored_notifs: Vec, + // Highest version from stored, retained for use with increment + // when all the unacked storeds are ack'd + unacked_stored_highest: Option, + connected_at: u64, + stats: SessionStatistics, +} + +impl Default for WebPushClient { + fn default() -> Self { + let (_, rx) = mpsc::unbounded(); + Self { + uaid: Default::default(), + uid: Default::default(), + rx, + flags: Default::default(), + message_month: Default::default(), + unacked_direct_notifs: Default::default(), + unacked_stored_notifs: Default::default(), + unacked_stored_highest: Default::default(), + connected_at: Default::default(), + stats: Default::default(), + } + } +} + +impl WebPushClient { + fn unacked_messages(&self) -> bool { + !self.unacked_stored_notifs.is_empty() || !self.unacked_direct_notifs.is_empty() + } +} + +#[derive(Default)] +pub struct ClientFlags { + include_topic: bool, + increment_storage: bool, + check: bool, + reset_uaid: bool, + rotate_message_table: bool, +} + +impl ClientFlags { + fn new() -> Self { + Self { + include_topic: true, + increment_storage: false, + check: false, + reset_uaid: false, + rotate_message_table: false, + } + } +} + +pub struct UnAuthClientData { + srv: Rc, + ws: T, + user_agent: String, + host: String, + broadcast_services: Rc>, +} + +impl UnAuthClientData +where + T: Stream + + Sink + + 'static, +{ + fn input_with_timeout(&mut self, timeout: &mut Timeout) -> Poll { + let item = match timeout.poll()? { + Async::Ready(_) => return Err("Client timed out".into()), + Async::NotReady => match self.ws.poll()? { + Async::Ready(None) => return Err("Client dropped".into()), + Async::Ready(Some(msg)) => Async::Ready(msg), + Async::NotReady => Async::NotReady, + }, + }; + Ok(item) + } +} + +pub struct AuthClientData { + srv: Rc, + ws: T, + webpush: Rc>, + broadcast_services: Rc>, +} + +impl AuthClientData +where + T: Stream + + Sink + + 'static, +{ + fn input_or_notif(&mut self) -> Poll, Error> { + let mut webpush = self.webpush.borrow_mut(); + let item = match webpush.rx.poll() { + Ok(Async::Ready(Some(notif))) => Either::B(notif), + Ok(Async::Ready(None)) => return Err("Sending side dropped".into()), + Ok(Async::NotReady) => match self.ws.poll()? { + Async::Ready(None) => return Err("Client dropped".into()), + Async::Ready(Some(msg)) => Either::A(msg), + Async::NotReady => return Ok(Async::NotReady), + }, + Err(_) => return Err("Unexpected error".into()), + }; + Ok(Async::Ready(item)) + } +} + +#[derive(StateMachineFuture)] +pub enum UnAuthClientState +where + T: Stream + + Sink + + 'static, +{ + #[state_machine_future(start, transitions(AwaitProcessHello))] + AwaitHello { + data: UnAuthClientData, + timeout: Timeout, + tx: mpsc::UnboundedSender, + rx: mpsc::UnboundedReceiver, + }, + + #[state_machine_future(transitions(AwaitSessionComplete))] + AwaitProcessHello { + response: MyFuture, + data: UnAuthClientData, + interested_broadcasts: Vec, + tx: mpsc::UnboundedSender, + rx: mpsc::UnboundedReceiver, + }, + + #[state_machine_future(transitions(UnAuthDone))] + AwaitSessionComplete { + auth_state_machine: AuthClientStateFuture, + srv: Rc, + user_agent: String, + webpush: Rc>, + }, + + #[state_machine_future(ready)] + UnAuthDone(()), + + #[state_machine_future(error)] + UnAuthClientStateError(Error), +} + +impl PollUnAuthClientState for UnAuthClientState +where + T: Stream + + Sink + + 'static, +{ + fn poll_await_hello<'a>( + hello: &'a mut RentToOwn<'a, AwaitHello>, + ) -> Poll, Error> { + trace!("State: AwaitHello"); + let (uaid, services) = { + let AwaitHello { + ref mut data, + ref mut timeout, + .. + } = **hello; + match try_ready!(data.input_with_timeout(timeout)) { + ClientMessage::Hello { + uaid, + use_webpush: Some(true), + broadcasts, + .. + } => ( + uaid.and_then(|uaid| Uuid::parse_str(uaid.as_str()).ok()), + Service::from_hashmap(broadcasts.unwrap_or_default()), + ), + _ => return Err("Invalid message, must be hello".into()), + } + }; + + let AwaitHello { data, tx, rx, .. } = hello.take(); + let connected_at = ms_since_epoch(); + let response = Box::new(data.srv.ddb.hello( + &connected_at, + uaid.as_ref(), + &data.srv.opts.router_table_name, + &data.srv.opts.router_url, + &data.srv.opts.message_table_names, + &data.srv.opts.current_message_month, + &data.srv.metrics, + )); + transition!(AwaitProcessHello { + response, + data, + interested_broadcasts: services, + tx, + rx, + }) + } + + fn poll_await_process_hello<'a>( + process_hello: &'a mut RentToOwn<'a, AwaitProcessHello>, + ) -> Poll, Error> { + trace!("State: AwaitProcessHello"); + let (uaid, message_month, check_storage, reset_uaid, rotate_message_table, connected_at) = { + match try_ready!(process_hello.response.poll()) { + HelloResponse { + uaid: Some(uaid), + message_month, + check_storage, + reset_uaid, + rotate_message_table, + connected_at, + } => ( + uaid, + message_month, + check_storage, + reset_uaid, + rotate_message_table, + connected_at, + ), + HelloResponse { uaid: None, .. } => { + return Err("Already connected elsewhere".into()) + } + } + }; + + let AwaitProcessHello { + data, + interested_broadcasts, + tx, + rx, + .. + } = process_hello.take(); + data.srv.metrics.incr("ua.command.hello").ok(); + + let UnAuthClientData { + srv, + ws, + user_agent, + host, + broadcast_services, + } = data; + + // Setup the objects and such needed for a WebPushClient + let mut flags = ClientFlags::new(); + flags.check = check_storage; + flags.reset_uaid = reset_uaid; + flags.rotate_message_table = rotate_message_table; + let ServiceClientInit(client_services, broadcasts) = + srv.broadcast_init(&interested_broadcasts); + broadcast_services.replace(client_services); + let uid = Uuid::new_v4(); + let webpush = Rc::new(RefCell::new(WebPushClient { + uaid, + uid: uid, + flags, + rx, + message_month, + connected_at, + stats: SessionStatistics { + uaid: uaid.simple().to_string(), + uaid_reset: reset_uaid, + existing_uaid: check_storage, + connection_type: String::from("webpush"), + host: host.clone(), + ..Default::default() + }, + ..Default::default() + })); + srv.connect_client(RegisteredClient { uaid, uid, tx }); + + let response = ServerMessage::Hello { + uaid: uaid.simple().to_string(), + status: 200, + use_webpush: Some(true), + broadcasts: Service::into_hashmap(broadcasts), + }; + let auth_state_machine = AuthClientState::start( + vec![response], + false, + AuthClientData { + srv: srv.clone(), + ws, + webpush: webpush.clone(), + broadcast_services: broadcast_services.clone(), + }, + ); + transition!(AwaitSessionComplete { + auth_state_machine, + srv, + user_agent, + webpush, + }) + } + + fn poll_await_session_complete<'a>( + session_complete: &'a mut RentToOwn<'a, AwaitSessionComplete>, + ) -> Poll { + // xxx: handle error cases with maybe a log message? + let _error = { + match session_complete.auth_state_machine.poll() { + Ok(Async::Ready(_)) => None, + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(e) => Some(e), + } + }; + + let AwaitSessionComplete { + srv, + user_agent, + webpush, + .. + } = session_complete.take(); + let mut webpush = webpush.borrow_mut(); + // If there's any notifications in the queue, move them to our unacked direct notifs + webpush.rx.close(); + loop { + match webpush.rx.poll() { + Ok(Async::Ready(Some(msg))) => match msg { + ServerNotification::CheckStorage | ServerNotification::Disconnect => continue, + ServerNotification::Notification(notif) => { + webpush.unacked_direct_notifs.push(notif) + } + }, + Ok(Async::Ready(None)) | Ok(Async::NotReady) | Err(_) => break, + } + } + let now = ms_since_epoch(); + let elapsed = (now - webpush.connected_at) / 1_000; + let parser = Parser::new(); + let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&parser, &user_agent); + srv.metrics + .time_with_tags("ua.connection.lifespan", elapsed) + .with_tag("ua_os_family", metrics_os) + .with_tag("ua_browser_family", metrics_browser) + .with_tag("host", &webpush.stats.host) + .send(); + + // If there's direct unack'd messages, they need to be saved out without blocking + // here + srv.disconnet_client(&webpush.uaid, &webpush.uid); + let mut stats = webpush.stats.clone(); + let unacked_direct_notifs = webpush.unacked_direct_notifs.len(); + if unacked_direct_notifs > 0 { + debug!("Writing direct notifications to storage"); + stats.direct_storage += unacked_direct_notifs as i32; + let mut notifs = mem::replace(&mut webpush.unacked_direct_notifs, Vec::new()); + // Ensure we don't store these as legacy by setting a 0 as the sortkey_timestamp + // That will ensure the Python side doesn't mark it as legacy during conversion and + // still get the correct default us_time when saving. + for notif in &mut notifs { + notif.sortkey_timestamp = Some(0); + } + srv.handle.spawn( + srv.ddb + .store_messages(&webpush.uaid, &webpush.message_month, notifs) + .then(|_| { + debug!("Finished saving unacked direct notifications"); + Ok(()) + }), + ); + } + + // Log out the final stats message + info!("Session"; + "uaid_hash" => &stats.uaid, + "uaid_reset" => stats.uaid_reset, + "existing_uaid" => stats.existing_uaid, + "connection_type" => &stats.connection_type, + "host" => &stats.host, + "ua_name" => ua_result.name, + "ua_os_family" => ua_result.os, + "ua_os_ver" => ua_result.os_version, + "ua_browser_family" => ua_result.vendor, + "ua_browser_ver" => ua_result.version, + "ua_category" => ua_result.category, + "connection_time" => elapsed, + "direct_acked" => stats.direct_acked, + "direct_storage" => stats.direct_storage, + "stored_retrieved" => stats.stored_retrieved, + "stored_acked" => stats.stored_acked, + "nacks" => stats.nacks, + "registers" => stats.registers, + "unregisters" => stats.unregisters, + ); + transition!(UnAuthDone(())) + } +} + +#[derive(StateMachineFuture)] +pub enum AuthClientState +where + T: Stream + + Sink + + 'static, +{ + #[state_machine_future(start, transitions(DetermineAck, SendThenWait))] + SendThenWait { + remaining_data: Vec, + poll_complete: bool, + data: AuthClientData, + }, + + #[state_machine_future(transitions(IncrementStorage, CheckStorage, AwaitDropUser, + AwaitMigrateUser, AwaitInput))] + DetermineAck { data: AuthClientData }, + + #[state_machine_future(transitions(DetermineAck, SendThenWait, AwaitInput, AwaitRegister, + AwaitUnregister, AwaitDelete))] + AwaitInput { data: AuthClientData }, + + #[state_machine_future(transitions(AwaitIncrementStorage))] + IncrementStorage { data: AuthClientData }, + + #[state_machine_future(transitions(DetermineAck))] + AwaitIncrementStorage { + response: MyFuture, + data: AuthClientData, + }, + + #[state_machine_future(transitions(AwaitCheckStorage))] + CheckStorage { data: AuthClientData }, + + #[state_machine_future(transitions(SendThenWait, DetermineAck))] + AwaitCheckStorage { + response: MyFuture, + data: AuthClientData, + }, + + #[state_machine_future(transitions(DetermineAck))] + AwaitMigrateUser { + response: MyFuture<()>, + data: AuthClientData, + }, + + #[state_machine_future(transitions(AuthDone))] + AwaitDropUser { + response: MyFuture<()>, + data: AuthClientData, + }, + + #[state_machine_future(transitions(SendThenWait))] + AwaitRegister { + channel_id: Uuid, + response: MyFuture, + data: AuthClientData, + }, + + #[state_machine_future(transitions(SendThenWait))] + AwaitUnregister { + channel_id: Uuid, + code: u32, + response: MyFuture, + data: AuthClientData, + }, + + #[state_machine_future(transitions(DetermineAck))] + AwaitDelete { + response: MyFuture<()>, + data: AuthClientData, + }, + + #[state_machine_future(ready)] + AuthDone(()), + + #[state_machine_future(error)] + AuthClientStateError(Error), +} + +impl PollAuthClientState for AuthClientState +where + T: Stream + + Sink + + 'static, +{ + fn poll_send_then_wait<'a>( + send: &'a mut RentToOwn<'a, SendThenWait>, + ) -> Poll, Error> { + trace!("State: SendThenWait"); + let start_send = { + let SendThenWait { + ref mut remaining_data, + poll_complete, + ref mut data, + .. + } = **send; + if poll_complete { + try_ready!(data.ws.poll_complete()); + false + } else if !remaining_data.is_empty() { + let item = remaining_data.remove(0); + let ret = data.ws.start_send(item).chain_err(|| "unable to send")?; + match ret { + AsyncSink::Ready => true, + AsyncSink::NotReady(returned) => { + remaining_data.insert(0, returned); + return Ok(Async::NotReady); + } + } + } else { + false + } + }; + + let SendThenWait { + data, + remaining_data, + .. + } = send.take(); + if start_send { + transition!(SendThenWait { + remaining_data, + poll_complete: true, + data, + }); + } else if !remaining_data.is_empty() { + transition!(SendThenWait { + remaining_data, + poll_complete: false, + data, + }); + } + transition!(DetermineAck { data }) + } + + fn poll_determine_ack<'a>( + detack: &'a mut RentToOwn<'a, DetermineAck>, + ) -> Poll, Error> { + let DetermineAck { data } = detack.take(); + let webpush_rc = data.webpush.clone(); + let webpush = webpush_rc.borrow(); + let all_acked = !webpush.unacked_messages(); + if all_acked && webpush.flags.check && webpush.flags.increment_storage { + transition!(IncrementStorage { data }); + } else if all_acked && webpush.flags.check { + transition!(CheckStorage { data }); + } else if all_acked && webpush.flags.rotate_message_table { + debug!("Triggering migration"); + let response = Box::new(data.srv.ddb.migrate_user( + &webpush.uaid, + &webpush.message_month, + &data.srv.opts.current_message_month, + &data.srv.opts.router_table_name, + )); + transition!(AwaitMigrateUser { response, data }); + } else if all_acked && webpush.flags.reset_uaid { + let response = Box::new( + data.srv + .ddb + .drop_uaid(&data.srv.opts.router_table_name, &webpush.uaid) + ); + transition!(AwaitDropUser { response, data }); + } + transition!(AwaitInput { data }) + } + + fn poll_await_input<'a>( + await: &'a mut RentToOwn<'a, AwaitInput>, + ) -> Poll, Error> { + trace!("State: AwaitInput"); + let input = try_ready!(await.data.input_or_notif()); + let AwaitInput { data } = await.take(); + let webpush_rc = data.webpush.clone(); + let mut webpush = webpush_rc.borrow_mut(); + match input { + Either::A(ClientMessage::BroadcastSubscribe { broadcasts }) => { + let service_delta = { + let mut broadcast_services = data.broadcast_services.borrow_mut(); + data.srv.client_service_add_service( + &mut broadcast_services, + &Service::from_hashmap(broadcasts), + ) + }; + if let Some(delta) = service_delta { + transition!(SendThenWait { + remaining_data: vec![ + ServerMessage::Broadcast { + broadcasts: Service::into_hashmap(delta), + }, + ], + poll_complete: false, + data, + }); + } else { + transition!(AwaitInput { data }); + } + } + Either::A(ClientMessage::Register { + channel_id: channel_id_str, + key, + }) => { + debug!("Got a register command"; + "channel_id" => &channel_id_str); + let channel_id = + Uuid::parse_str(&channel_id_str).chain_err(|| "Invalid channelID")?; + if channel_id.hyphenated().to_string() != channel_id_str { + return Err("Bad UUID format, use lower case, dashed format".into()); + } + + let uaid = webpush.uaid; + let message_month = webpush.message_month.clone(); + let srv = data.srv.clone(); + let fut = data.srv + .ddb + .register(&srv, &uaid, &channel_id, &message_month, key); + transition!(AwaitRegister { + channel_id, + response: fut, + data, + }); + } + Either::A(ClientMessage::Unregister { channel_id, code }) => { + debug!("Got a unregister command"); + // XXX: unregister should check the format of channel_id like + // register does + let uaid = webpush.uaid; + let message_month = webpush.message_month.clone(); + let response = + Box::new(data.srv.ddb.unregister(&uaid, &channel_id, &message_month)); + transition!(AwaitUnregister { + channel_id, + code: code.unwrap_or(200), + response, + data, + }); + } + Either::A(ClientMessage::Nack { .. }) => { + data.srv.metrics.incr("ua.command.nack").ok(); + webpush.stats.nacks += 1; + transition!(AwaitInput { data }); + } + Either::A(ClientMessage::Ack { updates }) => { + data.srv.metrics.incr("ua.command.ack").ok(); + let mut fut: Option> = None; + for notif in &updates { + if let Some(pos) = webpush.unacked_direct_notifs.iter().position(|v| { + v.channel_id == notif.channel_id && v.version == notif.version + }) { + webpush.stats.direct_acked += 1; + webpush.unacked_direct_notifs.remove(pos); + continue; + }; + if let Some(pos) = webpush.unacked_stored_notifs.iter().position(|v| { + v.channel_id == notif.channel_id && v.version == notif.version + }) { + webpush.stats.stored_acked += 1; + let message_month = webpush.message_month.clone(); + let n = webpush.unacked_stored_notifs.remove(pos); + // Topic/legacy messages have no sortkey_timestamp + if n.sortkey_timestamp.is_none() { + fut = if let Some(call) = fut { + let my_fut = + data.srv + .ddb + .delete_message(&message_month, &webpush.uaid, &n); + Some(Box::new(call.and_then(move |_| my_fut))) + } else { + Some(Box::new(data.srv.ddb.delete_message( + &message_month, + &webpush.uaid, + &n, + ))) + } + } + continue; + }; + } + if let Some(my_fut) = fut { + transition!(AwaitDelete { + response: my_fut, + data, + }); + } else { + transition!(DetermineAck { data }); + } + } + Either::B(ServerNotification::Notification(notif)) => { + if notif.ttl != 0 { + webpush.unacked_direct_notifs.push(notif.clone()); + } + debug!("Got a notification to send, sending!"); + transition!(SendThenWait { + remaining_data: vec![ServerMessage::Notification(notif)], + poll_complete: false, + data, + }); + } + Either::B(ServerNotification::CheckStorage) => { + webpush.flags.include_topic = true; + webpush.flags.check = true; + transition!(DetermineAck { data }); + } + Either::B(ServerNotification::Disconnect) => { + debug!("Got told to disconnect, connecting client has our uaid"); + Err("Repeat UAID disconnect".into()) + } + _ => Err("Invalid message".into()), + } + } + + fn poll_increment_storage<'a>( + increment_storage: &'a mut RentToOwn<'a, IncrementStorage>, + ) -> Poll, Error> { + trace!("State: IncrementStorage"); + let webpush_rc = increment_storage.data.webpush.clone(); + let webpush = webpush_rc.borrow(); + let timestamp = webpush + .unacked_stored_highest + .ok_or("unacked_stored_highest unset")? + .to_string(); + let response = Box::new(increment_storage.data.srv.ddb.increment_storage( + &webpush.message_month, + &webpush.uaid, + ×tamp, + )); + transition!(AwaitIncrementStorage { + response, + data: increment_storage.take().data, + }) + } + + fn poll_await_increment_storage<'a>( + await_increment_storage: &'a mut RentToOwn<'a, AwaitIncrementStorage>, + ) -> Poll, Error> { + trace!("State: AwaitIncrementStorage"); + try_ready!(await_increment_storage.response.poll()); + let AwaitIncrementStorage { data, .. } = await_increment_storage.take(); + let webpush = data.webpush.clone(); + webpush.borrow_mut().flags.increment_storage = false; + transition!(DetermineAck { data }) + } + + fn poll_check_storage<'a>( + check_storage: &'a mut RentToOwn<'a, CheckStorage>, + ) -> Poll, Error> { + trace!("State: CheckStorage"); + let CheckStorage { data } = check_storage.take(); + let response = Box::new({ + let webpush = data.webpush.borrow(); + data.srv.ddb.check_storage( + &webpush.message_month.clone(), + &webpush.uaid, + webpush.flags.include_topic, + webpush.unacked_stored_highest, + ) + }); + transition!(AwaitCheckStorage { response, data }) + } + + fn poll_await_check_storage<'a>( + await_check_storage: &'a mut RentToOwn<'a, AwaitCheckStorage>, + ) -> Poll, Error> { + trace!("State: AwaitCheckStorage"); + let (include_topic, mut messages, timestamp) = + match try_ready!(await_check_storage.response.poll()) { + CheckStorageResponse { + include_topic, + messages, + timestamp, + } => (include_topic, messages, timestamp), + }; + debug!("Got checkstorage response"); + + let AwaitCheckStorage { data, .. } = await_check_storage.take(); + let webpush_rc = data.webpush.clone(); + let mut webpush = webpush_rc.borrow_mut(); + webpush.flags.include_topic = include_topic; + debug!("Setting unacked stored highest to {:?}", timestamp); + webpush.unacked_stored_highest = timestamp; + if !messages.is_empty() { + // Filter out TTL expired messages + let now = sec_since_epoch() as u32; + messages.retain(|ref msg| now < msg.ttl + msg.timestamp); + webpush.flags.increment_storage = !include_topic && timestamp.is_some(); + // If there's still messages send them out + if !messages.is_empty() { + webpush + .unacked_stored_notifs + .extend(messages.iter().cloned()); + transition!(SendThenWait { + remaining_data: messages + .into_iter() + .map(ServerMessage::Notification) + .collect(), + poll_complete: false, + data, + }) + } else { + // No messages remaining + transition!(DetermineAck { data }) + } + } else { + webpush.flags.check = false; + transition!(DetermineAck { data }) + } + } + + fn poll_await_migrate_user<'a>( + await_migrate_user: &'a mut RentToOwn<'a, AwaitMigrateUser>, + ) -> Poll, Error> { + trace!("State: AwaitMigrateUser"); + try_ready!(await_migrate_user.response.poll()); + let AwaitMigrateUser { data, .. } = await_migrate_user.take(); + { + let mut webpush = data.webpush.borrow_mut(); + webpush.message_month = data.srv.opts.current_message_month.clone(); + webpush.flags.rotate_message_table = false; + } + transition!(DetermineAck { data }) + } + + fn poll_await_drop_user<'a>( + await_drop_user: &'a mut RentToOwn<'a, AwaitDropUser>, + ) -> Poll { + trace!("State: AwaitDropUser"); + try_ready!(await_drop_user.response.poll()); + transition!(AuthDone(())) + } + + fn poll_await_register<'a>( + await_register: &'a mut RentToOwn<'a, AwaitRegister>, + ) -> Poll, Error> { + trace!("State: AwaitRegister"); + let msg = match try_ready!(await_register.response.poll()) { + RegisterResponse::Success { endpoint } => { + let mut webpush = await_register.data.webpush.borrow_mut(); + await_register + .data + .srv + .metrics + .incr("ua.command.register") + .ok(); + webpush.stats.registers += 1; + ServerMessage::Register { + channel_id: await_register.channel_id, + status: 200, + push_endpoint: endpoint, + } + } + RegisterResponse::Error { error_msg, status } => { + debug!("Got unregister fail, error: {}", error_msg); + ServerMessage::Register { + channel_id: await_register.channel_id, + status, + push_endpoint: "".into(), + } + } + }; + + transition!(SendThenWait { + remaining_data: vec![msg], + poll_complete: false, + data: await_register.take().data, + }) + } + + fn poll_await_unregister<'a>( + await_unregister: &'a mut RentToOwn<'a, AwaitUnregister>, + ) -> Poll, Error> { + trace!("State: AwaitUnRegister"); + let msg = if try_ready!(await_unregister.response.poll()) { + debug!("Got the unregister response"); + let mut webpush = await_unregister.data.webpush.borrow_mut(); + webpush.stats.unregisters += 1; + ServerMessage::Unregister { + channel_id: await_unregister.channel_id, + status: 200, + } + } else { + debug!("Got unregister fail"); + ServerMessage::Unregister { + channel_id: await_unregister.channel_id, + status: 500, + } + }; + + let AwaitUnregister { code, data, .. } = await_unregister.take(); + data.srv + .metrics + .incr_with_tags("ua.command.unregister") + .with_tag("code", &code.to_string()) + .send(); + transition!(SendThenWait { + remaining_data: vec![msg], + poll_complete: false, + data + }) + } + + fn poll_await_delete<'a>( + await_delete: &'a mut RentToOwn<'a, AwaitDelete>, + ) -> Poll, Error> { + trace!("State: AwaitDelete"); + try_ready!(await_delete.response.poll()); + transition!(DetermineAck { + data: await_delete.take().data, + }) + } +} diff --git a/src/db/commands.rs b/src/db/commands.rs new file mode 100644 index 00000000..ead1a21d --- /dev/null +++ b/src/db/commands.rs @@ -0,0 +1,416 @@ +use std::collections::HashSet; +use std::rc::Rc; +use std::result::Result as StdResult; +use uuid::Uuid; + +use cadence::{Counted, StatsdClient}; +use chrono::Utc; +use futures::{future, Future}; +use futures_backoff::retry_if; +use rusoto_dynamodb::{ + AttributeValue, DeleteItemError, DeleteItemInput, DeleteItemOutput, DynamoDb, GetItemError, + GetItemInput, GetItemOutput, ListTablesInput, ListTablesOutput, PutItemError, PutItemInput, + PutItemOutput, QueryError, QueryInput, UpdateItemError, UpdateItemInput, UpdateItemOutput, +}; +use serde_dynamodb; + +use super::models::{DynamoDbNotification, DynamoDbUser}; +use super::util::generate_last_connect; +use super::{HelloResponse, MAX_EXPIRY, USER_RECORD_VERSION}; +use errors::*; +use protocol::Notification; +use util::timing::sec_since_epoch; + +#[derive(Default)] +pub struct FetchMessageResponse { + pub timestamp: Option, + pub messages: Vec, +} + +/// Indicate whether this last_connect falls in the current month +fn has_connected_this_month(user: &DynamoDbUser) -> bool { + user.last_connect.map_or(false, |v| { + let pat = Utc::now().format("%Y%m").to_string(); + v.to_string().starts_with(&pat) + }) +} + +pub fn list_tables( + ddb: Rc>, + start_key: Option, +) -> impl Future { + let input = ListTablesInput { + exclusive_start_table_name: start_key, + limit: Some(100), + }; + ddb.list_tables(&input) + .chain_err(|| "Unable to list tables") +} + +pub fn fetch_messages( + ddb: Rc>, + table_name: &str, + uaid: &Uuid, + limit: u32, +) -> impl Future { + let attr_values = hashmap! { + ":uaid".to_string() => val!(S => uaid.simple().to_string()), + ":cmi".to_string() => val!(S => "02"), + }; + let input = QueryInput { + key_condition_expression: Some("uaid = :uaid AND chidmessageid < :cmi".to_string()), + expression_attribute_values: Some(attr_values), + table_name: table_name.to_string(), + consistent_read: Some(true), + limit: Some(limit as i64), + ..Default::default() + }; + + let cond = |err: &QueryError| matches!(err, &QueryError::ProvisionedThroughputExceeded(_)); + retry_if(move || ddb.query(&input), cond) + .chain_err(|| "Error fetching messages") + .and_then(|output| { + let mut notifs: Vec = + output.items.map_or_else(Vec::new, |items| { + debug!("Got response of: {:?}", items); + // TODO: Capture translation errors and report them as we shouldn't + // have corrupt data + items + .into_iter() + .inspect(|i| debug!("Item: {:?}", i)) + .filter_map(|item| serde_dynamodb::from_hashmap(item).ok()) + .collect() + }); + if notifs.is_empty() { + return Ok(Default::default()); + } + + // Load the current_timestamp from the subscription registry entry which is + // the first DynamoDbNotification and remove it from the vec. + let timestamp = notifs.remove(0).current_timestamp; + // Convert any remaining DynamoDbNotifications to Notification's + // TODO: Capture translation errors and report them as we shouldn't have corrupt data + let messages = notifs + .into_iter() + .filter_map(|ddb_notif| ddb_notif.into_notif().ok()) + .collect(); + Ok(FetchMessageResponse { + timestamp, + messages, + }) + }) +} + +pub fn fetch_timestamp_messages( + ddb: Rc>, + table_name: &str, + uaid: &Uuid, + timestamp: Option, + limit: u32, +) -> impl Future { + let range_key = if let Some(ts) = timestamp { + format!("02:{}:z", ts) + } else { + "01;".to_string() + }; + let attr_values = hashmap! { + ":uaid".to_string() => val!(S => uaid.simple().to_string()), + ":cmi".to_string() => val!(S => range_key), + }; + let input = QueryInput { + key_condition_expression: Some("uaid = :uaid AND chidmessageid > :cmi".to_string()), + expression_attribute_values: Some(attr_values), + table_name: table_name.to_string(), + consistent_read: Some(true), + limit: Some(limit as i64), + ..Default::default() + }; + + let cond = |err: &QueryError| matches!(err, &QueryError::ProvisionedThroughputExceeded(_)); + retry_if(move || ddb.query(&input), cond) + .chain_err(|| "Error fetching messages") + .and_then(|output| { + let messages = output.items.map_or_else(Vec::new, |items| { + debug!("Got response of: {:?}", items); + // TODO: Capture translation errors and report them as we shouldn't have corrupt data + items + .into_iter() + .filter_map(|item| serde_dynamodb::from_hashmap(item).ok()) + .filter_map(|ddb_notif: DynamoDbNotification| ddb_notif.into_notif().ok()) + .collect() + }); + let timestamp = messages.iter().filter_map(|m| m.sortkey_timestamp).max(); + Ok(FetchMessageResponse { + timestamp, + messages, + }) + }) +} + +pub fn drop_user( + ddb: Rc>, + uaid: &Uuid, + router_table_name: &str, +) -> impl Future { + let input = DeleteItemInput { + table_name: router_table_name.to_string(), + key: ddb_item! { uaid: s => uaid.simple().to_string() }, + ..Default::default() + }; + retry_if( + move || ddb.delete_item(&input), + |err: &DeleteItemError| matches!(err, &DeleteItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error dropping user") +} + +fn get_uaid( + ddb: Rc>, + uaid: &Uuid, + router_table_name: &str, +) -> impl Future { + let input = GetItemInput { + table_name: router_table_name.to_string(), + consistent_read: Some(true), + key: ddb_item! { uaid: s => uaid.simple().to_string() }, + ..Default::default() + }; + retry_if( + move || ddb.get_item(&input), + |err: &GetItemError| matches!(err, &GetItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error fetching user") +} + +pub fn register_user( + ddb: Rc>, + user: &DynamoDbUser, + router_table: &str, +) -> impl Future { + let item = match serde_dynamodb::to_hashmap(user) { + Ok(item) => item, + Err(e) => return future::err(e).chain_err(|| "Failed to serialize item"), + }; + let router_table = router_table.to_string(); + let attr_values = hashmap! { + ":router_type".to_string() => val!(S => user.router_type), + ":connected_at".to_string() => val!(N => user.connected_at), + }; + + retry_if( + move || { + debug!("Registering user: {:?}", item); + ddb.put_item(&PutItemInput { + item: item.clone(), + table_name: router_table.clone(), + expression_attribute_values: Some(attr_values.clone()), + condition_expression: Some( + r#"( + attribute_not_exists(router_type) or + (router_type = :router_type) + ) and ( + attribute_not_exists(node_id) or + (connected_at < :connected_at) + )"#.to_string(), + ), + return_values: Some("ALL_OLD".to_string()), + ..Default::default() + }) + }, + |err: &PutItemError| matches!(err, &PutItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error storing user record") +} + +pub fn update_user_message_month( + ddb: Rc>, + uaid: &Uuid, + router_table_name: &str, + message_month: &str, +) -> impl Future { + let attr_values = hashmap! { + ":curmonth".to_string() => val!(S => message_month.to_string()), + ":lastconnect".to_string() => val!(N => generate_last_connect().to_string()), + }; + let update_item = UpdateItemInput { + key: ddb_item! { uaid: s => uaid.simple().to_string() }, + update_expression: Some( + "SET current_month=:curmonth, last_connect=:lastconnect".to_string(), + ), + expression_attribute_values: Some(attr_values), + table_name: router_table_name.to_string(), + ..Default::default() + }; + + retry_if( + move || ddb.update_item(&update_item).and_then(|_| future::ok(())), + |err: &UpdateItemError| matches!(err, &UpdateItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error updating user message month") +} + +pub fn all_channels( + ddb: Rc>, + uaid: &Uuid, + message_table_name: &str, +) -> impl Future, Error = Error> { + let input = GetItemInput { + table_name: message_table_name.to_string(), + consistent_read: Some(true), + key: ddb_item! { + uaid: s => uaid.simple().to_string(), + chidmessageid: s => " ".to_string() + }, + ..Default::default() + }; + + let cond = |err: &GetItemError| matches!(err, &GetItemError::ProvisionedThroughputExceeded(_)); + retry_if(move || ddb.get_item(&input), cond) + .and_then(|output| { + let channels = output + .item + .and_then(|item| { + serde_dynamodb::from_hashmap::(item) + .ok() + .and_then(|notif| notif.chids) + }) + .unwrap_or_else(HashSet::new); + future::ok(channels) + }) + .or_else(|_err| future::ok(HashSet::new())) +} + +pub fn save_channels( + ddb: Rc>, + uaid: &Uuid, + channels: HashSet, + message_table_name: &str, +) -> impl Future { + let chids: Vec = channels.into_iter().collect(); + let expiry = sec_since_epoch() + 2 * MAX_EXPIRY; + let attr_values = hashmap! { + ":chids".to_string() => val!(SS => chids), + ":expiry".to_string() => val!(N => expiry), + }; + let update_item = UpdateItemInput { + key: ddb_item! { + uaid: s => uaid.simple().to_string(), + chidmessageid: s => " ".to_string() + }, + update_expression: Some("ADD chids :chids SET expiry=:expiry".to_string()), + expression_attribute_values: Some(attr_values), + table_name: message_table_name.to_string(), + ..Default::default() + }; + + retry_if( + move || ddb.update_item(&update_item).and_then(|_| future::ok(())), + |err: &UpdateItemError| matches!(err, &UpdateItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error saving channels") +} + +pub fn unregister_channel_id( + ddb: Rc>, + uaid: &Uuid, + channel_id: &Uuid, + message_table_name: &str, +) -> impl Future { + let chid = channel_id.hyphenated().to_string(); + let attr_values = hashmap! { + ":channel_id".to_string() => val!(SS => vec![chid]), + }; + let update_item = UpdateItemInput { + key: ddb_item! { + uaid: s => uaid.simple().to_string(), + chidmessageid: s => " ".to_string() + }, + update_expression: Some("DELETE chids :channel_id".to_string()), + expression_attribute_values: Some(attr_values), + table_name: message_table_name.to_string(), + ..Default::default() + }; + + retry_if( + move || ddb.update_item(&update_item), + |err: &UpdateItemError| matches!(err, &UpdateItemError::ProvisionedThroughputExceeded(_)), + ).chain_err(|| "Error unregistering channel") +} + +pub fn lookup_user( + ddb: Rc>, + uaid: &Uuid, + connected_at: &u64, + router_url: &str, + router_table_name: &str, + message_table_names: &[String], + current_message_month: &str, + metrics: &StatsdClient, +) -> MyFuture<(HelloResponse, Option)> { + let response = get_uaid(ddb.clone(), uaid, router_table_name); + // Prep all these for the move into the static closure capture + let cur_month = current_message_month.to_string(); + let uaid2 = *uaid; + let router_table = router_table_name.to_string(); + let messages_tables = message_table_names.to_vec(); + let connected_at = *connected_at; + let router_url = router_url.to_string(); + let metrics = metrics.clone(); + let response = response.and_then(move |data| -> MyFuture<_> { + let mut hello_response: HelloResponse = Default::default(); + hello_response.message_month = cur_month.clone(); + let user = handle_user_result( + &cur_month, + &messages_tables, + connected_at, + router_url, + data, + &mut hello_response, + ); + match user { + Ok(user) => Box::new(future::ok((hello_response, Some(user)))), + Err((false, _)) => Box::new(future::ok((hello_response, None))), + Err((true, code)) => { + metrics + .incr_with_tags("ua.expiration") + .with_tag("code", &code.to_string()) + .send(); + let response = drop_user(ddb, &uaid2, &router_table) + .and_then(|_| future::ok((hello_response, None))) + .chain_err(|| "Unable to drop user"); + Box::new(response) + } + } + }); + Box::new(response) +} + +/// Helper function for determining if a returned user record is valid for use +/// or if it should be dropped and a new one created. +fn handle_user_result( + cur_month: &String, + messages_tables: &[String], + connected_at: u64, + router_url: String, + data: GetItemOutput, + hello_response: &mut HelloResponse, +) -> StdResult { + let item = data.item.ok_or((false, 104))?; + let mut user: DynamoDbUser = serde_dynamodb::from_hashmap(item).map_err(|_| (true, 104))?; + + let user_month = user.current_month.clone(); + let month = user_month.ok_or((true, 104))?; + if !messages_tables.contains(cur_month) { + return Err((true, 105)); + } + hello_response.check_storage = true; + hello_response.message_month = month.clone(); + hello_response.rotate_message_table = *cur_month != month; + hello_response.reset_uaid = user + .record_version + .map_or(true, |rec_ver| rec_ver < USER_RECORD_VERSION); + + user.last_connect = if has_connected_this_month(&user) { + None + } else { + Some(generate_last_connect()) + }; + user.node_id = Some(router_url); + user.connected_at = connected_at; + Ok(user) +} diff --git a/src/db/macros.rs b/src/db/macros.rs new file mode 100644 index 00000000..3e716ddf --- /dev/null +++ b/src/db/macros.rs @@ -0,0 +1,109 @@ +/// A bunch of macro helpers from rusoto_helpers code, which they pulled from crates.io because +/// they were waiting for rusuto to hit 1.0.0 or something. For sanity, they are instead accumulated +/// here for our use. +#[allow(unused_macros)] +macro_rules! attributes { + ($($val:expr => $attr_type:expr),*) => { + { + let mut temp_vec = Vec::new(); + $( + temp_vec.push(AttributeDefinition { + attribute_name: String::from($val), + attribute_type: String::from($attr_type) + }); + )* + temp_vec + } + } +} + +#[allow(unused_macros)] +macro_rules! key_schema { + ($($name:expr => $key_type:expr),*) => { + { + let mut temp_vec = Vec::new(); + $( + temp_vec.push(KeySchemaElement { + key_type: String::from($key_type), + attribute_name: String::from($name) + }); + )* + temp_vec + } + } +} + +macro_rules! val { + (B => $val:expr) => {{ + let mut attr = AttributeValue::default(); + attr.b = Some($val); + attr + }}; + (S => $val:expr) => {{ + let mut attr = AttributeValue::default(); + attr.s = Some($val.to_string()); + attr + }}; + (SS => $val:expr) => {{ + let mut attr = AttributeValue::default(); + let vals: Vec = $val.iter().map(|v| v.to_string()).collect(); + attr.ss = Some(vals); + attr + }}; + (N => $val:expr) => {{ + let mut attr = AttributeValue::default(); + attr.n = Some($val.to_string()); + attr + }}; +} + +/// Create a **HashMap** from a list of key-value pairs +/// +/// ## Example +/// +/// ``` +/// #[macro_use] extern crate rusoto_helpers; +/// # fn main() { +/// +/// let map = hashmap!{ +/// "a" => 1, +/// "b" => 2, +/// }; +/// assert_eq!(map["a"], 1); +/// assert_eq!(map["b"], 2); +/// assert_eq!(map.get("c"), None); +/// # } +/// ``` +macro_rules! hashmap { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); + + ($($key:expr => $value:expr,)+) => { hashmap!($($key => $value),+) }; + ($($key:expr => $value:expr),*) => { + { + let _cap = hashmap!(@count $($key),*); + let mut _map = ::std::collections::HashMap::with_capacity(_cap); + $( + _map.insert($key, $value); + )* + _map + } + }; +} + +/// Shorthand for specifying a dynamodb item +macro_rules! ddb_item { + ($($p:tt: $t:tt => $x:expr),*) => { + { + use rusoto_dynamodb::AttributeValue; + hashmap!{ + $( + String::from(stringify!($p)) => AttributeValue { + $t: Some($x), + ..Default::default() + }, + )* + } + } + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 00000000..e003bc23 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,405 @@ +use std::collections::HashSet; +use std::env; +use std::rc::Rc; +use uuid::Uuid; + +use cadence::StatsdClient; +use futures::{future, Future}; +use futures_backoff::retry_if; +use rusoto_core::reactor::RequestDispatcher; +use rusoto_core::Region; +use rusoto_credential::StaticProvider; +use rusoto_dynamodb::{ + AttributeValue, BatchWriteItemError, BatchWriteItemInput, DeleteItemError, DeleteItemInput, + DynamoDb, DynamoDbClient, PutRequest, UpdateItemError, UpdateItemInput, UpdateItemOutput, + WriteRequest, +}; +use serde_dynamodb; + +#[macro_use] +mod macros; +mod commands; +mod models; +use errors::*; +use protocol::Notification; +use server::Server; +mod util; +use util::timing::sec_since_epoch; + +use self::commands::FetchMessageResponse; +use self::models::{DynamoDbNotification, DynamoDbUser}; + +const MAX_EXPIRY: u64 = 2_592_000; +const USER_RECORD_VERSION: u8 = 1; + +/// Basic requirements for notification content to deliver to websocket client +/// - channelID (the subscription website intended for) +/// - version (only really utilized for notification acknowledgement in +/// webpush, used to be the sole carrier of data, can now be anything) +/// - data (encrypted content) +/// - headers (hash of crypto headers: encoding, encrypption, crypto-key, encryption-key) +#[derive(Default, Clone)] +pub struct HelloResponse { + pub uaid: Option, + pub message_month: String, + pub check_storage: bool, + pub reset_uaid: bool, + pub rotate_message_table: bool, + pub connected_at: u64, +} + +pub struct CheckStorageResponse { + pub include_topic: bool, + pub messages: Vec, + pub timestamp: Option, +} + +pub enum RegisterResponse { + Success { endpoint: String }, + + Error { error_msg: String, status: u32 }, +} + +pub struct DynamoStorage { + ddb: Rc>, +} + +impl DynamoStorage { + pub fn new() -> Self { + let ddb: Box = if let Ok(endpoint) = env::var("AWS_LOCAL_DYNAMODB") { + Box::new(DynamoDbClient::new( + RequestDispatcher::default(), + StaticProvider::new_minimal("BogusKey".to_string(), "BogusKey".to_string()), + Region::Custom { + name: "us-east-1".to_string(), + endpoint, + }, + )) + } else { + Box::new(DynamoDbClient::simple(Region::default())) + }; + Self { ddb: Rc::new(ddb) } + } + + pub fn list_message_tables(&self, prefix: &str) -> Result> { + let mut names: Vec = Vec::new(); + let mut start_key = None; + loop { + let result = commands::list_tables(self.ddb.clone(), start_key).wait()?; + start_key = result.last_evaluated_table_name; + if let Some(table_names) = result.table_names { + names.extend(table_names); + } + if start_key.is_none() { + break; + } + } + let names = names + .into_iter() + .filter(|name| name.starts_with(prefix)) + .collect(); + Ok(names) + } + + pub fn increment_storage( + &self, + table_name: &str, + uaid: &Uuid, + timestamp: &str, + ) -> impl Future { + let ddb = self.ddb.clone(); + let expiry = sec_since_epoch() + 2 * MAX_EXPIRY; + let attr_values = hashmap! { + ":timestamp".to_string() => val!(N => timestamp), + ":expiry".to_string() => val!(N => expiry), + }; + let update_input = UpdateItemInput { + key: ddb_item! { + uaid: s => uaid.simple().to_string(), + chidmessageid: s => " ".to_string() + }, + update_expression: Some("SET current_timestamp=:timestamp, expiry=:expiry".to_string()), + expression_attribute_values: Some(attr_values), + table_name: table_name.to_string(), + ..Default::default() + }; + + retry_if( + move || ddb.update_item(&update_input), + |err: &UpdateItemError| { + matches!(err, &UpdateItemError::ProvisionedThroughputExceeded(_)) + }, + ).chain_err(|| "Error incrementing storage") + } + + pub fn hello( + &self, + connected_at: &u64, + uaid: Option<&Uuid>, + router_table_name: &str, + router_url: &str, + message_table_names: &[String], + current_message_month: &str, + metrics: &StatsdClient, + ) -> impl Future { + let router_table_name = router_table_name.to_string(); + let response: MyFuture<(HelloResponse, Option)> = if let Some(uaid) = uaid { + commands::lookup_user( + self.ddb.clone(), + &uaid, + connected_at, + router_url, + &router_table_name, + message_table_names, + current_message_month, + metrics, + ) + } else { + Box::new(future::ok(( + HelloResponse { + message_month: current_message_month.to_string(), + ..Default::default() + }, + None, + ))) + }; + let ddb = self.ddb.clone(); + let router_url = router_url.to_string(); + let connected_at = *connected_at; + + response.and_then(move |(mut hello_response, user_opt)| { + let hello_message_month = hello_response.message_month.clone(); + let user = user_opt.unwrap_or_else(|| DynamoDbUser { + current_month: Some(hello_message_month), + node_id: Some(router_url), + connected_at, + ..Default::default() + }); + let uaid = user.uaid; + let mut err_response = hello_response.clone(); + err_response.connected_at = connected_at; + commands::register_user(ddb, &user, router_table_name.as_ref()) + .and_then(move |result| { + debug!("Success adding user, item output: {:?}", result); + hello_response.uaid = Some(uaid); + future::ok(hello_response) + }) + .or_else(move |e| { + debug!("Error registering user: {:?}", e); + future::ok(err_response) + }) + }) + } + + pub fn register( + &self, + srv: &Rc, + uaid: &Uuid, + channel_id: &Uuid, + message_month: &str, + key: Option, + ) -> MyFuture { + let ddb = self.ddb.clone(); + let endpoint = match srv.make_endpoint(uaid, channel_id, key) { + Ok(result) => result, + Err(_) => { + return Box::new(future::ok(RegisterResponse::Error { + error_msg: "Failed to generate endpoint".to_string(), + status: 400, + })) + } + }; + let mut chids = HashSet::new(); + chids.insert(channel_id.hyphenated().to_string()); + let response = commands::save_channels(ddb, uaid, chids, message_month) + .and_then(move |_| future::ok(RegisterResponse::Success { endpoint })) + .or_else(move |_| { + future::ok(RegisterResponse::Error { + status: 503, + error_msg: "Failed to register channel".to_string(), + }) + }); + Box::new(response) + } + + pub fn drop_uaid( + &self, + table_name: &str, + uaid: &Uuid, + ) -> impl Future { + commands::drop_user(self.ddb.clone(), uaid, table_name) + .and_then(|_| future::ok(())) + .chain_err(|| "Unable to drop user record") + } + + pub fn unregister( + &self, + uaid: &Uuid, + channel_id: &Uuid, + message_month: &str, + ) -> impl Future { + commands::unregister_channel_id(self.ddb.clone(), uaid, channel_id, message_month) + .and_then(|_| future::ok(true)) + .or_else(|_| future::ok(false)) + } + + /// Migrate a user to a new month table + pub fn migrate_user( + &self, + uaid: &Uuid, + message_month: &str, + current_message_month: &str, + router_table_name: &str, + ) -> impl Future { + let uaid = *uaid; + let ddb = self.ddb.clone(); + let ddb2 = self.ddb.clone(); + let cur_month = current_message_month.to_string(); + let cur_month2 = cur_month.clone(); + let router_table_name = router_table_name.to_string(); + + commands::all_channels(self.ddb.clone(), &uaid, message_month) + .and_then(move |channels| -> MyFuture<_> { + if channels.is_empty() { + Box::new(future::ok(())) + } else { + Box::new(commands::save_channels(ddb, &uaid, channels, &cur_month)) + } + }) + .and_then(move |_| { + commands::update_user_message_month(ddb2, &uaid, &router_table_name, &cur_month2) + }) + .and_then(|_| future::ok(())) + .chain_err(|| "Unable to migrate user") + } + + /// Store a batch of messages when shutting down + pub fn store_messages( + &self, + uaid: &Uuid, + message_month: &str, + messages: Vec, + ) -> impl Future { + let ddb = self.ddb.clone(); + let put_items: Vec = messages + .into_iter() + .filter_map(|n| { + serde_dynamodb::to_hashmap(&DynamoDbNotification::from_notif(uaid, n)) + .ok() + .map(|hm| WriteRequest { + put_request: Some(PutRequest { item: hm }), + delete_request: None, + }) + }) + .collect(); + let batch_input = BatchWriteItemInput { + request_items: hashmap! { message_month.to_string() => put_items }, + ..Default::default() + }; + + let cond = |err: &BatchWriteItemError| { + matches!(err, &BatchWriteItemError::ProvisionedThroughputExceeded(_)) + }; + retry_if(move || ddb.batch_write_item(&batch_input), cond) + .and_then(|_| future::ok(())) + .map_err(|err| { + debug!("Error saving notification: {:?}", err); + err + }) + // TODO: Use Sentry to capture/report this error + .chain_err(|| "Error saving notifications") + } + + /// Delete a given notification from the database + /// + /// No checks are done to see that this message came from the database or has + /// sufficient properties for a delete as that is expected to have been done + /// before this is called. + pub fn delete_message( + &self, + table_name: &str, + uaid: &Uuid, + notif: &Notification, + ) -> impl Future { + let ddb = self.ddb.clone(); + let delete_input = DeleteItemInput { + table_name: table_name.to_string(), + key: ddb_item! { + uaid: s => uaid.simple().to_string(), + chidmessageid: s => notif.sort_key() + }, + ..Default::default() + }; + + let cond = |err: &DeleteItemError| { + matches!(err, &DeleteItemError::ProvisionedThroughputExceeded(_)) + }; + retry_if(move || ddb.delete_item(&delete_input), cond) + .and_then(|_| future::ok(())) + .chain_err(|| "Error deleting notification") + } + + pub fn check_storage( + &self, + table_name: &str, + uaid: &Uuid, + include_topic: bool, + timestamp: Option, + ) -> impl Future { + let response: MyFuture = if include_topic { + Box::new(commands::fetch_messages( + self.ddb.clone(), + table_name, + uaid, + 11 as u32, + )) + } else { + Box::new(future::ok(Default::default())) + }; + let uaid = *uaid; + let table_name = table_name.to_string(); + let ddb = self.ddb.clone(); + + response.and_then(move |resp| -> MyFuture<_> { + // Return now from this future if we have messages + if !resp.messages.is_empty() { + debug!("Topic message returns: {:?}", resp.messages); + return Box::new(future::ok(CheckStorageResponse { + include_topic: true, + messages: resp.messages, + timestamp: resp.timestamp, + })); + } + // Use the timestamp returned by the topic query if we were looking at the topics + let timestamp = if include_topic { + resp.timestamp + } else { + timestamp + }; + let next_query: MyFuture<_> = { + if resp.messages.is_empty() || resp.timestamp.is_some() { + Box::new(commands::fetch_timestamp_messages( + ddb, + table_name.as_ref(), + &uaid, + timestamp, + 10 as u32, + )) + } else { + Box::new(future::ok(Default::default())) + } + }; + let next_query = next_query.and_then(move |resp: FetchMessageResponse| { + // If we didn't get a timestamp off the last query, use the original + // value if passed one + let timestamp = resp.timestamp.or(timestamp); + Ok(CheckStorageResponse { + include_topic: false, + messages: resp.messages, + timestamp, + }) + }); + Box::new(next_query) + }) + } +} diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 00000000..3277e2ad --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,275 @@ +use std::cmp::min; +use std::collections::{HashMap, HashSet}; +use std::result::Result as StdResult; +use uuid::Uuid; + +use regex::RegexSet; +use serde::Serializer; + +use db::util::generate_last_connect; +use errors::*; +use protocol::Notification; +use util::timing::{ms_since_epoch, sec_since_epoch}; + +use super::{MAX_EXPIRY, USER_RECORD_VERSION}; + +/// Custom Uuid serializer +/// +/// Serializes a Uuid as a simple string instead of hyphenated +fn uuid_serializer(x: &Uuid, s: S) -> StdResult +where + S: Serializer, +{ + s.serialize_str(&x.simple().to_string()) +} + +/// Direct representation of a DynamoDB Notification as we store it in the database +/// Most attributes are optional +#[derive(Default, Deserialize, PartialEq, Debug, Clone, Serialize)] +struct NotificationHeaders { + #[serde(skip_serializing_if = "Option::is_none")] + crypto_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + encryption: Option, + #[serde(skip_serializing_if = "Option::is_none")] + encryption_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + encoding: Option, +} + +fn insert_to_map(map: &mut HashMap, name: &str, val: Option) { + if let Some(val) = val { + map.insert(name.to_string(), val); + } +} + +impl From for HashMap { + fn from(val: NotificationHeaders) -> Self { + let mut map = Self::new(); + insert_to_map(&mut map, "crypto_key", val.crypto_key); + insert_to_map(&mut map, "encryption", val.encryption); + insert_to_map(&mut map, "encryption_key", val.encryption_key); + insert_to_map(&mut map, "encoding", val.encoding); + map + } +} + +impl From> for NotificationHeaders { + fn from(val: HashMap) -> Self { + Self { + crypto_key: val.get("crypto_key").map(|v| v.to_string()), + encryption: val.get("encryption").map(|v| v.to_string()), + encryption_key: val.get("encryption_key").map(|v| v.to_string()), + encoding: val.get("encoding").map(|v| v.to_string()), + } + } +} + +#[derive(Deserialize, PartialEq, Debug, Clone, Serialize)] +pub struct DynamoDbUser { + // DynamoDB + #[serde(serialize_with = "uuid_serializer")] + pub uaid: Uuid, + // Time in milliseconds that the user last connected at + pub connected_at: u64, + // Router type of the user + pub router_type: String, + // Keyed time in a month the user last connected at with limited key range for indexing + #[serde(skip_serializing_if = "Option::is_none")] + pub last_connect: Option, + // Last node/port the client was or may be connected to + #[serde(skip_serializing_if = "Option::is_none")] + pub node_id: Option, + // Record version + #[serde(skip_serializing_if = "Option::is_none")] + pub record_version: Option, + // Current month table in the database the user is on + #[serde(skip_serializing_if = "Option::is_none")] + pub current_month: Option, +} + +impl Default for DynamoDbUser { + fn default() -> Self { + Self { + uaid: Uuid::new_v4(), + connected_at: ms_since_epoch(), + router_type: "webpush".to_string(), + last_connect: Some(generate_last_connect()), + node_id: None, + record_version: Some(USER_RECORD_VERSION), + current_month: None, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct DynamoDbNotification { + // DynamoDB + #[serde(serialize_with = "uuid_serializer")] + uaid: Uuid, + // DynamoDB + // Format: + // Topic Messages: + // 01:{channel id}:{topic} + // New Messages: + // 02:{timestamp int in microseconds}:{channel id} + chidmessageid: String, + // Magic entry stored in the first Message record that indicates the highest + // non-topic timestamp we've read into + #[serde(skip_serializing_if = "Option::is_none")] + pub current_timestamp: Option, + // Magic entry stored in the first Message record that indicates the valid + // channel id's + #[serde(skip_serializing)] + pub chids: Option>, + // Time in seconds from epoch + #[serde(skip_serializing_if = "Option::is_none")] + timestamp: Option, + // DynamoDB expiration timestamp per + // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html + expiry: u32, + // TTL value provided by application server for the message + #[serde(skip_serializing_if = "Option::is_none")] + ttl: Option, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + headers: Option, + // This is the acknowledgement-id used for clients to ack that they have received the + // message. Some Python code refers to this as a message_id. Endpoints generate this + // value before sending it to storage or a connection node. + #[serde(skip_serializing_if = "Option::is_none")] + updateid: Option, +} + +impl DynamoDbNotification { + fn parse_sort_key(key: &str) -> Result { + lazy_static! { + static ref RE: RegexSet = + RegexSet::new(&[r"^01:\S+:\S+$", r"^02:\d+:\S+$", r"^\S{3,}:\S+$",]).unwrap(); + } + if !RE.is_match(key) { + return Err("Invalid chidmessageid".into()); + } + + let v: Vec<&str> = key.split(':').collect(); + match v[0] { + "01" => { + if v.len() != 3 { + return Err("Invalid topic key".into()); + } + let (channel_id, topic) = (v[1], v[2]); + let channel_id = Uuid::parse_str(channel_id)?; + Ok(RangeKey { + channel_id, + topic: Some(topic.to_string()), + sortkey_timestamp: None, + legacy_version: None, + }) + } + "02" => { + if v.len() != 3 { + return Err("Invalid topic key".into()); + } + let (sortkey, channel_id) = (v[1], v[2]); + let channel_id = Uuid::parse_str(channel_id)?; + Ok(RangeKey { + channel_id, + topic: None, + sortkey_timestamp: Some(sortkey.parse()?), + legacy_version: None, + }) + } + _ => { + if v.len() != 2 { + return Err("Invalid topic key".into()); + } + let (channel_id, legacy_version) = (v[0], v[1]); + let channel_id = Uuid::parse_str(channel_id)?; + Ok(RangeKey { + channel_id, + topic: None, + sortkey_timestamp: None, + legacy_version: Some(legacy_version.to_string()), + }) + } + } + } + + // TODO: Implement as TryFrom whenever that lands + pub fn into_notif(self) -> Result { + let key = Self::parse_sort_key(&self.chidmessageid)?; + let version = key + .legacy_version + .or(self.updateid) + .ok_or("No valid updateid/version found")?; + + Ok(Notification { + channel_id: key.channel_id, + version, + ttl: self.ttl.ok_or("No TTL found")?, + timestamp: self.timestamp.ok_or("No timestamp found")?, + topic: key.topic, + data: self.data, + headers: self.headers.map(|m| m.into()), + sortkey_timestamp: key.sortkey_timestamp, + }) + } + + pub fn from_notif(uaid: &Uuid, val: Notification) -> Self { + Self { + uaid: *uaid, + chidmessageid: val.sort_key(), + timestamp: Some(val.timestamp), + expiry: sec_since_epoch() as u32 + min(val.ttl, MAX_EXPIRY as u32), + ttl: Some(val.ttl), + data: val.data, + headers: val.headers.map(|h| h.into()), + updateid: Some(val.version), + ..Default::default() + } + } +} + +struct RangeKey { + channel_id: Uuid, + topic: Option, + pub sortkey_timestamp: Option, + legacy_version: Option, +} + +#[cfg(test)] +mod tests { + use super::DynamoDbNotification; + use util::us_since_epoch; + use uuid::Uuid; + + #[test] + fn test_parse_sort_key_ver1() { + let chid = Uuid::new_v4(); + let chidmessageid = format!("01:{}:mytopic", chid.hyphenated().to_string()); + let key = DynamoDbNotification::parse_sort_key(&chidmessageid).unwrap(); + assert_eq!(key.topic, Some("mytopic".to_string())); + assert_eq!(key.channel_id, chid); + assert_eq!(key.sortkey_timestamp, None); + } + + #[test] + fn test_parse_sort_key_ver2() { + let chid = Uuid::new_v4(); + let sortkey_timestamp = us_since_epoch(); + let chidmessageid = format!("02:{}:{}", sortkey_timestamp, chid.hyphenated().to_string()); + let key = DynamoDbNotification::parse_sort_key(&chidmessageid).unwrap(); + assert_eq!(key.topic, None); + assert_eq!(key.channel_id, chid); + assert_eq!(key.sortkey_timestamp, Some(sortkey_timestamp)); + } + + #[test] + fn test_parse_sort_key_bad_values() { + for val in vec!["02j3i2o", "03:ffas:wef", "01::mytopic", "02:oops:ohnoes"] { + let key = DynamoDbNotification::parse_sort_key(val); + assert!(key.is_err()); + } + } +} diff --git a/src/db/util.rs b/src/db/util.rs new file mode 100644 index 00000000..1b72e14f --- /dev/null +++ b/src/db/util.rs @@ -0,0 +1,15 @@ +use chrono::Utc; +use rand::{thread_rng, Rng}; + +/// Generate a last_connect +/// +/// This intentionally generates a limited set of keys for each month in a +/// known sequence. For each month, there's 24 hours * 10 random numbers for +/// a total of 240 keys per month depending on when the user migrates forward. +pub fn generate_last_connect() -> u64 { + let today = Utc::now(); + let mut rng = thread_rng(); + let num = rng.gen_range(0, 10); + let val = format!("{}{:04}", today.format("%Y%m%H"), num); + val.parse::().unwrap() +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000..a27c4207 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,87 @@ +//! Error handling for Rust +//! +//! This module defines various utilities for handling errors in the Rust +//! thread. This uses the `error-chain` crate to ergonomically define errors, +//! enable them for usage with `?`, and otherwise give us some nice utilities. +//! It's expected that this module is always glob imported: +//! +//! ```ignore +//! use errors::*; +//! ``` +//! +//! And functions in general should then return `Result<()>`. You can add extra +//! error context via `chain_err`: +//! +//! ```ignore +//! let e = some_function_returning_a_result().chain_err(|| { +//! "some extra context here to make a nicer error" +//! })?; +//! ``` +//! +//! And you can also use the `MyFuture` type alias for "nice" uses of futures +//! +//! ```ignore +//! fn add(a: i32) -> MyFuture { +//! // .. +//! } +//! ``` +//! +//! You can find some more documentation about this in the `error-chain` crate +//! online. + +use std::any::Any; +use std::error; +use std::io; +use std::num; + +use cadence; +use config; +use futures::Future; +use httparse; +use sentry; +use serde_json; +use tungstenite; +use uuid; + +error_chain! { + foreign_links { + Ws(tungstenite::Error); + Io(io::Error); + Json(serde_json::Error); + Httparse(httparse::Error); + MetricError(cadence::MetricError); + SentryError(sentry::Error); + UuidParseError(uuid::ParseError); + ParseIntError(num::ParseIntError); + ConfigError(config::ConfigError); + } + + errors { + Thread(payload: Box) { + description("thread panicked") + } + } +} + +pub type MyFuture = Box>; + +pub trait FutureChainErr { + fn chain_err(self, callback: F) -> MyFuture + where + F: FnOnce() -> E + 'static, + E: Into; +} + +impl FutureChainErr for F +where + F: Future + 'static, + F::Error: error::Error + Send + 'static, +{ + fn chain_err(self, callback: C) -> MyFuture + where + C: FnOnce() -> E + 'static, + E: Into, + { + Box::new(self.then(|r| r.chain_err(callback))) + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 00000000..7ae901f0 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,83 @@ +//! Internal router HTTP API +//! +//! Accepts PUT requests to deliver notifications to a connected client or trigger +//! a client to check storage. +//! +//! Valid URL's: +//! PUT /push/UAID - Deliver notification to a client +//! PUT /notify/UAID - Tell a client to check storage + +use std::rc::Rc; +use std::str; + +use futures::future::ok; +use futures::{Future, Stream}; +use hyper; +use hyper::{Method, StatusCode}; +use serde_json; +use tokio_service::Service; +use uuid::Uuid; + +use server::Server; + +pub struct Push(pub Rc); + +impl Service for Push { + type Request = hyper::Request; + type Response = hyper::Response; + type Error = hyper::Error; + type Future = Box>; + + fn call(&self, req: hyper::Request) -> Self::Future { + let mut response = hyper::Response::new(); + let req_path = req.path().to_string(); + let path_vec: Vec<&str> = req_path.split('/').collect(); + if path_vec.len() != 3 { + response.set_status(StatusCode::NotFound); + return Box::new(ok(response)); + } + let (method_name, uaid) = (path_vec[1], path_vec[2]); + let uaid = match Uuid::parse_str(uaid) { + Ok(id) => id, + Err(_) => { + debug!("uri not uuid: {}", req.uri().to_string()); + response.set_status(StatusCode::BadRequest); + return Box::new(ok(response)); + } + }; + let srv = self.0.clone(); + match (req.method(), method_name, uaid) { + (&Method::Put, "push", uaid) => { + // Due to consumption of body as a future we must return here + let body = req.body().concat2(); + return Box::new(body.and_then(move |body| { + let s = String::from_utf8(body.to_vec()).unwrap(); + if let Ok(msg) = serde_json::from_str(&s) { + if srv.notify_client(uaid, msg).is_ok() { + Ok(hyper::Response::new().with_status(StatusCode::Ok)) + } else { + Ok(hyper::Response::new() + .with_status(StatusCode::BadGateway) + .with_body("Client not available.")) + } + } else { + Ok(hyper::Response::new() + .with_status(hyper::StatusCode::BadRequest) + .with_body("Unable to decode body payload")) + } + })); + } + (&Method::Put, "notif", uaid) => { + if srv.check_client_storage(uaid).is_ok() { + response.set_status(StatusCode::Ok) + } else { + response.set_status(StatusCode::BadGateway); + response.set_body("Client not available."); + } + }, + (_, "push", _) | (_, "notif", _) => response.set_status(StatusCode::MethodNotAllowed), + _ => response.set_status(StatusCode::NotFound), + }; + Box::new(ok(response)) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..c5be8bfb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,129 @@ +//! WIP: Implementation of Web Push ("autopush" as well) in Rust +//! +//! This crate currently provides an implementation of an asynchronous Web Push +//! server which is intended to be interfaced with from Python. The crate mostly +//! has a C API which is driven from `__init__.py` in Python and orchestrated +//! from Python. This is currently done to help ease the transition from the old +//! Python implementation to the new Rust implementation. Currently there's a +//! good bit of API calls to remote services still implemented in Python, but +//! the thinking is that over time these services will be rewritten in to Rust +//! and the Python codebase will shrink. +//! +//! In any case though, this'll focus mainly on the Rust bits rather than the +//! Python bits! It's worth nothing though that this crate is intended to be +//! used with `cffi` in Python, which is "seamlessly" worked with through the +//! `snaek` Python dependency. That basically just means that Python "headers" +//! for this Rust crate are generated automatically. +//! +//! # High level overview +//! +//! At 10,000 feet the general architecture here is that the main Python thread +//! spins up a Rust thread which actually does all the relevant I/O. The one +//! Rust thread uses a `Core` from `tokio-core` to perform all I/O and schedule +//! asynchronous tasks. The `tungstenite` crate is used to parse and manage the +//! WebSocket protocol, with `tokio_tungstenite` being a nicer wrapper for +//! futures-style APIs. +//! +//! The entire server is written in an asynchronous fashion using the `futures` +//! crate in Rust. This basically just means that everything is exposed as a +//! future (similar to the concept in other languages) and that's how bits and +//! pieces are chained together. +//! +//! Each connected client maintains a state machine of where it is in the +//! webpush protocol (see `states.dot` at the root of this repository). Note +//! that not all states are implemented yet, this is a work in progress! All I/O +//! is managed by Rust and various state transitions are managed by Rust as +//! well. Movement between states happens typically as a result of calls into +//! Python. The various operations here will call into Python to do things like +//! db/HTTP requests and then the results are interpreted in Rust to progress +//! the state machine. +//! +//! # Module index +//! +//! There's a number of modules that currently make up the Rust implementation, +//! and one-line summaries of these are: +//! +//! * `queue` - a MPMC queue which is used to send messages to Python and Python +//! uses to delegate work to worker threads. +//! * `server` - the main bindings for the WebPush server, where the tokio +//! `Core` is created and managed inside of the Rust thread. +//! * `client` - this is where the state machine for each connected client is +//! located, managing connections over time and sending out notifications as +//! they arrive. +//! * `protocol` - a definition of the Web Push protocol messages which are send +//! over websockets. +//! * `call` - definitions of various calls that can be made into Python, each +//! of which returning a future of the response. +//! +//! Other modules tend to be miscellaneous implementation details and likely +//! aren't as relevant to the Web Push implementation. +//! +//! Otherwise be sure to check out each module for more documentation! +extern crate base64; +extern crate bytes; +extern crate cadence; +extern crate chan_signal; +extern crate chrono; +extern crate config; +extern crate docopt; +extern crate fernet; +#[macro_use] +extern crate futures; +extern crate futures_backoff; +extern crate hex; +extern crate httparse; +extern crate hyper; +#[macro_use] +extern crate lazy_static; +extern crate libc; +#[macro_use] +extern crate matches; +extern crate mozsvc_common; +extern crate openssl; +extern crate rand; +extern crate regex; +extern crate reqwest; +extern crate rusoto_core; +extern crate rusoto_credential; +extern crate rusoto_dynamodb; +extern crate sentry; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_dynamodb; +#[macro_use] +extern crate serde_json; +#[macro_use] +extern crate slog; +extern crate slog_async; +extern crate slog_mozlog_json; +#[macro_use] +extern crate slog_scope; +extern crate slog_stdlog; +extern crate slog_term; +#[macro_use] +extern crate state_machine_future; +extern crate time; +extern crate tokio_core; +extern crate tokio_io; +extern crate tokio_openssl; +extern crate tokio_service; +extern crate tokio_tungstenite; +extern crate tungstenite; +extern crate uuid; +extern crate woothee; + +#[macro_use] +extern crate error_chain; + +#[macro_use] +mod db; +mod client; +pub mod errors; +mod http; +mod logging; +mod protocol; +pub mod server; +pub mod settings; +#[macro_use] +mod util; diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 00000000..d4eb24b5 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,50 @@ +use std::io; + +use errors::Result; + +use mozsvc_common::{aws::get_ec2_instance_id, get_hostname}; +use slog::{self, Drain}; +use slog_async; +use slog_mozlog_json::MozLogJson; +use slog_scope; +use slog_stdlog; +use slog_term; + +pub fn init_logging(json: bool) -> Result<()> { + let logger = if json { + let hostname = get_ec2_instance_id() + .map(&str::to_owned) + .or_else(get_hostname) + .ok_or_else(|| "Couldn't get_hostname")?; + + let drain = MozLogJson::new(io::stdout()) + .logger_name(format!( + "{}-{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .msg_type(format!("{}:log", env!("CARGO_PKG_NAME"))) + .hostname(hostname) + .build() + .fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + slog::Logger::root(drain, slog_o!()) + } else { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + slog::Logger::root(drain, slog_o!()) + }; + // XXX: cancel slog_scope's NoGlobalLoggerSet for now, it's difficult to + // prevent it from potentially panicing during tests. reset_logging resets + // the global logger during shutdown anyway: + // https://github.com/slog-rs/slog/issues/169 + slog_scope::set_global_logger(logger).cancel_reset(); + slog_stdlog::init().ok(); + Ok(()) +} + +pub fn reset_logging() { + let logger = slog::Logger::root(slog::Discard, o!()); + slog_scope::set_global_logger(logger).cancel_reset(); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..8738c04f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,53 @@ +#[macro_use] +extern crate serde_derive; +extern crate autopush; +extern crate chan_signal; +extern crate docopt; + +use std::env; + +use chan_signal::Signal; +use docopt::Docopt; + +use autopush::errors::{Result, ResultExt}; +use autopush::server::{AutopushServer, ServerOptions}; +use autopush::settings::Settings; + +const USAGE: &'static str = " +Usage: autopush_rs [options] + +Options: + -h, --help Show this message. + --config-connection=CONFIGFILE Connection configuration file path. + --config-shared=CONFIGFILE Common configuration file path. +"; + +#[derive(Debug, Deserialize)] +struct Args { + flag_config_connection: Option, + flag_config_shared: Option, +} + +fn main() -> Result<()> { + let signal = chan_signal::notify(&[Signal::INT, Signal::TERM]); + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + let mut filenames = Vec::new(); + if let Some(shared_filename) = args.flag_config_shared { + filenames.push(shared_filename); + } + if let Some(config_filename) = args.flag_config_connection { + filenames.push(config_filename); + } + let settings = Settings::with_env_and_config_files(&filenames)?; + // Setup the AWS env var if it was set + if let Some(ref ddb_local) = settings.aws_ddb_endpoint { + env::set_var("AWS_LOCAL_DYNAMODB", ddb_local); + } + let server_opts = ServerOptions::from_settings(settings)?; + let server = AutopushServer::new(server_opts); + server.start(); + signal.recv().unwrap(); + server.stop().chain_err(|| "Failed to shutdown properly") +} diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 00000000..e318c6ae --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,158 @@ +//! Definition of Internal Router, Python, and Websocket protocol messages +//! +//! This module is a structured definition of several protocol. Both +//! messages received from the client and messages sent from the server are +//! defined here. The `derive(Deserialize)` and `derive(Serialize)` annotations +//! are used to generate the ability to serialize these structures to JSON, +//! using the `serde` crate. More docs for serde can be found at +//! https://serde.rs + +use std::collections::HashMap; +use uuid::Uuid; + +use util::ms_since_epoch; + +// Used for the server to flag a webpush client to deliver a Notification or Check storage +pub enum ServerNotification { + CheckStorage, + Notification(Notification), + Disconnect, +} + +impl Default for ServerNotification { + fn default() -> Self { + ServerNotification::Disconnect + } +} + +#[derive(Deserialize)] +#[serde(tag = "messageType", rename_all = "snake_case")] +pub enum ClientMessage { + Hello { + uaid: Option, + #[serde(rename = "channelIDs", skip_serializing_if = "Option::is_none")] + channel_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + use_webpush: Option, + #[serde(skip_serializing_if = "Option::is_none")] + broadcasts: Option>, + }, + + Register { + #[serde(rename = "channelID")] + channel_id: String, + key: Option, + }, + + Unregister { + #[serde(rename = "channelID")] + channel_id: Uuid, + code: Option, + }, + + BroadcastSubscribe { + broadcasts: HashMap, + }, + + Ack { + updates: Vec, + }, + + Nack { + code: Option, + version: String, + }, +} + +#[derive(Deserialize)] +pub struct ClientAck { + #[serde(rename = "channelID")] + pub channel_id: Uuid, + pub version: String, +} + +#[derive(Serialize)] +#[serde(tag = "messageType", rename_all = "snake_case")] +pub enum ServerMessage { + Hello { + uaid: String, + status: u32, + #[serde(skip_serializing_if = "Option::is_none")] + use_webpush: Option, + broadcasts: HashMap, + }, + + Register { + #[serde(rename = "channelID")] + channel_id: Uuid, + status: u32, + #[serde(rename = "pushEndpoint")] + push_endpoint: String, + }, + + Unregister { + #[serde(rename = "channelID")] + channel_id: Uuid, + status: u32, + }, + + Broadcast { + broadcasts: HashMap, + }, + + Notification(Notification), +} + +#[derive(Serialize, Default, Deserialize, Clone, Debug)] +pub struct Notification { + #[serde(rename = "channelID")] + pub channel_id: Uuid, + pub version: String, + #[serde(default = "default_ttl", skip_serializing)] + pub ttl: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + pub timestamp: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing)] + pub sortkey_timestamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, +} + +impl Notification { + /// Return an appropriate sort_key to use for the chidmessageid + /// + /// For new messages: + /// 02:{sortkey_timestamp}:{chid} + /// + /// For topic messages: + /// 01:{chid}:{topic} + /// + /// Old format for non-topic messages that is no longer returned: + /// {chid}:{message_id} + pub fn sort_key(&self) -> String { + let chid = self.channel_id.hyphenated(); + if let Some(ref topic) = self.topic { + format!("01:{}:{}", chid, topic) + } else if let Some(sortkey_timestamp) = self.sortkey_timestamp { + format!( + "02:{}:{}", + if sortkey_timestamp == 0 { + ms_since_epoch() + } else { + sortkey_timestamp + }, + chid + ) + } else { + // Legacy messages which we should never get anymore + format!("{}:{}", chid, self.version) + } + } +} + +fn default_ttl() -> u32 { + 0 +} diff --git a/src/server/dispatch.rs b/src/server/dispatch.rs new file mode 100644 index 00000000..25091e3f --- /dev/null +++ b/src/server/dispatch.rs @@ -0,0 +1,92 @@ +//! A future to figure out where we're going to dispatch a TCP socket. +//! +//! When the websocket server receives a TCP connection it may be a websocket +//! request or a general HTTP request. Right now the websocket library we're +//! using, Tungstenite, doesn't have built-in support for handling this +//! situation, so we roll our own. +//! +//! The general idea here is that we're going to read just enough data off the +//! socket to parse an initial HTTP request. This request will be parsed by the +//! `httparse` crate. Once we've got a request we take a look at the headers and +//! if we find a websocket upgrade we classify it as a websocket request. If +//! it's otherwise a `/status` request, we return that we're supposed to get the +//! status, and finally after all that if it doesn't match we return an error. +//! +//! This is basically a "poor man's" HTTP router and while it should be good +//! enough for now it should probably be extended/refactored in the future! +//! +//! Note that also to implement this we buffer the request that we read in +//! memory and then attach that to a socket once we've classified what kind of +//! socket this is. That's done to replay the bytes we read again for the +//! tungstenite library, which'll duplicate header parsing but we don't have +//! many other options for now! + +use bytes::BytesMut; +use futures::{Future, Poll}; +use httparse; +use tokio_core::net::TcpStream; +use tokio_io::AsyncRead; + +use errors::*; +use server::tls::MaybeTlsStream; +use server::webpush_io::WebpushIo; + +pub struct Dispatch { + socket: Option>, + data: BytesMut, +} + +pub enum RequestType { + Websocket, + Status, + LogCheck, +} + +impl Dispatch { + pub fn new(socket: MaybeTlsStream) -> Self { + Self { + socket: Some(socket), + data: BytesMut::new(), + } + } +} + +impl Future for Dispatch { + type Item = (WebpushIo, RequestType); + type Error = Error; + + fn poll(&mut self) -> Poll<(WebpushIo, RequestType), Error> { + loop { + if self.data.len() == self.data.capacity() { + self.data.reserve(16); // get some extra space + } + if try_ready!(self.socket.as_mut().unwrap().read_buf(&mut self.data)) == 0 { + return Err("early eof".into()); + } + let ty = { + let mut headers = [httparse::EMPTY_HEADER; 16]; + let mut req = httparse::Request::new(&mut headers); + match req.parse(&self.data)? { + httparse::Status::Complete(_) => {} + httparse::Status::Partial => continue, + } + + if req.headers.iter().any(|h| h.name == "Upgrade") { + RequestType::Websocket + } else { + match req.path { + Some(ref path) if path.starts_with("/status") => RequestType::Status, + Some(ref path) if path.starts_with("/v1/err/crit") => RequestType::LogCheck, + _ => { + debug!("unknown http request {:?}", req); + return Err("unknown http request".into()); + } + } + } + }; + + let tcp = self.socket.take().unwrap(); + return Ok((WebpushIo::new(tcp, self.data.take()), ty).into()); + } + } +} diff --git a/src/server/metrics.rs b/src/server/metrics.rs new file mode 100644 index 00000000..3654ff32 --- /dev/null +++ b/src/server/metrics.rs @@ -0,0 +1,23 @@ +//! Metrics tie-ins + +use std::net::UdpSocket; + +use cadence::{BufferedUdpMetricSink, NopMetricSink, QueuingMetricSink, StatsdClient}; + +use errors::*; +use server::ServerOptions; + +/// Create a cadence StatsdClient from the given options +pub fn metrics_from_opts(opts: &ServerOptions) -> Result { + Ok(if let Some(statsd_host) = opts.statsd_host.as_ref() { + let socket = UdpSocket::bind("0.0.0.0:0")?; + socket.set_nonblocking(true)?; + + let host = (statsd_host.as_str(), opts.statsd_port); + let udp_sink = BufferedUdpMetricSink::from(host, socket)?; + let sink = QueuingMetricSink::from(udp_sink); + StatsdClient::from_sink("autopush", sink) + } else { + StatsdClient::from_sink("autopush", NopMetricSink) + }) +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 00000000..10d5bd45 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,988 @@ +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::default::Default; +use std::env; +use std::io; +use std::net::SocketAddr; +use std::panic; +use std::panic::PanicInfo; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +use base64; +use cadence::StatsdClient; +use fernet::{Fernet, MultiFernet}; +use futures::sync::oneshot; +use futures::task; +use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; +use hex; +use hyper::server::Http; +use hyper::{self, header, StatusCode}; +use openssl::hash; +use openssl::ssl::SslAcceptor; +use reqwest; +use sentry; +use serde_json; +use time; +use tokio_core::net::TcpListener; +use tokio_core::reactor::{Core, Handle, Timeout}; +use tokio_io; +use tokio_tungstenite::{accept_hdr_async, WebSocketStream}; +use tungstenite::handshake::server::Request; +use tungstenite::Message; +use uuid::Uuid; + +use client::{Client, RegisteredClient}; +use db::DynamoStorage; +use errors::*; +use errors::{Error, Result}; +use http; +use logging; +use protocol::{ClientMessage, Notification, ServerMessage, ServerNotification}; +use server::dispatch::{Dispatch, RequestType}; +use server::metrics::metrics_from_opts; +use server::webpush_io::WebpushIo; +use settings::Settings; +use util::megaphone::{ + ClientServices, MegaphoneAPIResponse, Service, ServiceChangeTracker, ServiceClientInit, +}; +use util::{timeout, RcObject}; + +mod dispatch; +mod metrics; +mod tls; +mod webpush_io; + +const UAHEADER: &str = "User-Agent"; + +fn ito_dur(seconds: u32) -> Option { + if seconds == 0 { + None + } else { + Some(Duration::new(seconds.into(), 0)) + } +} + +fn fto_dur(seconds: f64) -> Option { + if seconds == 0.0 { + None + } else { + Some(Duration::new( + seconds as u64, + (seconds.fract() * 1_000_000_000.0) as u32, + )) + } +} + +// a signaler to shut down a tokio Core and its associated thread +struct ShutdownHandle(oneshot::Sender<()>, thread::JoinHandle<()>); + +pub struct AutopushServer { + opts: Arc, + shutdown_handles: Cell>>, +} + +impl AutopushServer { + pub fn new(opts: ServerOptions) -> Self { + Self { + opts: Arc::new(opts), + shutdown_handles: Cell::new(None), + } + } + + pub fn start(&self) { + logging::init_logging(!self.opts.human_logs).expect("init_logging failed"); + let handles = Server::start(&self.opts).expect("failed to start server"); + self.shutdown_handles.set(Some(handles)); + } + + /// Blocks execution of the calling thread until the helper thread with the + /// tokio reactor has exited. + pub fn stop(&self) -> Result<()> { + let mut result = Ok(()); + if let Some(shutdown_handles) = self.shutdown_handles.take() { + for ShutdownHandle(tx, thread) in shutdown_handles { + let _ = tx.send(()); + if let Err(err) = thread.join() { + result = Err(From::from(ErrorKind::Thread(err))); + } + } + } + logging::reset_logging(); + result + } +} + +pub struct ServerOptions { + pub debug: bool, + pub router_port: u16, + pub port: u16, + fernet: MultiFernet, + pub ssl_key: Option, + pub ssl_cert: Option, + pub ssl_dh_param: Option, + pub open_handshake_timeout: Option, + pub auto_ping_interval: Duration, + pub auto_ping_timeout: Duration, + pub max_connections: Option, + pub close_handshake_timeout: Option, + pub message_table_names: Vec, + pub current_message_month: String, + pub router_table_name: String, + pub router_url: String, + pub endpoint_url: String, + pub statsd_host: Option, + pub statsd_port: u16, + pub megaphone_api_url: Option, + pub megaphone_api_token: Option, + pub megaphone_poll_interval: Duration, + pub human_logs: bool, +} + +impl ServerOptions { + pub fn from_settings(settings: Settings) -> Result { + let fernets: Vec = settings + .crypto_key + .split(',') + .map(|s| s.trim().to_string()) + .map(|key| Fernet::new(&key).expect("Invalid key supplied")) + .collect(); + let fernet = MultiFernet::new(fernets); + let ddb = DynamoStorage::new(); + let message_table_names = ddb + .list_message_tables(&settings.message_tablename) + .expect("Failed to locate message tables"); + let router_url = settings.router_url(); + let endpoint_url = settings.endpoint_url(); + let mut opts = Self { + debug: settings.debug, + port: settings.port, + fernet, + router_port: settings.router_port, + statsd_host: if settings.statsd_host.is_empty() { + None + } else { + Some(settings.statsd_host) + }, + statsd_port: settings.statsd_port, + message_table_names, + current_message_month: "".to_string(), + router_table_name: settings.router_tablename, + router_url, + endpoint_url, + ssl_key: settings.router_ssl_key.map(PathBuf::from), + ssl_cert: settings.router_ssl_cert.map(PathBuf::from), + ssl_dh_param: settings.router_ssl_dh_param.map(PathBuf::from), + auto_ping_interval: fto_dur(settings.auto_ping_interval) + .expect("auto ping interval cannot be 0"), + auto_ping_timeout: fto_dur(settings.auto_ping_timeout) + .expect("auto ping timeout cannot be 0"), + close_handshake_timeout: ito_dur(settings.close_handshake_timeout), + max_connections: if settings.max_connections == 0 { + None + } else { + Some(settings.max_connections) + }, + open_handshake_timeout: ito_dur(5), + megaphone_api_url: settings.megaphone_api_url, + megaphone_api_token: settings.megaphone_api_token, + megaphone_poll_interval: ito_dur(settings.megaphone_poll_interval) + .expect("megaphone poll interval cannot be 0"), + human_logs: settings.human_logs, + }; + opts.message_table_names.sort_unstable(); + opts.current_message_month = opts + .message_table_names + .last() + .expect("No last message month found") + .to_string(); + Ok(opts) + } +} + +pub struct Server { + uaids: RefCell>, + broadcaster: RefCell, + pub ddb: DynamoStorage, + open_connections: Cell, + tls_acceptor: Option, + pub opts: Arc, + pub handle: Handle, + pub metrics: StatsdClient, +} + +impl Server { + /// Creates a new server handle to send to python. + /// + /// This will spawn a new server with the `opts` specified, spinning up a + /// separate thread for the tokio reactor. The returned ShutdownHandles can + /// be used to interact with it (e.g. shut it down). + fn start(opts: &Arc) -> Result> { + let mut shutdown_handles = vec![]; + if let Some(handle) = Server::start_sentry()? { + shutdown_handles.push(handle); + } + + let (inittx, initrx) = oneshot::channel(); + let (donetx, donerx) = oneshot::channel(); + + let opts = opts.clone(); + let thread = thread::spawn(move || { + let (srv, mut core) = match Server::new(&opts) { + Ok(core) => { + inittx.send(None).unwrap(); + core + } + Err(e) => return inittx.send(Some(e)).unwrap(), + }; + + // Internal HTTP server setup + { + let handle = core.handle(); + let addr = SocketAddr::from(([0, 0, 0, 0], srv.opts.router_port)); + let push_listener = TcpListener::bind(&addr, &handle).unwrap(); + let http = Http::::new(); + let push_srv = push_listener.incoming().for_each(move |(socket, _)| { + handle.spawn( + http.serve_connection(socket, http::Push(srv.clone())) + .map(|_| ()) + .map_err(|e| debug!("Http server connection error: {}", e)), + ); + Ok(()) + }); + core.handle().spawn(push_srv.then(|res| { + debug!("Http server {:?}", res); + Ok(()) + })); + } + + core.run(donerx).expect("Main Core run error"); + }); + + match initrx.wait() { + Ok(Some(e)) => Err(e), + Ok(None) => { + shutdown_handles.push(ShutdownHandle(donetx, thread)); + Ok(shutdown_handles) + } + Err(_) => panic::resume_unwind(thread.join().unwrap_err()), + } + } + + /// Setup Sentry logging if a SENTRY_DSN exists + fn start_sentry() -> Result> { + let creds = match env::var("SENTRY_DSN") { + Ok(dsn) => dsn.parse::()?, + Err(_) => return Ok(None), + }; + + // Spin up a new thread with a new reactor core for the sentry handler + let (donetx, donerx) = oneshot::channel(); + let thread = thread::spawn(move || { + let mut core = Core::new().expect("Unable to create core"); + let sentry = sentry::Sentry::from_settings(core.handle(), Default::default(), creds); + // Get the prior panic hook + let hook = panic::take_hook(); + sentry.register_panic_handler(Some(move |info: &PanicInfo| -> () { + hook(info); + })); + core.run(donerx).expect("Sentry Core run error"); + }); + + Ok(Some(ShutdownHandle(donetx, thread))) + } + + fn new(opts: &Arc) -> Result<(Rc, Core)> { + let core = Core::new()?; + let broadcaster = if let Some(ref megaphone_url) = opts.megaphone_api_url { + let megaphone_token = opts + .megaphone_api_token + .as_ref() + .expect("Megaphone API requires a Megaphone API Token to be set"); + ServiceChangeTracker::with_api_services(megaphone_url, megaphone_token) + .expect("Unable to initialize megaphone with provided URL") + } else { + ServiceChangeTracker::new(Vec::new()) + }; + let srv = Rc::new(Server { + opts: opts.clone(), + broadcaster: RefCell::new(broadcaster), + ddb: DynamoStorage::new(), + uaids: RefCell::new(HashMap::new()), + open_connections: Cell::new(0), + handle: core.handle(), + tls_acceptor: tls::configure(opts), + metrics: metrics_from_opts(opts)?, + }); + let addr = SocketAddr::from(([0, 0, 0, 0], srv.opts.port)); + let ws_listener = TcpListener::bind(&addr, &srv.handle)?; + + let handle = core.handle(); + let srv2 = srv.clone(); + let ws_srv = ws_listener + .incoming() + .map_err(Error::from) + .for_each(move |(socket, addr)| { + // Make sure we're not handling too many clients before we start the + // websocket handshake. + let max = srv.opts.max_connections.unwrap_or(u32::max_value()); + if srv.open_connections.get() >= max { + info!( + "dropping {} as we already have too many open \ + connections", + addr + ); + return Ok(()); + } + srv.open_connections.set(srv.open_connections.get() + 1); + + // TODO: TCP socket options here? + + // Process TLS (if configured) + let socket = tls::accept(&srv, socket); + + // Figure out if this is a websocket or a `/status` request, + let request = socket.and_then(Dispatch::new); + + // Time out both the TLS accept (if any) along with the dispatch + // to figure out where we're going. + let request = timeout(request, srv.opts.open_handshake_timeout, &handle); + let srv2 = srv.clone(); + let handle2 = handle.clone(); + + let host = format!("{}", addr.ip()); + + // Setup oneshot to extract the user-agent from the header callback + let (uatx, uarx) = oneshot::channel(); + let callback = |req: &Request| { + if let Some(value) = req.headers.find_first(UAHEADER) { + let mut valstr = String::new(); + for c in value.iter() { + let c = *c as char; + valstr.push(c); + } + debug!("Found user-agent string"; "user-agent" => valstr.as_str()); + uatx.send(valstr).unwrap(); + } + debug!("No agent string found"); + Ok(None) + }; + + let client = request.and_then(move |(socket, request)| -> MyFuture<_> { + match request { + RequestType::Status => write_status(socket), + RequestType::LogCheck => write_log_check(socket), + RequestType::Websocket => { + // Perform the websocket handshake on each + // connection, but don't let it take too long. + let ws = accept_hdr_async(socket, callback) + .chain_err(|| "failed to accept client"); + let ws = timeout(ws, srv2.opts.open_handshake_timeout, &handle2); + + // Once the handshake is done we'll start the main + // communication with the client, managing pings + // here and deferring to `Client` to start driving + // the internal state machine. + Box::new( + ws.and_then(move |ws| { + PingManager::new(&srv2, ws, uarx, host) + .chain_err(|| "failed to make ping handler") + }).flatten(), + ) + } + } + }); + + let srv = srv.clone(); + handle.spawn(client.then(move |res| { + srv.open_connections.set(srv.open_connections.get() - 1); + if let Err(e) = res { + let mut error = e.to_string(); + for err in e.iter().skip(1) { + error.push_str("\n"); + error.push_str(&err.to_string()); + } + debug!("{}: {}", addr, error); + } + Ok(()) + })); + + Ok(()) + }); + + if let Some(ref megaphone_url) = opts.megaphone_api_url { + let megaphone_token = opts + .megaphone_api_token + .as_ref() + .expect("Megaphone API requires a Megaphone API Token to be set"); + let fut = MegaphoneUpdater::new( + megaphone_url, + megaphone_token, + opts.megaphone_poll_interval, + &srv2, + ).expect("Unable to start megaphone updater"); + core.handle().spawn(fut.then(|res| { + debug!("megaphone result: {:?}", res.map(drop)); + Ok(()) + })); + } + core.handle().spawn(ws_srv.then(|res| { + debug!("srv res: {:?}", res.map(drop)); + Ok(()) + })); + + Ok((srv2, core)) + } + + /// Create an v1 or v2 WebPush endpoint from the identifiers + /// + /// Both endpoints use bytes instead of hex to reduce ID length. + // v1 is the uaid + chid + // v2 is the uaid + chid + sha256(key).bytes + pub fn make_endpoint(&self, uaid: &Uuid, chid: &Uuid, key: Option) -> Result { + let root = format!("{}/wpush/", self.opts.endpoint_url); + let mut base = hex::decode(uaid.simple().to_string()).chain_err(|| "Error decoding")?; + base.extend(hex::decode(chid.simple().to_string()).chain_err(|| "Error decoding")?); + if let Some(k) = key { + let raw_key = base64::decode_config(&k, base64::URL_SAFE) + .chain_err(|| "Error encrypting payload")?; + let key_digest = hash::hash(hash::MessageDigest::sha256(), &raw_key) + .chain_err(|| "Error creating message digest for key")?; + base.extend(key_digest.iter()); + let encrypted = self + .opts + .fernet + .encrypt(&base) + .trim_matches('=') + .to_string(); + Ok(format!("{}v2/{}", root, encrypted)) + } else { + let encrypted = self + .opts + .fernet + .encrypt(&base) + .trim_matches('=') + .to_string(); + Ok(format!("{}v1/{}", root, encrypted)) + } + } + + /// Informs this server that a new `client` has connected + /// + /// For now just registers internal state by keeping track of the `client`, + /// namely its channel to send notifications back. + pub fn connect_client(&self, client: RegisteredClient) { + debug!("Connecting a client!"); + if let Some(client) = self.uaids.borrow_mut().insert(client.uaid, client) { + // Drop existing connection + let result = client.tx.unbounded_send(ServerNotification::Disconnect); + if result.is_ok() { + debug!("Told client to disconnect as a new one wants to connect"); + } + } + } + + /// A notification has come for the uaid + pub fn notify_client(&self, uaid: Uuid, notif: Notification) -> Result<()> { + let uaids = self.uaids.borrow(); + if let Some(client) = uaids.get(&uaid) { + debug!("Found a client to deliver a notification to"); + let result = client + .tx + .unbounded_send(ServerNotification::Notification(notif)); + if result.is_ok() { + debug!("Dropped notification in queue"); + return Ok(()); + } + } + Err("User not connected".into()) + } + + /// A check for notification command has come for the uaid + pub fn check_client_storage(&self, uaid: Uuid) -> Result<()> { + let uaids = self.uaids.borrow(); + if let Some(client) = uaids.get(&uaid) { + let result = client.tx.unbounded_send(ServerNotification::CheckStorage); + if result.is_ok() { + debug!("Told client to check storage"); + return Ok(()); + } + } + Err("User not connected".into()) + } + + /// The client specified by `uaid` has disconnected. + pub fn disconnet_client(&self, uaid: &Uuid, uid: &Uuid) { + debug!("Disconnecting client!"); + let mut uaids = self.uaids.borrow_mut(); + let client_exists = uaids.get(uaid).map_or(false, |client| client.uid == *uid); + if client_exists { + uaids.remove(uaid).expect("Couldn't remove client?"); + } + } + + /// Generate a new service client list for a newly connected client + pub fn broadcast_init(&self, services: &[Service]) -> ServiceClientInit { + debug!("Initialized broadcast services"); + self.broadcaster.borrow().service_delta(services) + } + + /// Calculate whether there's new service versions to go out + pub fn broadcast_delta(&self, client_services: &mut ClientServices) -> Option> { + self.broadcaster + .borrow() + .change_count_delta(client_services) + } + + /// Add services to be tracked by a client + pub fn client_service_add_service( + &self, + client_services: &mut ClientServices, + services: &[Service], + ) -> Option> { + self.broadcaster + .borrow() + .client_service_add_service(client_services, services) + } +} + +enum MegaphoneState { + Waiting, + Requesting(MyFuture), +} + +struct MegaphoneUpdater { + srv: Rc, + api_url: String, + api_token: String, + state: MegaphoneState, + timeout: Timeout, + poll_interval: Duration, + client: reqwest::unstable::async::Client, +} + +impl MegaphoneUpdater { + fn new( + uri: &str, + token: &str, + poll_interval: Duration, + srv: &Rc, + ) -> io::Result { + let client = reqwest::unstable::async::Client::builder() + .timeout(Duration::from_secs(1)) + .build(&srv.handle) + .expect("Unable to build reqwest client"); + Ok(MegaphoneUpdater { + srv: srv.clone(), + api_url: uri.to_string(), + api_token: token.to_string(), + state: MegaphoneState::Waiting, + timeout: Timeout::new(poll_interval, &srv.handle)?, + poll_interval, + client, + }) + } +} + +impl Future for MegaphoneUpdater { + type Item = (); + type Error = Error; + + fn poll(&mut self) -> Poll<(), Error> { + loop { + let new_state = match self.state { + MegaphoneState::Waiting => { + try_ready!(self.timeout.poll()); + debug!("Sending megaphone API request"); + let fut = self + .client + .get(&self.api_url) + .header(header::Authorization(self.api_token.clone())) + .send() + .and_then(|response| response.error_for_status()) + .and_then(|mut response| response.json()) + .map_err(|_| "Unable to query/decode the API query".into()); + MegaphoneState::Requesting(Box::new(fut)) + } + MegaphoneState::Requesting(ref mut response) => { + let at = Instant::now() + self.poll_interval; + match response.poll() { + Ok(Async::Ready(MegaphoneAPIResponse { broadcasts })) => { + debug!("Fetched broadcasts: {:?}", broadcasts); + let mut broadcaster = self.srv.broadcaster.borrow_mut(); + for srv in Service::from_hashmap(broadcasts) { + broadcaster.add_service(srv); + } + } + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(_) => { + // TODO: Flag sentry that we can't poll megaphone API + debug!("Failed to get response, queue again"); + } + }; + self.timeout.reset(at); + MegaphoneState::Waiting + } + }; + self.state = new_state; + } + } +} + +enum WaitingFor { + SendPing, + Pong, + Close, +} + +enum CloseState { + Exchange(T), + Closing, +} + +struct PingManager { + socket: RcObject>>, + timeout: Timeout, + waiting: WaitingFor, + srv: Rc, + client: CloseState>>>>, +} + +impl PingManager { + fn new( + srv: &Rc, + socket: WebSocketStream, + uarx: oneshot::Receiver, + host: String, + ) -> io::Result { + // The `socket` is itself a sink and a stream, and we've also got a sink + // (`tx`) and a stream (`rx`) to send messages. Half of our job will be + // doing all this proxying: reading messages from `socket` and sending + // them to `tx` while also reading messages from `rx` and sending them + // on `socket`. + // + // Our other job will be to manage the websocket protocol pings going + // out and coming back. The `opts` provided indicate how often we send + // pings and how long we'll wait for the ping to come back before we + // time it out. + // + // To make these tasks easier we start out by throwing the `socket` into + // an `Rc` object. This'll allow us to share it between the ping/pong + // management and message shuffling. + let socket = RcObject::new(WebpushSocket::new(socket)); + Ok(PingManager { + timeout: Timeout::new(srv.opts.auto_ping_interval, &srv.handle)?, + waiting: WaitingFor::SendPing, + socket: socket.clone(), + client: CloseState::Exchange(Client::new(socket, srv, uarx, host)), + srv: srv.clone(), + }) + } +} + +impl Future for PingManager { + type Item = (); + type Error = Error; + + fn poll(&mut self) -> Poll<(), Error> { + let mut socket = self.socket.borrow_mut(); + loop { + if socket.ping { + // Don't check if we already have a delta to broadcast + if socket.broadcast_delta.is_none() { + // Determine if we can do a broadcast check, we need a connected webpush client + if let CloseState::Exchange(ref mut client) = self.client { + if let Some(delta) = client.broadcast_delta() { + socket.broadcast_delta = Some(delta); + } + } + } + + if socket.send_ping()?.is_ready() { + // If we just sent a broadcast, reset the ping interval and clear the delta + if socket.broadcast_delta.is_some() { + let at = Instant::now() + self.srv.opts.auto_ping_interval; + self.timeout.reset(at); + socket.broadcast_delta = None; + self.waiting = WaitingFor::SendPing + } else { + let at = Instant::now() + self.srv.opts.auto_ping_timeout; + self.timeout.reset(at); + self.waiting = WaitingFor::Pong + } + } else { + break; + } + } + debug_assert!(!socket.ping); + match self.waiting { + WaitingFor::SendPing => { + debug_assert!(!socket.pong_timeout); + debug_assert!(!socket.pong_received); + match self.timeout.poll()? { + Async::Ready(()) => { + debug!("scheduling a ping to get sent"); + socket.ping = true; + } + Async::NotReady => break, + } + } + WaitingFor::Pong => { + if socket.pong_received { + // If we received a pong, then switch us back to waiting + // to send out a ping + debug!("pong received, going back to sending a ping"); + debug_assert!(!socket.pong_timeout); + let at = Instant::now() + self.srv.opts.auto_ping_interval; + self.timeout.reset(at); + self.waiting = WaitingFor::SendPing; + socket.pong_received = false; + } else if socket.pong_timeout { + // If our socket is waiting to deliver a pong timeout, + // then no need to keep checking the timer and we can + // keep going + debug!("waiting for socket to see pong timed out"); + break; + } else if self.timeout.poll()?.is_ready() { + // We may not actually be reading messages from the + // websocket right now, could have been waiting on + // something else. Instead of immediately returning an + // error here wait for the stream to return `NotReady` + // when looking for messages, as then we're extra sure + // that no pong was received after this timeout elapsed. + debug!("waited too long for a pong"); + socket.pong_timeout = true; + } else { + break; + } + } + WaitingFor::Close => { + debug_assert!(!socket.pong_timeout); + if self.timeout.poll()?.is_ready() { + if let CloseState::Exchange(ref mut client) = self.client { + client.shutdown(); + } + // So did the shutdown not work? We must call shutdown but no client here? + return Err("close handshake took too long".into()); + } + } + } + } + + // Be sure to always flush out any buffered messages/pings + socket + .poll_complete() + .chain_err(|| "failed routine `poll_complete` call")?; + drop(socket); + + // At this point looks our state of ping management A-OK, so try to + // make progress on our client, and when done with that execute the + // closing handshake. + loop { + match self.client { + CloseState::Exchange(ref mut client) => try_ready!(client.poll()), + CloseState::Closing => return Ok(self.socket.borrow_mut().close()?), + } + + self.client = CloseState::Closing; + if let Some(dur) = self.srv.opts.close_handshake_timeout { + let at = Instant::now() + dur; + self.timeout.reset(at); + self.waiting = WaitingFor::Close; + } + } + } +} + +// Wrapper struct to take a Sink/Stream of `Message` to a Sink/Stream of +// `ClientMessage` and `ServerMessage`. +struct WebpushSocket { + inner: T, + pong_received: bool, + ping: bool, + pong_timeout: bool, + broadcast_delta: Option>, +} + +impl WebpushSocket { + fn new(t: T) -> WebpushSocket { + WebpushSocket { + inner: t, + pong_received: false, + ping: false, + pong_timeout: false, + broadcast_delta: None, + } + } + + fn send_ping(&mut self) -> Poll<(), Error> + where + T: Sink, + Error: From, + { + if self.ping { + let msg = if let Some(broadcasts) = self.broadcast_delta.clone() { + debug!("sending a broadcast delta"); + let server_msg = ServerMessage::Broadcast { + broadcasts: Service::into_hashmap(broadcasts), + }; + let s = serde_json::to_string(&server_msg).chain_err(|| "failed to serialize")?; + Message::Text(s) + } else { + debug!("sending a ping"); + Message::Ping(Vec::new()) + }; + match self.inner.start_send(msg)? { + AsyncSink::Ready => { + debug!("ping sent"); + self.ping = false; + } + AsyncSink::NotReady(_) => { + debug!("ping not ready to be sent"); + return Ok(Async::NotReady); + } + } + } + Ok(Async::Ready(())) + } +} + +impl Stream for WebpushSocket +where + T: Stream, + Error: From, +{ + type Item = ClientMessage; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + loop { + let msg = match self.inner.poll()? { + Async::Ready(Some(msg)) => msg, + Async::Ready(None) => return Ok(None.into()), + Async::NotReady => { + // If we don't have any more messages and our pong timeout + // elapsed (set above) then this is where we start + // triggering errors. + if self.pong_timeout { + return Err("failed to receive a pong in time".into()); + } + return Ok(Async::NotReady); + } + }; + match msg { + Message::Text(ref s) => { + trace!("text message {}", s); + let msg = serde_json::from_str(s).chain_err(|| "invalid json text")?; + return Ok(Some(msg).into()); + } + + Message::Binary(_) => return Err("binary messages not accepted".into()), + + // sending a pong is already managed by lower layers, just go to + // the next message + Message::Ping(_) => {} + + // Wake up ourselves to ensure the above ping logic eventually + // sees this pong. + Message::Pong(_) => { + self.pong_received = true; + self.pong_timeout = false; + task::current().notify(); + } + } + } + } +} + +impl Sink for WebpushSocket +where + T: Sink, + Error: From, +{ + type SinkItem = ServerMessage; + type SinkError = Error; + + fn start_send(&mut self, msg: ServerMessage) -> StartSend { + if self.send_ping()?.is_not_ready() { + return Ok(AsyncSink::NotReady(msg)); + } + let s = serde_json::to_string(&msg).chain_err(|| "failed to serialize")?; + match self.inner.start_send(Message::Text(s))? { + AsyncSink::Ready => Ok(AsyncSink::Ready), + AsyncSink::NotReady(_) => Ok(AsyncSink::NotReady(msg)), + } + } + + fn poll_complete(&mut self) -> Poll<(), Error> { + try_ready!(self.send_ping()); + Ok(self.inner.poll_complete()?) + } + + fn close(&mut self) -> Poll<(), Error> { + try_ready!(self.poll_complete()); + Ok(self.inner.close()?) + } +} + +fn write_status(socket: WebpushIo) -> MyFuture<()> { + write_json( + socket, + StatusCode::Ok, + json!({ + "status": "OK", + "version": env!("CARGO_PKG_VERSION"), + }), + ) +} + +fn write_log_check(socket: WebpushIo) -> MyFuture<()> { + let status = StatusCode::ImATeapot; + let code: u16 = status.into(); + + error!("Test Critical Message"; + "status_code" => code, + "errno" => 0, + ); + thread::spawn(|| { + panic!("LogCheck"); + }); + + write_json( + socket, + StatusCode::ImATeapot, + json!({ + "code": code, + "errno": 999, + "error": "Test Failure", + "mesage": "FAILURE:Success", + }), + ) +} + +fn write_json(socket: WebpushIo, status: StatusCode, body: serde_json::Value) -> MyFuture<()> { + let body = body.to_string(); + let data = format!( + "\ + HTTP/1.1 {status}\r\n\ + Server: webpush\r\n\ + Date: {date}\r\n\ + Content-Length: {len}\r\n\ + Content-Type: application/json\r\n\ + \r\n\ + {body}\ + ", + status = status, + date = time::at(time::get_time()).rfc822(), + len = body.len(), + body = body, + ); + Box::new( + tokio_io::io::write_all(socket, data.into_bytes()) + .map(|_| ()) + .chain_err(|| "failed to write status response"), + ) +} diff --git a/src/server/tls.rs b/src/server/tls.rs new file mode 100644 index 00000000..315df39d --- /dev/null +++ b/src/server/tls.rs @@ -0,0 +1,138 @@ +//! TLS support for the autopush server +//! +//! Currently tungstenite in the way we use it just operates generically over an +//! `AsyncRead`/`AsyncWrite` stream, so this provides a `MaybeTlsStream` type +//! which dispatches at runtime whether it's a plaintext or TLS stream after a +//! connection is established. + +use std::fs::File; +use std::io::{self, Read, Write}; +use std::path::Path; +use std::rc::Rc; + +use futures::future; +use futures::{Future, Poll}; +use openssl::dh::Dh; +use openssl::pkey::PKey; +use openssl::ssl::{SslAcceptor, SslMethod, SslMode}; +use openssl::x509::X509; +use tokio_core::net::TcpStream; +use tokio_io::{AsyncRead, AsyncWrite}; +use tokio_openssl::{SslAcceptorExt, SslStream}; + +use errors::*; +use server::{Server, ServerOptions}; + +/// Creates an `SslAcceptor`, if needed, ready to accept TLS connections. +/// +/// This method is called early on when the server is created and the +/// `SslAcceptor` type is stored globally in the `Server` structure, later used +/// to process all accepted TCP sockets. +pub fn configure(opts: &ServerOptions) -> Option { + let key = match opts.ssl_key { + Some(ref key) => read(key), + None => return None, + }; + let key = PKey::private_key_from_pem(&key).expect("failed to create private key"); + let cert = read(opts.ssl_cert.as_ref().expect("ssl_cert not configured")); + let cert = X509::from_pem(&cert).expect("failed to create certificate"); + + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()) + .expect("failed to create ssl acceptor builder"); + builder + .set_private_key(&key) + .expect("failed to set private key"); + builder + .set_certificate(&cert) + .expect("failed to set certificate"); + builder + .check_private_key() + .expect("private key check failed"); + + if let Some(dh_param) = opts.ssl_dh_param.as_ref() { + let dh_param = Dh::params_from_pem(&read(dh_param)).expect("failed to create dh"); + builder + .set_tmp_dh(&dh_param) + .expect("failed to set dh_param"); + } + + // Should help reduce peak memory consumption for idle connections + builder.set_mode(SslMode::RELEASE_BUFFERS); + + return Some(builder.build()); + + fn read(path: &Path) -> Vec { + let mut out = Vec::new(); + File::open(path) + .expect(&format!("failed to open {:?}", path)) + .read_to_end(&mut out) + .expect(&format!("failed to read {:?}", path)); + out + } +} + +/// Performs the TLS handshake, if necessary, for a socket. +/// +/// This is typically called just after a socket has been accepted from the TCP +/// listener. If the server is configured without TLS then this will immediately +/// return with a plaintext socket, or otherwise it will perform an asynchronous +/// TLS handshake and only resolve once that's completed. +pub fn accept(srv: &Rc, socket: TcpStream) -> MyFuture> { + match srv.tls_acceptor { + Some(ref acceptor) => Box::new( + acceptor + .accept_async(socket) + .map(MaybeTlsStream::Tls) + .chain_err(|| "failed to accept TLS socket"), + ), + None => Box::new(future::ok(MaybeTlsStream::Plain(socket))), + } +} + +pub enum MaybeTlsStream { + Plain(T), + Tls(SslStream), +} + +impl Read for MaybeTlsStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match *self { + MaybeTlsStream::Plain(ref mut s) => s.read(buf), + MaybeTlsStream::Tls(ref mut s) => s.read(buf), + } + } +} + +impl Write for MaybeTlsStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + MaybeTlsStream::Plain(ref mut s) => s.write(buf), + MaybeTlsStream::Tls(ref mut s) => s.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match *self { + MaybeTlsStream::Plain(ref mut s) => s.flush(), + MaybeTlsStream::Tls(ref mut s) => s.flush(), + } + } +} + +impl AsyncRead for MaybeTlsStream { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + match *self { + MaybeTlsStream::Plain(ref s) => s.prepare_uninitialized_buffer(buf), + MaybeTlsStream::Tls(ref s) => s.prepare_uninitialized_buffer(buf), + } + } +} + +impl AsyncWrite for MaybeTlsStream { + fn shutdown(&mut self) -> Poll<(), io::Error> { + match *self { + MaybeTlsStream::Plain(ref mut s) => s.shutdown(), + MaybeTlsStream::Tls(ref mut s) => s.shutdown(), + } + } +} diff --git a/src/server/webpush_io.rs b/src/server/webpush_io.rs new file mode 100644 index 00000000..2dcb06f1 --- /dev/null +++ b/src/server/webpush_io.rs @@ -0,0 +1,66 @@ +//! I/O wrapper created through `Dispatch` +//! +//! Most I/O happens through just raw TCP sockets, but at the beginning of a +//! request we'll take a look at the headers and figure out where to route it. +//! After that, for tungstenite the websocket library, we'll want to replay the +//! data we already read as there's no ability to pass this in currently. That +//! means we'll parse headers twice, but alas! + +use std::io::{self, Read, Write}; + +use bytes::BytesMut; +use futures::Poll; +use tokio_core::net::TcpStream; +use tokio_io::{AsyncRead, AsyncWrite}; + +use server::tls::MaybeTlsStream; + +pub struct WebpushIo { + tcp: MaybeTlsStream, + header_to_read: Option, +} + +impl WebpushIo { + pub fn new(tcp: MaybeTlsStream, header: BytesMut) -> Self { + Self { + tcp: tcp, + header_to_read: Some(header), + } + } +} + +impl Read for WebpushIo { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + // Start off by replaying the bytes already read, and after that just + // delegate everything to the internal `TcpStream` + if let Some(ref mut header) = self.header_to_read { + let n = (&header[..]).read(buf)?; + header.split_to(n); + if buf.is_empty() || n > 0 { + return Ok(n); + } + } + self.header_to_read = None; + self.tcp.read(buf) + } +} + +// All `write` calls are routed through the `TcpStream` instance directly as we +// don't buffer this at all. +impl Write for WebpushIo { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.tcp.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.tcp.flush() + } +} + +impl AsyncRead for WebpushIo {} + +impl AsyncWrite for WebpushIo { + fn shutdown(&mut self) -> Poll<(), io::Error> { + AsyncWrite::shutdown(&mut self.tcp) + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 00000000..7af28a93 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,131 @@ +use std::net::ToSocketAddrs; + +use config::{Config, ConfigError, Environment, File}; +use fernet::Fernet; +use mozsvc_common::get_hostname; + +lazy_static! { + static ref HOSTNAME: String = get_hostname().unwrap(); + static ref RESOLVED_HOSTNAME: String = get_resolved_hostname(); +} + +fn get_resolved_hostname() -> String { + let hostname = get_hostname().expect("Can't get hostname"); + hostname + .to_socket_addrs() + .expect("Failed to resolve hostnames") + .last() + .expect("No hostnames found") + .to_string() +} + +#[derive(Debug, Deserialize)] +pub struct Settings { + pub debug: bool, + pub port: u16, + pub hostname: Option, + pub resolve_hostname: bool, + pub router_port: u16, + pub router_hostname: Option, + pub router_tablename: String, + pub message_tablename: String, + pub router_ssl_key: Option, + pub router_ssl_cert: Option, + pub router_ssl_dh_param: Option, + pub auto_ping_interval: f64, + pub auto_ping_timeout: f64, + pub max_connections: u32, + pub close_handshake_timeout: u32, + pub endpoint_scheme: String, + pub endpoint_hostname: Option, + pub endpoint_port: u16, + pub crypto_key: String, + pub statsd_host: String, + pub statsd_port: u16, + pub aws_ddb_endpoint: Option, + pub megaphone_api_url: Option, + pub megaphone_api_token: Option, + pub megaphone_poll_interval: u32, + pub human_logs: bool, +} + +impl Settings { + /// Load the settings from the config files in order first then the environment. + pub fn with_env_and_config_files(filenames: &[String]) -> Result { + let mut s = Config::default(); + // Set our defaults, this can be fixed up drastically later after: + // https://github.com/mehcode/config-rs/issues/60 + s.set_default("debug", false)?; + s.set_default("port", 8080)?; + s.set_default("resolve_hostname", false)?; + s.set_default("router_port", 8081)?; + s.set_default("router_tablename", "router")?; + s.set_default("message_tablename", "message")?; + s.set_default("auto_ping_interval", 300)?; + s.set_default("auto_ping_timeout", 4)?; + s.set_default("max_connections", 0)?; + s.set_default("close_handshake_timeout", 0)?; + s.set_default("endpoint_scheme", "http")?; + s.set_default("endpoint_port", 8082)?; + s.set_default("crypto_key", Fernet::generate_key())?; + s.set_default("statsd_host", "localhost")?; + s.set_default("statsd_port", 8125)?; + s.set_default("megaphone_poll_interval", 30)?; + s.set_default("human_logs", false)?; + + // Merge the configs from the files + for filename in filenames { + s.merge(File::with_name(filename))?; + } + + // Merge the environment overrides + s.merge(Environment::with_prefix("autopush"))?; + s.try_into() + } + + pub fn router_url(&self) -> String { + let router_scheme = if self.router_ssl_key.is_none() { + "http" + } else { + "https" + }; + let hostname = self.host_name(); + format!( + "{}://{}:{}", + router_scheme, + self.router_hostname.as_ref().unwrap_or(&hostname), + self.router_port + ) + } + + pub fn endpoint_url(&self) -> String { + format!( + "{}://{}:{}", + self.endpoint_scheme, + self.endpoint_hostname + .as_ref() + .expect("Endpoint hostname must be supplied"), + self.endpoint_port + ) + } + + fn host_name(&self) -> String { + if let Some(ref hostname) = self.hostname { + if self.resolve_hostname { + return hostname + .to_socket_addrs() + .expect("Failed to resolve hostnames") + .last() + .expect("No hostnames found") + .to_string(); + } else { + return hostname.clone(); + } + } + if self.resolve_hostname { + RESOLVED_HOSTNAME.clone() + } else { + HOSTNAME.clone() + } + } +} diff --git a/src/util/megaphone.rs b/src/util/megaphone.rs new file mode 100644 index 00000000..7011c5c1 --- /dev/null +++ b/src/util/megaphone.rs @@ -0,0 +1,359 @@ +use errors::Result; +use std::collections::HashMap; +use std::time::Duration; + +use reqwest; + +// A Service entry Key in a ServiceRegistry +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] +struct ServiceKey(u32); + +// A list of services that a client is interested in and the last change seen +#[derive(Debug, Default)] +pub struct ClientServices { + service_list: Vec, + change_count: u32, +} + +#[derive(Debug)] +struct ServiceRegistry { + lookup: HashMap, + table: Vec, +} + +// Return result of the first delta call for a client given a full list of service id's and versions +#[derive(Debug)] +pub struct ServiceClientInit(pub ClientServices, pub Vec); + +impl ServiceRegistry { + fn new() -> ServiceRegistry { + ServiceRegistry { + lookup: HashMap::new(), + table: Vec::new(), + } + } + + // Add's a new service to the lookup table, returns the existing key if the service already + // exists + fn add_service(&mut self, service_id: String) -> ServiceKey { + if let Some(v) = self.lookup.get(&service_id) { + return ServiceKey(*v); + } + let i = self.table.len(); + self.table.push(service_id.clone()); + self.lookup.insert(service_id, i as u32); + ServiceKey(i as u32) + } + + fn lookup_id(&self, key: ServiceKey) -> Option { + self.table.get(key.0 as usize).cloned() + } + + fn lookup_key(&self, service_id: &str) -> Option { + self.lookup.get(service_id).cloned().map(ServiceKey) + } +} + +// An individual service and the current change count +#[derive(Debug)] +struct ServiceRevision { + change_count: u32, + service: ServiceKey, +} + +// A provided Service/Version used for `ChangeList` initialization, client comparisons, and +// outgoing deltas +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Service { + service_id: String, + version: String, +} + +// Handy From impls for common hashmap to/from conversions +impl From<(String, String)> for Service { + fn from(val: (String, String)) -> Service { + Service { + service_id: val.0, + version: val.1, + } + } +} + +impl From for (String, String) { + fn from(svc: Service) -> (String, String) { + (svc.service_id, svc.version) + } +} + +impl Service { + pub fn from_hashmap(val: HashMap) -> Vec { + val.into_iter().map(|v| v.into()).collect() + } + + pub fn into_hashmap(service_vec: Vec) -> HashMap { + service_vec.into_iter().map(|v| v.into()).collect() + } +} + +// ServiceChangeTracker tracks the services, their change_count, and the service lookup registry +#[derive(Debug)] +pub struct ServiceChangeTracker { + service_list: Vec, + service_registry: ServiceRegistry, + service_versions: HashMap, + change_count: u32, +} + +#[derive(Deserialize)] +pub struct MegaphoneAPIResponse { + pub broadcasts: HashMap, +} + +impl ServiceChangeTracker { + /// Creates a new `ServiceChangeTracker` initialized with the provided `services`. + pub fn new(services: Vec) -> ServiceChangeTracker { + let mut svc_change_tracker = ServiceChangeTracker { + service_list: Vec::new(), + service_registry: ServiceRegistry::new(), + service_versions: HashMap::new(), + change_count: 0, + }; + for srv in services { + let key = svc_change_tracker + .service_registry + .add_service(srv.service_id); + svc_change_tracker.service_versions.insert(key, srv.version); + } + svc_change_tracker + } + + /// Creates a new `ServiceChangeTracker` initialized from a Megaphone API server version set + /// as provided as the fetch URL. + /// + /// This method uses a synchronous HTTP call. + pub fn with_api_services(url: &str, token: &str) -> reqwest::Result { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(1)) + .build()?; + let MegaphoneAPIResponse { broadcasts } = client + .get(url) + .header(reqwest::header::Authorization(token.to_string())) + .send()? + .error_for_status()? + .json()?; + let services = Service::from_hashmap(broadcasts); + Ok(ServiceChangeTracker::new(services)) + } + + /// Add a new service to the ServiceChangeTracker, triggering a change_count increase. + /// Note: If the service already exists, it will be updated instead. + pub fn add_service(&mut self, service: Service) -> u32 { + if let Ok(change_count) = self.update_service(service.clone()) { + return change_count; + } + self.change_count += 1; + let key = self.service_registry.add_service(service.service_id); + self.service_versions.insert(key, service.version); + self.service_list.push(ServiceRevision { + change_count: self.change_count, + service: key, + }); + self.change_count + } + + /// Update a `service` to a new revision, triggering a change_count increase. + /// + /// Returns an error if the `service` was never initialized/added. + pub fn update_service(&mut self, service: Service) -> Result { + let key = self.service_registry + .lookup_key(&service.service_id) + .ok_or("Service not found")?; + + if let Some(ver) = self.service_versions.get_mut(&key) { + if *ver == service.version { + return Ok(self.change_count); + } + *ver = service.version; + } else { + return Err("Service not found".into()); + } + + // Check to see if this service has been updated since initialization + let svc_index = self.service_list + .iter() + .enumerate() + .filter_map(|(i, svc)| if svc.service == key { Some(i) } else { None }) + .nth(0); + self.change_count += 1; + if let Some(svc_index) = svc_index { + let mut svc = self.service_list.remove(svc_index); + svc.change_count = self.change_count; + self.service_list.push(svc); + } else { + self.service_list.push(ServiceRevision { + change_count: self.change_count, + service: key, + }) + } + Ok(self.change_count) + } + + /// Returns the new service versions since the provided `client_set`. + pub fn change_count_delta(&self, client_set: &mut ClientServices) -> Option> { + if self.change_count <= client_set.change_count { + return None; + } + let mut svc_delta = Vec::new(); + for svc in self.service_list.iter().rev() { + if svc.change_count <= client_set.change_count { + break; + } + if !client_set.service_list.contains(&svc.service) { + continue; + } + if let Some(ver) = self.service_versions.get(&svc.service) { + if let Some(svc_id) = self.service_registry.lookup_id(svc.service) { + svc_delta.push(Service { + service_id: svc_id, + version: (*ver).clone(), + }); + } + } + } + client_set.change_count = self.change_count; + if svc_delta.is_empty() { + None + } else { + Some(svc_delta) + } + } + + /// Returns a delta for `services` that are out of date with the latest version and a new + /// `ClientSet``. + pub fn service_delta(&self, services: &[Service]) -> ServiceClientInit { + let mut svc_list = Vec::new(); + let mut svc_delta = Vec::new(); + for svc in services.iter() { + if let Some(svc_key) = self.service_registry.lookup_key(&svc.service_id) { + if let Some(ver) = self.service_versions.get(&svc_key) { + if *ver != svc.version { + svc_delta.push(Service { + service_id: svc.service_id.clone(), + version: (*ver).clone(), + }); + } + } + svc_list.push(svc_key); + } + } + ServiceClientInit( + ClientServices { + service_list: svc_list, + change_count: self.change_count, + }, + svc_delta, + ) + } + + /// Update a `ClientServices` to account for a new service. + /// + /// Returns services that have changed. + pub fn client_service_add_service( + &self, + client_service: &mut ClientServices, + services: &[Service], + ) -> Option> { + let mut svc_delta = self.change_count_delta(client_service) + .unwrap_or_default(); + for svc in services.iter() { + if let Some(svc_key) = self.service_registry.lookup_key(&svc.service_id) { + if let Some(ver) = self.service_versions.get(&svc_key) { + if *ver != svc.version { + svc_delta.push(Service { + service_id: svc.service_id.clone(), + version: (*ver).clone(), + }); + } + } + client_service.service_list.push(svc_key) + } + } + if svc_delta.is_empty() { + None + } else { + Some(svc_delta) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_service_base() -> Vec { + vec![ + Service { + service_id: String::from("svca"), + version: String::from("rev1"), + }, + Service { + service_id: String::from("svcb"), + version: String::from("revalha"), + }, + ] + } + + #[test] + fn test_service_change_tracker() { + let services = make_service_base(); + let client_services = services.clone(); + let mut svc_chg_tracker = ServiceChangeTracker::new(services); + let ServiceClientInit(mut client_svc, delta) = + svc_chg_tracker.service_delta(&client_services); + assert_eq!(delta.len(), 0); + assert_eq!(client_svc.change_count, 0); + assert_eq!(client_svc.service_list.len(), 2); + + svc_chg_tracker + .update_service(Service { + service_id: String::from("svca"), + version: String::from("rev2"), + }) + .ok(); + let delta = svc_chg_tracker.change_count_delta(&mut client_svc); + assert!(delta.is_some()); + let delta = delta.unwrap(); + assert_eq!(delta.len(), 1); + } + + #[test] + fn test_service_change_handles_new_services() { + let services = make_service_base(); + let client_services = services.clone(); + let mut svc_chg_tracker = ServiceChangeTracker::new(services); + let ServiceClientInit(mut client_svc, _) = svc_chg_tracker.service_delta(&client_services); + + svc_chg_tracker.add_service(Service { + service_id: String::from("svcc"), + version: String::from("revmega"), + }); + let delta = svc_chg_tracker.change_count_delta(&mut client_svc); + assert!(delta.is_none()); + + let delta = svc_chg_tracker + .client_service_add_service( + &mut client_svc, + &vec![ + Service { + service_id: String::from("svcc"), + version: String::from("revision_alpha"), + }, + ], + ) + .unwrap(); + assert_eq!(delta.len(), 1); + assert_eq!(delta[0].version, String::from("revmega")); + assert_eq!(client_svc.change_count, 1); + assert_eq!(svc_chg_tracker.service_list.len(), 1); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..47ffca91 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,42 @@ +//! Various small utilities accumulated over time for the WebPush server +use std::time::Duration; + +use futures::future::{Either, Future, IntoFuture}; +use tokio_core::reactor::{Handle, Timeout}; + +use errors::*; + +pub mod megaphone; +mod rc; +mod send_all; +pub mod timing; +mod user_agent; + +pub use self::rc::RcObject; +pub use self::send_all::MySendAll; +pub use self::timing::{ms_since_epoch, sec_since_epoch, us_since_epoch}; +pub use self::user_agent::parse_user_agent; + +/// Convenience future to time out the resolution of `f` provided within the +/// duration provided. +/// +/// If the `dur` is `None` then the returned future is equivalent to `f` (no +/// timeout) and otherwise the returned future will cancel `f` and resolve to an +/// error if the `dur` timeout elapses before `f` resolves. +pub fn timeout(f: F, dur: Option, handle: &Handle) -> MyFuture +where + F: Future + 'static, + F::Error: Into, +{ + let dur = match dur { + Some(dur) => dur, + None => return Box::new(f.map_err(|e| e.into())), + }; + let timeout = Timeout::new(dur, handle).into_future().flatten(); + Box::new(f.select2(timeout).then(|res| match res { + Ok(Either::A((item, _timeout))) => Ok(item), + Err(Either::A((e, _timeout))) => Err(e.into()), + Ok(Either::B(((), _item))) => Err("timed out".into()), + Err(Either::B((e, _item))) => Err(e.into()), + })) +} diff --git a/src/util/rc.rs b/src/util/rc.rs new file mode 100644 index 00000000..ad82524f --- /dev/null +++ b/src/util/rc.rs @@ -0,0 +1,53 @@ +use std::cell::{RefCell, RefMut}; +use std::rc::Rc; + +use futures::{Poll, Sink, StartSend, Stream}; + +/// Helper object to turn `Rc>` into a `Stream` and `Sink` +/// +/// This is basically just a helper to allow multiple "owning" references to a +/// `T` which is both a `Stream` and a `Sink`. Similar to `Stream::split` in the +/// futures crate, but doesn't actually split it (and allows internal access). +pub struct RcObject(Rc>); + +impl RcObject { + pub fn new(t: T) -> RcObject { + RcObject(Rc::new(RefCell::new(t))) + } + + pub fn borrow_mut(&self) -> RefMut { + self.0.borrow_mut() + } +} + +impl Stream for RcObject { + type Item = T::Item; + type Error = T::Error; + + fn poll(&mut self) -> Poll, T::Error> { + self.0.borrow_mut().poll() + } +} + +impl Sink for RcObject { + type SinkItem = T::SinkItem; + type SinkError = T::SinkError; + + fn start_send(&mut self, msg: T::SinkItem) -> StartSend { + self.0.borrow_mut().start_send(msg) + } + + fn poll_complete(&mut self) -> Poll<(), T::SinkError> { + self.0.borrow_mut().poll_complete() + } + + fn close(&mut self) -> Poll<(), T::SinkError> { + self.0.borrow_mut().close() + } +} + +impl Clone for RcObject { + fn clone(&self) -> RcObject { + RcObject(self.0.clone()) + } +} diff --git a/src/util/send_all.rs b/src/util/send_all.rs new file mode 100644 index 00000000..bc1bdec0 --- /dev/null +++ b/src/util/send_all.rs @@ -0,0 +1,91 @@ +use futures::stream::Fuse; +use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; + +// This is a copy of `Future::forward`, except that it doesn't close the sink +// when it's finished. +pub struct MySendAll { + sink: Option, + stream: Option>, + buffered: Option, +} + +impl MySendAll +where + U: Sink, + T: Stream, + T::Error: From, +{ + #[allow(unused)] + pub fn new(t: T, u: U) -> MySendAll { + MySendAll { + sink: Some(u), + stream: Some(t.fuse()), + buffered: None, + } + } + + fn sink_mut(&mut self) -> &mut U { + self.sink + .as_mut() + .take() + .expect("Attempted to poll MySendAll after completion") + } + + fn stream_mut(&mut self) -> &mut Fuse { + self.stream + .as_mut() + .take() + .expect("Attempted to poll MySendAll after completion") + } + + fn take_result(&mut self) -> (T, U) { + let sink = self.sink + .take() + .expect("Attempted to poll MySendAll after completion"); + let fuse = self.stream + .take() + .expect("Attempted to poll MySendAll after completion"); + (fuse.into_inner(), sink) + } + + fn try_start_send(&mut self, item: T::Item) -> Poll<(), U::SinkError> { + debug_assert!(self.buffered.is_none()); + if let AsyncSink::NotReady(item) = try!(self.sink_mut().start_send(item)) { + self.buffered = Some(item); + return Ok(Async::NotReady); + } + Ok(Async::Ready(())) + } +} + +impl Future for MySendAll +where + U: Sink, + T: Stream, + T::Error: From, +{ + type Item = (T, U); + type Error = T::Error; + + fn poll(&mut self) -> Poll<(T, U), T::Error> { + // If we've got an item buffered already, we need to write it to the + // sink before we can do anything else + if let Some(item) = self.buffered.take() { + try_ready!(self.try_start_send(item)) + } + + loop { + match try!(self.stream_mut().poll()) { + Async::Ready(Some(item)) => try_ready!(self.try_start_send(item)), + Async::Ready(None) => { + try_ready!(self.sink_mut().poll_complete()); + return Ok(Async::Ready(self.take_result())); + } + Async::NotReady => { + try_ready!(self.sink_mut().poll_complete()); + return Ok(Async::NotReady); + } + } + } + } +} diff --git a/src/util/timing.rs b/src/util/timing.rs new file mode 100644 index 00000000..7c7a7e47 --- /dev/null +++ b/src/util/timing.rs @@ -0,0 +1,18 @@ +use chrono::prelude::*; + +/// Get the time since the UNIX epoch in seconds +pub fn sec_since_epoch() -> u64 { + Utc::now().timestamp() as u64 +} + +/// Get the time since the UNIX epoch in milliseconds +pub fn ms_since_epoch() -> u64 { + Utc::now().timestamp_millis() as u64 +} + +/// Get the time since the UNIX epoch in microseconds +#[allow(dead_code)] +pub fn us_since_epoch() -> u64 { + let now = Utc::now(); + (now.timestamp() as u64) * 1_000_000 + (now.timestamp_subsec_micros() as u64) +} diff --git a/src/util/user_agent.rs b/src/util/user_agent.rs new file mode 100644 index 00000000..9b337db9 --- /dev/null +++ b/src/util/user_agent.rs @@ -0,0 +1,92 @@ +use woothee::parser::{Parser, WootheeResult}; + +// List of valid user-agent attributes to keep, anything not in this +// list is considered 'Other'. We log the user-agent on connect always +// to retain the full string, but for DD more tags are expensive so we +// limit to these. +const VALID_UA_BROWSER: &[&str] = &["Chrome", "Firefox", "Safari", "Opera"]; + +// See dataset.rs in https://github.com/woothee/woothee-rust for the +// full list (WootheeResult's 'os' field may fall back to its 'name' +// field). Windows has many values and we only care that its Windows +const VALID_UA_OS: &[&str] = &["Firefox OS", "Linux", "Mac OSX"]; + +pub fn parse_user_agent<'a>( + parser: &'a Parser, + agent: &str, +) -> (WootheeResult<'a>, &'a str, &'a str) { + let wresult = parser.parse(&agent).unwrap_or_else(|| WootheeResult { + name: "", + category: "", + os: "", + os_version: "".to_string(), + browser_type: "", + version: "".to_string(), + vendor: "", + }); + + // Determine a base os/browser for metrics' tags + let metrics_os = if wresult.os.starts_with("Windows") { + "Windows" + } else if VALID_UA_OS.contains(&wresult.os) { + wresult.os + } else { + "Other" + }; + let metrics_browser = if VALID_UA_BROWSER.contains(&wresult.name) { + wresult.name + } else { + "Other" + }; + (wresult, metrics_os, metrics_browser) +} + +#[cfg(test)] +mod tests { + use woothee::parser::Parser; + + use super::parse_user_agent; + + #[test] + fn test_linux() { + let agent = r#"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.2) Gecko/20090807 Mandriva Linux/1.9.1.2-1.1mud2009.1 (2009.1) Firefox/3.5.2 FirePHP/0.3,gzip(gfe),gzip(gfe)"#; + let parser = Parser::new(); + let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&parser, &agent); + assert_eq!(metrics_os, "Linux"); + assert_eq!(ua_result.os, "Linux"); + assert_eq!(metrics_browser, "Firefox"); + } + + #[test] + fn test_windows() { + let agent = r#"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)"#; + let parser = Parser::new(); + let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&parser, &agent); + assert_eq!(metrics_os, "Windows"); + assert_eq!(ua_result.os, "Windows 7"); + assert_eq!(metrics_browser, "Firefox"); + } + + #[test] + fn test_osx() { + let agent = + r#"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.5; rv:2.1.1) Gecko/ Firefox/5.0.1"#; + let parser = Parser::new(); + let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&parser, &agent); + assert_eq!(metrics_os, "Mac OSX"); + assert_eq!(ua_result.os, "Mac OSX"); + assert_eq!(metrics_browser, "Firefox"); + } + + #[test] + fn test_other() { + let agent = + r#"BlackBerry9000/4.6.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102"#; + let parser = Parser::new(); + let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&parser, &agent); + assert_eq!(metrics_os, "Other"); + assert_eq!(ua_result.os, "BlackBerry"); + assert_eq!(metrics_browser, "Other"); + assert_eq!(ua_result.name, "UNKNOWN"); + } +}