From 180675c5f7832dc77f80f4909d50cf9d2dedb13e Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 20 May 2024 16:24:48 -0400 Subject: [PATCH] refactor: remove autopush crate (#706) refactor: remove autopush crate --- .cargo/audit.toml | 6 - Cargo.lock | 161 +- Cargo.toml | 4 - autopush-common/Cargo.toml | 1 - autopush-common/src/errors.rs | 2 - autopush/Cargo.toml | 60 - autopush/README.md | 6 - autopush/src/client.rs | 1411 ----------------- autopush/src/db/commands.rs | 527 ------ autopush/src/db/macros.rs | 113 -- autopush/src/db/mod.rs | 597 ------- autopush/src/db/models.rs | 300 ---- autopush/src/db/util.rs | 15 - autopush/src/http.rs | 98 -- autopush/src/main.rs | 108 -- autopush/src/megaphone.rs | 406 ----- autopush/src/server/dispatch.rs | 103 -- autopush/src/server/metrics.rs | 27 - autopush/src/server/middleware/mod.rs | 3 - autopush/src/server/middleware/sentry2.rs | 301 ---- autopush/src/server/mod.rs | 1009 ------------ autopush/src/server/protocol.rs | 135 -- autopush/src/server/rc.rs | 53 - autopush/src/server/registry.rs | 117 -- autopush/src/server/tls.rs | 147 -- autopush/src/server/webpush_io.rs | 84 - autopush/src/settings.rs | 250 --- .../integration/test_integration_all_rust.py | 1 - 28 files changed, 17 insertions(+), 6028 deletions(-) delete mode 100644 autopush/Cargo.toml delete mode 100644 autopush/README.md delete mode 100644 autopush/src/client.rs delete mode 100644 autopush/src/db/commands.rs delete mode 100644 autopush/src/db/macros.rs delete mode 100644 autopush/src/db/mod.rs delete mode 100644 autopush/src/db/models.rs delete mode 100644 autopush/src/db/util.rs delete mode 100644 autopush/src/http.rs delete mode 100644 autopush/src/main.rs delete mode 100644 autopush/src/megaphone.rs delete mode 100644 autopush/src/server/dispatch.rs delete mode 100644 autopush/src/server/metrics.rs delete mode 100644 autopush/src/server/middleware/mod.rs delete mode 100644 autopush/src/server/middleware/sentry2.rs delete mode 100644 autopush/src/server/mod.rs delete mode 100644 autopush/src/server/protocol.rs delete mode 100644 autopush/src/server/rc.rs delete mode 100644 autopush/src/server/registry.rs delete mode 100644 autopush/src/server/tls.rs delete mode 100644 autopush/src/server/webpush_io.rs delete mode 100644 autopush/src/settings.rs diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 7620ba284..4127e3904 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,13 +1,7 @@ [advisories] ignore = [ - "RUSTSEC-2020-0071", # Bound by Rusoto 0.42, reqwest 0.9, hyper 0.12, chrono 0.4. - "RUSTSEC-2021-0078", # Bound by Rusoto 0.42, Reqwest 0.9 requiring Hyper 0.12 - "RUSTSEC-2021-0079", # Bound by Rusoto 0.42, Reqwest 0.9, a2 0.5 requiring Hyper 0.12 "RUSTSEC-2021-0124", # Bound by tokio restrictions, rusoto, reqwest, hyper... - "RUSTSEC-2023-0034", # Bound by Rusoto 0.42, Reqwest 0.9, a2 0.5 requiring Hyper 0.12 "RUSTSEC-2023-0052", # Bound by Rusoto 0.47, Rustls 0.20, hyper-rustls 0.22, a2 0.8 - "RUSTSEC-2023-0065", # Bound by tokio-tungstenite - "RUSTSEC-2024-0006", # Bound by Rusoto 0.42 "RUSTSEC-2024-0336", # Bound by hyper-alpn 0.4.1, hyper-rustls 0.22.0/0.23.4, # tokio-rustls 0.22.0/0.23.4 (not affected) ] diff --git a/Cargo.lock b/Cargo.lock index fcf680965..55ffc5197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,7 +100,7 @@ dependencies = [ "http 0.2.12", "httparse", "httpdate", - "itoa 1.0.11", + "itoa", "language-tags", "local-channel", "mime", @@ -278,7 +278,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "itoa 1.0.11", + "itoa", "language-tags", "log", "mime", @@ -818,7 +818,6 @@ dependencies = [ "tokio 0.2.25", "tokio 1.37.0", "tokio-core", - "tungstenite", "url", "uuid", "woothee", @@ -845,7 +844,7 @@ dependencies = [ "futures-util", "h2 0.3.26", "http 0.2.12", - "itoa 1.0.11", + "itoa", "log", "mime", "percent-encoding", @@ -872,12 +871,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - [[package]] name = "base64" version = "0.13.1" @@ -940,25 +933,13 @@ dependencies = [ "serde", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -967,16 +948,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array", ] [[package]] @@ -1015,12 +987,6 @@ version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "bytebuffer" version = "2.2.0" @@ -1357,7 +1323,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array", "typenum", ] @@ -1367,7 +1333,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.7", + "generic-array", "subtle", ] @@ -1479,22 +1445,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1631,12 +1588,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" version = "2.0.2" @@ -1863,15 +1814,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2075,17 +2017,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa 0.4.8", -] - [[package]] name = "http" version = "0.2.12" @@ -2094,7 +2025,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes 1.6.0", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -2105,7 +2036,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes 1.6.0", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -2175,7 +2106,7 @@ dependencies = [ "http-body 0.4.6", "httparse", "httpdate", - "itoa 1.0.11", + "itoa", "pin-project-lite 0.2.14", "socket2", "tokio 1.37.0", @@ -2197,7 +2128,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "httparse", - "itoa 1.0.11", + "itoa", "pin-project-lite 0.2.14", "smallvec 1.13.2", "tokio 1.37.0", @@ -2356,15 +2287,6 @@ dependencies = [ "hashbrown 0.14.3", ] -[[package]] -name = "input_buffer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1b822cc844905551931d6f81608ed5f50a79c1078a4e2b4d42dbc7c1eedfbf" -dependencies = [ - "bytes 0.4.12", -] - [[package]] name = "instant" version = "0.1.12" @@ -2409,12 +2331,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.11" @@ -2604,7 +2520,7 @@ checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug 0.3.1", + "opaque-debug", ] [[package]] @@ -2861,12 +2777,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -4003,7 +3913,7 @@ version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ - "itoa 1.0.11", + "itoa", "ryu", "serde", ] @@ -4024,23 +3934,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.11", + "itoa", "ryu", "serde", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha1" version = "0.10.6" @@ -4062,7 +3960,7 @@ dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.1", + "opaque-debug", ] [[package]] @@ -4417,7 +4315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", - "itoa 1.0.11", + "itoa", "libc", "num-conv", "num_threads", @@ -4874,25 +4772,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0c2bd5aeb7dcd2bb32e472c8872759308495e5eccc942e929a513cd8d36110" -dependencies = [ - "base64 0.11.0", - "byteorder", - "bytes 0.4.12", - "http 0.1.21", - "httparse", - "input_buffer", - "log", - "rand 0.7.3", - "sha-1", - "url", - "utf-8", -] - [[package]] name = "typenum" version = "1.17.0" @@ -4984,12 +4863,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index e791de7c7..41b1f40d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] members = [ "autopush-common", - # Pending removal: https://github.com/mozilla-services/autopush-rs/issues/524 - #"autopush", "autoendpoint", "autoconnect", "autoconnect/autoconnect-common", @@ -104,8 +102,6 @@ tokio-compat-02 = "0.2" tokio-core = "0.1" tokio-io = "0.1" tokio-openssl = "0.6" -# Use older version of tungstenite to support legacy connection server. -tungstenite = { version = "0.9.2", default-features = false } # 0.10+ requires tokio 0.3+ uuid = { version = "1.1", features = ["serde", "v4"] } url = "2.2" diff --git a/autopush-common/Cargo.toml b/autopush-common/Cargo.toml index 4a999d086..ee5e8a0a6 100644 --- a/autopush-common/Cargo.toml +++ b/autopush-common/Cargo.toml @@ -51,7 +51,6 @@ tokio.workspace = true tokio-core.workspace = true # tokio-postgres.workspace = true thiserror.workspace = true -tungstenite.workspace = true uuid.workspace = true url.workspace = true diff --git a/autopush-common/src/errors.rs b/autopush-common/src/errors.rs index 3db5ea8d9..df63a7f9e 100644 --- a/autopush-common/src/errors.rs +++ b/autopush-common/src/errors.rs @@ -90,8 +90,6 @@ impl Serialize for ApcError { #[derive(Error, Debug)] pub enum ApcErrorKind { - #[error(transparent)] - Ws(#[from] tungstenite::Error), #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] diff --git a/autopush/Cargo.toml b/autopush/Cargo.toml deleted file mode 100644 index c428ce824..000000000 --- a/autopush/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "autopush" -version.workspace = true -authors.workspace = true -edition.workspace = true - -[[bin]] -name = "autopush_rs" -path = "src/main.rs" - -[dependencies] -base64.workspace = true -cadence.workspace = true -chrono.workspace = true -config.workspace = true -docopt.workspace = true -fernet.workspace = true -hex.workspace = true -httparse.workspace = true -lazy_static.workspace = true -log.workspace = true -mozsvc-common.workspace = true -openssl.workspace = true -rand.workspace = true -regex.workspace = true -sentry.workspace = true -serde.workspace = true -serde_derive.workspace = true -serde_json.workspace = true -slog.workspace = true -slog-async.workspace = true -slog-mozlog-json.workspace = true -slog-scope.workspace = true -slog-stdlog.workspace = true -slog-term.workspace = true -tungstenite.workspace = true -uuid.workspace = true - -autopush_common = { path = "../autopush-common", features = ["dynamodb"] } -bytes = "0.4" # XXX: pin to < 0.5 for hyper 0.12 -crossbeam-channel = "0.5" -env_logger = "0.11" -thiserror = "1.0" -futures = "0.1.29" # XXX: pin to 0.1 until likely hyper 0.13 -futures-backoff = "0.1.0" -futures-locks = "0.5" # pin to 0.5 until futures update -hyper = "^0.12.36" # pin to hyper 0.12 for now: 0.13 has many changes.. -reqwest = "0.9.24" # XXX: pin to < 0.10 until futures 0.3 -rusoto_core = "0.42.0" # 0.46+ requires futures 0.3+ -rusoto_credential = "0.42.0" # 0.46+ requires futures 0.3+ -rusoto_dynamodb = "0.42.0" # XXX: pin to 0.42 until futures 0.3 -serde_dynamodb = "0.4" # 0.7+ requires carg 0.46+ -signal-hook = "0.3" -# state_machine_future = { version = "0.1.6", features = ["debug_code_generation"] } -state_machine_future = "0.2.0" -tokio-core = "0.1" -tokio-io = "0.1.13" -tokio-openssl = "0.3.0" # XXX: pin to < 0.4 until hyper 0.13 -tokio-tungstenite = { version = "0.9.0", default-features = false } # 0.10+ requires tokio 0.3+ -woothee = "0.13" diff --git a/autopush/README.md b/autopush/README.md deleted file mode 100644 index fe8ac27b4..000000000 --- a/autopush/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Autopush Legacy - -This app is the legacy Autopush websocket server. It will be replaced by -[autoconnect](../autoconnect/), which is still in development. - -The function of this app should be maintained until it is fully replaced. diff --git a/autopush/src/client.rs b/autopush/src/client.rs deleted file mode 100644 index ec9d80afd..000000000 --- a/autopush/src/client.rs +++ /dev/null @@ -1,1411 +0,0 @@ -//! Management of connected clients to a WebPush server -#![allow(dead_code)] - -use cadence::{prelude::*, StatsdClient}; -use futures::future::Either; -use futures::sync::mpsc; -use futures::sync::oneshot::Receiver; -use futures::AsyncSink; -use futures::{future, try_ready}; -use futures::{Async, Future, Poll, Sink, Stream}; -use reqwest::r#async::Client as AsyncClient; -use rusoto_dynamodb::UpdateItemOutput; -use state_machine_future::{transition, RentToOwn, StateMachineFuture}; -use std::cell::RefCell; -use std::mem; -use std::rc::Rc; -use std::time::Duration; -use tokio_core::reactor::Timeout; -use uuid::Uuid; - -use crate::db::{CheckStorageResponse, DynamoDbUser, HelloResponse, RegisterResponse}; -use autopush_common::endpoint::make_endpoint; -use autopush_common::errors::{ApcError, ApcErrorKind}; -use autopush_common::notification::Notification; -use autopush_common::util::{ms_since_epoch, sec_since_epoch, user_agent::UserAgentInfo}; - -use crate::megaphone::{Broadcast, BroadcastSubs}; -use crate::server::protocol::{ClientMessage, ServerMessage, ServerNotification}; -use crate::server::Server; -use crate::MyFuture; - -/// Created and handed to the AutopushServer -pub struct RegisteredClient { - /// The User Agent ID (assigned to the remote UserAgent by the server) - pub uaid: Uuid, - /// Internally defined User ID - pub uid: Uuid, - /// The inbound channel for delivery of locally routed Push Notifications - pub tx: mpsc::UnboundedSender, -} - -/// Websocket connector client handler -pub struct Client -where - T: Stream - + Sink - + 'static, -{ - /// The current state machine's state - state_machine: UnAuthClientStateFuture, - /// Handle back to the autoconnect server - srv: Rc, - /// List of interested Broadcast/Megaphone subscriptions for this User Agent - broadcast_subs: Rc>, - /// local webpush router state command request channel. - 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) -> Client { - let srv = srv.clone(); - let timeout = - Timeout::new(srv.app_state.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_subs = Rc::new(RefCell::new(Default::default())); - // Initialize the state machine. (UAs start off as Unauthorized ) - let machine_state = UnAuthClientState::start( - UnAuthClientData { - srv: srv.clone(), - ws, - user_agent: uastr, - broadcast_subs: broadcast_subs.clone(), - }, - timeout, - tx.clone(), - rx, - ); - - Self { - state_machine: machine_state, - srv, - broadcast_subs, - tx, - } - } - - /// Determine the difference between the User Agents list of broadcast IDs and the ones - /// currently known by the server - pub fn broadcast_delta(&mut self) -> Option> { - let mut broadcast_subs = self.broadcast_subs.borrow_mut(); - self.srv.broadcast_delta(&mut broadcast_subs) - } - - /// Terminate all connections before shutting down the server. - 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 = ApcError; - - fn poll(&mut self) -> Poll<(), ApcError> { - 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, - - // 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, - sent_from_storage: u32, - last_ping: u64, - stats: SessionStatistics, - deferred_user_registration: Option, - ua_info: UserAgentInfo, -} - -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(), - sent_from_storage: Default::default(), - last_ping: Default::default(), - stats: Default::default(), - deferred_user_registration: Default::default(), - ua_info: 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 { - /// Whether check_storage queries for topic (not "timestamped") messages - include_topic: bool, - /// Flags the need to increment the last read for timestamp for timestamped messages - increment_storage: bool, - /// Whether this client needs to check storage for messages - check: bool, - /// Flags the need to drop the user record if the User record_version is less - /// than the USER_RECORD_VERSION constant. The reset is done after all existing - /// message traffic is sent over. - old_record_version: bool, - rotate_message_table: bool, -} - -impl ClientFlags { - fn new() -> Self { - Self { - include_topic: true, - increment_storage: false, - check: false, - old_record_version: false, - rotate_message_table: false, - } - } -} - -pub struct UnAuthClientData { - srv: Rc, - ws: T, - user_agent: String, - broadcast_subs: 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(ApcErrorKind::GeneralError("Client timed out".into()).into()) - } - Async::NotReady => match self.ws.poll()? { - Async::Ready(None) => { - return Err(ApcErrorKind::GeneralError("Client dropped".into()).into()) - } - Async::Ready(Some(msg)) => Async::Ready(msg), - Async::NotReady => Async::NotReady, - }, - }; - Ok(item) - } -} - -pub struct AuthClientData { - srv: Rc, - ws: T, - webpush: Rc>, - broadcast_subs: Rc>, -} - -impl AuthClientData -where - T: Stream - + Sink - + 'static, -{ - fn input_or_notif(&mut self) -> Poll, ApcError> { - 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(ApcErrorKind::GeneralError("Sending side dropped".into()).into()) - } - Ok(Async::NotReady) => match self.ws.poll()? { - Async::Ready(None) => { - return Err(ApcErrorKind::GeneralError("Client dropped".into()).into()) - } - Async::Ready(Some(msg)) => Either::A(msg), - Async::NotReady => return Ok(Async::NotReady), - }, - Err(_) => return Err(ApcErrorKind::GeneralError("Unexpected error".into()).into()), - }; - Ok(Async::Ready(item)) - } -} - -/* -STATE MACHINE -*/ -#[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(AwaitRegistryConnect))] - AwaitProcessHello { - response: MyFuture, - data: UnAuthClientData, - desired_broadcasts: Vec, - tx: mpsc::UnboundedSender, - rx: mpsc::UnboundedReceiver, - }, - - #[state_machine_future(transitions(AwaitSessionComplete))] - AwaitRegistryConnect { - response: MyFuture, - srv: Rc, - ws: T, - user_agent: String, - webpush: Rc>, - broadcast_subs: Rc>, - }, - - #[state_machine_future(transitions(AwaitRegistryDisconnect))] - AwaitSessionComplete { - auth_state_machine: AuthClientStateFuture, - srv: Rc, - webpush: Rc>, - }, - - #[state_machine_future(transitions(UnAuthDone))] - AwaitRegistryDisconnect { - response: MyFuture<()>, - srv: Rc, - webpush: Rc>, - error: Option, - }, - - #[state_machine_future(ready)] - UnAuthDone(()), - - #[state_machine_future(error)] - GeneralUnauthClientError(ApcError), -} - -impl PollUnAuthClientState for UnAuthClientState -where - T: Stream - + Sink - + 'static, -{ - fn poll_await_hello<'a>( - hello: &'a mut RentToOwn<'a, AwaitHello>, - ) -> Poll, ApcError> { - trace!("State: AwaitHello"); - let (uaid, desired_broadcasts) = { - 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()), - Broadcast::from_hashmap(broadcasts.unwrap_or_default()), - ), - _ => { - return Err(ApcErrorKind::BroadcastError( - "Invalid message, must be hello".into(), - ) - .into()) - } - } - }; - - let AwaitHello { data, tx, rx, .. } = hello.take(); - let connected_at = ms_since_epoch(); - trace!("❓ AwaitHello UAID: {:?}", uaid); - // Defer registration (don't write the user to the router table yet) - // when no uaid was specified. We'll get back a pending DynamoDbUser - // from the HelloResponse. It'll be potentially written to the db later - // whenever the user first subscribes to a channel_id - // (ClientMessage::Register). - let defer_registration = uaid.is_none(); - let response = Box::new(data.srv.ddb.hello( - connected_at, - uaid.as_ref(), - &data.srv.app_state.router_url, - defer_registration, - )); - transition!(AwaitProcessHello { - response, - data, - desired_broadcasts, - tx, - rx, - }) - } - - fn poll_await_process_hello<'a>( - process_hello: &'a mut RentToOwn<'a, AwaitProcessHello>, - ) -> Poll, ApcError> { - trace!("State: AwaitProcessHello"); - let ( - uaid, - message_month, - check_storage, - old_record_version, - rotate_message_table, - connected_at, - deferred_user_registration, - ) = { - let res = try_ready!(process_hello.response.poll()); - match res { - HelloResponse { - uaid: Some(uaid), - message_month, - check_storage, - old_record_version, - rotate_message_table, - connected_at, - deferred_user_registration, - } => { - trace!("❓ AfterAwaitProcessHello: uaid = {:?}", uaid); - ( - uaid, - message_month, - check_storage, - old_record_version, - rotate_message_table, - connected_at, - deferred_user_registration, - ) - } - HelloResponse { uaid: None, .. } => { - trace!("UAID undefined"); - return Err( - ApcErrorKind::GeneralError("Already connected elsewhere".into()).into(), - ); - } - } - }; - - trace!("❓ post hello: {:?}", &uaid); - - let AwaitProcessHello { - data, - desired_broadcasts, - tx, - rx, - .. - } = process_hello.take(); - let user_is_registered = deferred_user_registration.is_none(); - trace!( - "💬 Taken hello. user_is_registered: {}, {:?}", - user_is_registered, - uaid - ); - data.srv.metrics.incr("ua.command.hello").ok(); - - let UnAuthClientData { - srv, - ws, - user_agent, - broadcast_subs, - } = data; - - // Setup the objects and such needed for a WebPushClient - let mut flags = ClientFlags::new(); - flags.check = check_storage; - flags.old_record_version = old_record_version; - flags.rotate_message_table = rotate_message_table; - let (initialized_subs, broadcasts) = srv.broadcast_init(&desired_broadcasts); - broadcast_subs.replace(initialized_subs); - let uid = Uuid::new_v4(); - let webpush = Rc::new(RefCell::new(WebPushClient { - uaid, - uid, - flags, - rx, - message_month, - connected_at, - stats: SessionStatistics { - uaid: uaid.as_simple().to_string(), - uaid_reset: old_record_version, - existing_uaid: check_storage, - connection_type: String::from("webpush"), - ..Default::default() - }, - deferred_user_registration, - ua_info: UserAgentInfo::from(user_agent.as_ref()), - ..Default::default() - })); - - let response = Box::new( - srv.clients - .connect(RegisteredClient { uaid, uid, tx }) - .and_then(move |_| { - // generate the response message back to the client. - Ok(ServerMessage::Hello { - uaid: uaid.as_simple().to_string(), - status: 200, - use_webpush: Some(true), - broadcasts, - }) - }), - ); - - transition!(AwaitRegistryConnect { - response, - srv, - ws, - user_agent, - webpush, - broadcast_subs, - }) - } - - fn poll_await_registry_connect<'a>( - registry_connect: &'a mut RentToOwn<'a, AwaitRegistryConnect>, - ) -> Poll, ApcError> { - trace!("State: AwaitRegistryConnect"); - let hello_response = try_ready!(registry_connect.response.poll()); - - let AwaitRegistryConnect { - srv, - ws, - webpush, - broadcast_subs, - .. - } = registry_connect.take(); - - let auth_state_machine = AuthClientState::start( - vec![hello_response], - AuthClientData { - srv: srv.clone(), - ws, - webpush: webpush.clone(), - broadcast_subs, - }, - ); - - transition!(AwaitSessionComplete { - auth_state_machine, - srv, - webpush, - }) - } - - fn poll_await_session_complete<'a>( - session_complete: &'a mut RentToOwn<'a, AwaitSessionComplete>, - ) -> Poll { - trace!("State: AwaitSessionComplete"); - let error = { - match session_complete.auth_state_machine.poll() { - Ok(Async::NotReady) => return Ok(Async::NotReady), - Ok(Async::Ready(_)) => None, - - Err(e) => match e.kind { - ApcErrorKind::Ws(_) - | ApcErrorKind::Io(_) - | ApcErrorKind::PongTimeout - | ApcErrorKind::RepeatUaidDisconnect - | ApcErrorKind::ExcessivePing - | ApcErrorKind::InvalidStateTransition(_, _) - | ApcErrorKind::InvalidClientMessage(_) - | ApcErrorKind::SendError => None, - _ => Some(e), - }, - } - }; - - let AwaitSessionComplete { srv, webpush, .. } = session_complete.take(); - - let response = srv - .clients - .disconnect(&webpush.borrow().uaid, &webpush.borrow().uid); - - transition!(AwaitRegistryDisconnect { - response, - srv, - webpush, - error, - }) - } - - fn poll_await_registry_disconnect<'a>( - registry_disconnect: &'a mut RentToOwn<'a, AwaitRegistryDisconnect>, - ) -> Poll { - trace!("State: AwaitRegistryDisconnect"); - try_ready!(registry_disconnect.response.poll()); - - let AwaitRegistryDisconnect { - srv, - webpush, - error, - .. - } = registry_disconnect.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(); - while let Ok(Async::Ready(Some(msg))) = webpush.rx.poll() { - match msg { - ServerNotification::CheckStorage | ServerNotification::Disconnect => continue, - ServerNotification::Notification(notif) => { - webpush.unacked_direct_notifs.push(notif) - } - } - } - let now = ms_since_epoch(); - let elapsed = (now - webpush.connected_at) / 1_000; - let ua_info = webpush.ua_info.clone(); - // dogstatsd doesn't support timers: use histogram instead - srv.metrics - .time_with_tags("ua.connection.lifespan", elapsed) - .with_tag("ua_os_family", &ua_info.metrics_os) - .with_tag("ua_browser_family", &ua_info.metrics_browser) - .send(); - - // Log out the sentry message if applicable and convert to error msg - let error = if let Some(ref err) = error { - let ua_info = ua_info.clone(); - let mut event = sentry::event_from_error(err); - event.exception.last_mut().unwrap().stacktrace = - sentry::integrations::backtrace::backtrace_to_stacktrace(&err.backtrace); - - event.user = Some(sentry::User { - id: Some(webpush.uaid.as_simple().to_string()), - ..Default::default() - }); - event - .tags - .insert("ua_name".to_string(), ua_info.browser_name); - event - .tags - .insert("ua_os_family".to_string(), ua_info.metrics_os); - event - .tags - .insert("ua_os_ver".to_string(), ua_info.os_version); - event - .tags - .insert("ua_browser_family".to_string(), ua_info.metrics_browser); - event - .tags - .insert("ua_browser_ver".to_string(), ua_info.browser_version); - sentry::capture_event(event); - err.to_string() - } else { - "".to_string() - }; - // If there's direct unack'd messages, they need to be saved out without blocking - // here - 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::take(&mut webpush.unacked_direct_notifs); - // 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); - } - save_and_notify_undelivered_messages(&webpush, srv, notifs); - } - - // 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, - "ua_name" => ua_info.browser_name, - "ua_os_family" => ua_info.metrics_os, - "ua_os_ver" => ua_info.os_version, - "ua_browser_family" => ua_info.metrics_browser, - "ua_browser_ver" => ua_info.browser_version, - "ua_category" => ua_info.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, - "disconnect_reason" => error, - ); - transition!(UnAuthDone(())) - } -} - -fn save_and_notify_undelivered_messages( - webpush: &WebPushClient, - srv: Rc, - notifs: Vec, -) { - let srv2 = srv.clone(); - let uaid = webpush.uaid; - let connected_at = webpush.connected_at; - srv.handle.spawn( - srv.ddb - .store_messages(&webpush.uaid, &webpush.message_month, notifs) - .and_then(move |_| { - debug!("Finished saving unacked direct notifications, checking for reconnect"); - srv2.ddb.get_user(&uaid) - }) - .and_then(move |user| { - let user = match user.ok_or_else(|| { - ApcErrorKind::DatabaseError("No user record found".into()).into() - }) { - Ok(user) => user, - Err(e) => return future::err(e), - }; - - // Return an err to stop processing if the user hasn't reconnected yet, otherwise - // attempt to construct a client to make the request - if user.connected_at == connected_at { - future::err(ApcErrorKind::GeneralError("No notify needed".into()).into()) - } else if let Some(node_id) = user.node_id { - let result = AsyncClient::builder() - .timeout(Duration::from_secs(1)) - .build(); - if let Ok(client) = result { - future::ok((client, user.uaid, node_id)) - } else { - future::err( - ApcErrorKind::GeneralError("Unable to build http client".into()).into(), - ) - } - } else { - future::err( - ApcErrorKind::GeneralError("No new node_id, notify not needed".into()) - .into(), - ) - } - }) - .and_then(|(client, uaid, node_id)| { - // Send the notify to the user - let notify_url = format!("{}/notif/{}", node_id, uaid.as_simple()); - client - .put(¬ify_url) - .send() - .map_err(|_| ApcErrorKind::GeneralError("Failed to send".into()).into()) - }) - .then(|_| { - debug!("Finished cleanup"); - Ok(()) - }), - ); -} - -/* -STATE MACHINE: Main engine -*/ -#[derive(StateMachineFuture)] -pub enum AuthClientState -where - T: Stream - + Sink - + 'static, -{ - /// Send one or more locally routed notification messages - #[state_machine_future(start, transitions(AwaitSend, DetermineAck))] - Send { - smessages: Vec, - data: AuthClientData, - }, - - #[state_machine_future(transitions(DetermineAck, Send, AwaitDropUser))] - AwaitSend { - smessages: Vec, - data: AuthClientData, - }, - - #[state_machine_future(transitions( - IncrementStorage, - CheckStorage, - AwaitDropUser, - AwaitMigrateUser, - AwaitInput - ))] - DetermineAck { data: AuthClientData }, - - #[state_machine_future(transitions( - DetermineAck, - Send, - 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(Send, 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(Send))] - AwaitRegister { - channel_id: Uuid, - response: MyFuture, - data: AuthClientData, - }, - - #[state_machine_future(transitions(Send))] - 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)] - GeneralAuthClientStateError(ApcError), -} - -impl PollAuthClientState for AuthClientState -where - T: Stream - + Sink - + 'static, -{ - fn poll_send<'a>(send: &'a mut RentToOwn<'a, Send>) -> Poll, ApcError> { - trace!("State: Send"); - let sent = { - let Send { - ref mut smessages, - ref mut data, - .. - } = **send; - if !smessages.is_empty() { - trace!("🚟 Sending {} msgs: {:#?}", smessages.len(), smessages); - let item = smessages.remove(0); - let ret = data - .ws - .start_send(item) - .map_err(|_e| ApcErrorKind::SendError)?; - match ret { - AsyncSink::Ready => true, - AsyncSink::NotReady(returned) => { - smessages.insert(0, returned); - return Ok(Async::NotReady); - } - } - } else { - false - } - }; - - let Send { smessages, data } = send.take(); - if sent { - transition!(AwaitSend { smessages, data }); - } - transition!(DetermineAck { data }) - } - - fn poll_await_send<'a>( - await_send: &'a mut RentToOwn<'a, AwaitSend>, - ) -> Poll, ApcError> { - trace!("State: AwaitSend"); - try_ready!(await_send.data.ws.poll_complete()); - - let AwaitSend { smessages, data } = await_send.take(); - let webpush_rc = data.webpush.clone(); - let webpush = webpush_rc.borrow(); - if webpush.sent_from_storage > data.srv.app_state.msg_limit { - // Exceeded the max limit of stored messages: drop the user to trigger a - // re-register - debug!("Dropping user: exceeded msg_limit"); - let response = Box::new(data.srv.ddb.drop_uaid(&webpush.uaid)); - transition!(AwaitDropUser { response, data }); - } else if !smessages.is_empty() { - transition!(Send { smessages, data }); - } - transition!(DetermineAck { data }) - } - - fn poll_determine_ack<'a>( - detack: &'a mut RentToOwn<'a, DetermineAck>, - ) -> Poll, ApcError> { - 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"); - data.srv.metrics.incr("ua.rotate_message_table").ok(); - let response = Box::new( - data.srv - .ddb - .migrate_user(&webpush.uaid, &webpush.message_month), - ); - transition!(AwaitMigrateUser { response, data }); - } else if all_acked && webpush.flags.old_record_version { - debug!("Dropping user: flagged old_record_version"); - let response = Box::new(data.srv.ddb.drop_uaid(&webpush.uaid)); - transition!(AwaitDropUser { response, data }); - } - transition!(AwaitInput { data }) - } - - fn poll_await_input<'a>( - r#await: &'a mut RentToOwn<'a, AwaitInput>, - ) -> Poll, ApcError> { - trace!("State: AwaitInput"); - // The following is a blocking call. No action is taken until we either get a - // websocket data packet or there's an incoming notification. - let input = try_ready!(r#await.data.input_or_notif()); - let AwaitInput { data } = r#await.take(); - let webpush_rc = data.webpush.clone(); - let mut webpush = webpush_rc.borrow_mut(); - match input { - Either::A(ClientMessage::Hello { .. }) => Err(ApcErrorKind::InvalidStateTransition( - "AwaitInput".to_string(), - "Hello".to_string(), - ) - .into()), - Either::A(ClientMessage::BroadcastSubscribe { broadcasts }) => { - let broadcast_delta = { - let mut broadcast_subs = data.broadcast_subs.borrow_mut(); - data.srv.process_broadcasts( - &mut broadcast_subs, - &Broadcast::from_hashmap(broadcasts), - ) - }; - - if let Some(response) = broadcast_delta { - transition!(Send { - smessages: vec![ServerMessage::Broadcast { - broadcasts: response, - }], - data, - }); - } else { - transition!(AwaitInput { data }); - } - } - Either::A(ClientMessage::Register { - channel_id: channel_id_str, - key, - }) => { - debug!("Got a register command"; - "uaid" => &webpush.uaid.to_string(), - "channel_id" => &channel_id_str, - ); - let channel_id = Uuid::parse_str(&channel_id_str).map_err(|_e| { - ApcErrorKind::InvalidClientMessage(format!( - "Invalid channelID: {channel_id_str}" - )) - })?; - if channel_id.as_hyphenated().to_string() != channel_id_str { - return Err(ApcErrorKind::InvalidClientMessage(format!( - "Invalid UUID format, not lower-case/dashed: {channel_id}", - )) - .into()); - } - - let uaid = webpush.uaid; - let message_month = webpush.message_month.clone(); - let srv = &data.srv; - let fut = match make_endpoint( - &uaid, - &channel_id, - key.as_deref(), - &srv.app_state.endpoint_url, - &srv.app_state.fernet, - ) { - Ok(endpoint) => srv.ddb.register_channel( - &uaid, - &channel_id, - &message_month, - &endpoint, - webpush.deferred_user_registration.as_ref(), - ), - Err(e) => { - error!("make_endpoint: {:?}", e); - Box::new(future::ok(RegisterResponse::Error { - error_msg: "Failed to generate endpoint".to_string(), - status: 400, - })) - } - }; - 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_channel( - &uaid, - &channel_id, - &message_month, - )); - transition!(AwaitUnregister { - channel_id, - code: code.unwrap_or(200), - response, - data, - }); - } - Either::A(ClientMessage::Nack { code, .. }) => { - // only metric codes expected from the client (or 0) - let mcode = code - .and_then(|code| { - if (301..=303).contains(&code) { - Some(code) - } else { - None - } - }) - .unwrap_or(0); - data.srv - .metrics - .incr_with_tags("ua.command.nack") - .with_tag("code", &mcode.to_string()) - .send(); - 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::A(ClientMessage::Ping) => { - // Clients shouldn't ping > than once per minute or we - // disconnect them - if sec_since_epoch() - webpush.last_ping >= 45 { - trace!("🏓 Got a ping, sending pong"); - webpush.last_ping = sec_since_epoch(); - transition!(Send { - smessages: vec![ServerMessage::Ping], - data, - }) - } else { - trace!("🏓 Got a ping too quickly, disconnecting"); - Err(ApcErrorKind::ExcessivePing.into()) - } - } - Either::B(ServerNotification::Notification(notif)) => { - if notif.ttl != 0 { - webpush.unacked_direct_notifs.push(notif.clone()); - } - debug!("Got a notification to send, sending!"); - emit_metrics_for_send(&data.srv.metrics, ¬if, "Direct", &webpush.ua_info); - transition!(Send { - smessages: vec![ServerMessage::Notification(notif)], - 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(ApcErrorKind::RepeatUaidDisconnect.into()) - } - } - } - - fn poll_increment_storage<'a>( - increment_storage: &'a mut RentToOwn<'a, IncrementStorage>, - ) -> Poll, ApcError> { - trace!("State: IncrementStorage"); - let webpush_rc = increment_storage.data.webpush.clone(); - let webpush = webpush_rc.borrow(); - let timestamp = webpush - .unacked_stored_highest - .ok_or_else(|| ApcErrorKind::GeneralError("unacked_stored_highest unset".into()))? - .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, ApcError> { - 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, ApcError> { - 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, ApcError> { - trace!("State: AwaitCheckStorage"); - let CheckStorageResponse { - include_topic, - mut messages, - timestamp, - } = try_ready!(await_check_storage.response.poll()); - 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() { - webpush.flags.check = false; - webpush.sent_from_storage = 0; - transition!(DetermineAck { data }); - } - - // Filter out TTL expired messages - let now = sec_since_epoch(); - let srv = data.srv.clone(); - messages.retain(|n| { - if !n.expired(now) { - return true; - } - if n.sortkey_timestamp.is_none() { - srv.handle.spawn( - srv.ddb - .delete_message(&webpush.message_month, &webpush.uaid, n) - .then(|_| { - debug!("Deleting expired message without sortkey_timestamp"); - Ok(()) - }), - ); - } - false - }); - 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()); - let smessages: Vec<_> = messages - .into_iter() - .inspect(|msg| { - emit_metrics_for_send(&data.srv.metrics, msg, "Stored", &webpush.ua_info) - }) - .map(ServerMessage::Notification) - .collect(); - webpush.sent_from_storage += smessages.len() as u32; - transition!(Send { smessages, data }) - } else { - // No messages remaining - transition!(DetermineAck { data }) - } - } - - fn poll_await_migrate_user<'a>( - await_migrate_user: &'a mut RentToOwn<'a, AwaitMigrateUser>, - ) -> Poll, ApcError> { - 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.ddb.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, ApcError> { - 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(), - } - } - }; - - let data = await_register.take().data; - { - let mut webpush = data.webpush.borrow_mut(); - // If we completed a deferred user registration during a channel - // subscription (Client::Register), we're now all done with it - webpush.deferred_user_registration = None; - } - - transition!(Send { - smessages: vec![msg], - data, - }) - } - - fn poll_await_unregister<'a>( - await_unregister: &'a mut RentToOwn<'a, AwaitUnregister>, - ) -> Poll, ApcError> { - 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!(Send { - smessages: vec![msg], - data, - }) - } - - fn poll_await_delete<'a>( - await_delete: &'a mut RentToOwn<'a, AwaitDelete>, - ) -> Poll, ApcError> { - trace!("State: AwaitDelete"); - try_ready!(await_delete.response.poll()); - transition!(DetermineAck { - data: await_delete.take().data, - }) - } -} - -fn emit_metrics_for_send( - metrics: &StatsdClient, - notif: &Notification, - source: &'static str, - user_agent_info: &UserAgentInfo, -) { - metrics - .incr_with_tags("ua.notification.sent") - .with_tag("source", source) - .with_tag("topic", ¬if.topic.is_some().to_string()) - .with_tag("os", &user_agent_info.metrics_os) - // TODO: include `internal` if meta is set - .send(); - metrics - .count_with_tags( - "ua.message_data", - notif.data.as_ref().map_or(0, |data| data.len() as i64), - ) - .with_tag("source", source) - .with_tag("os", &user_agent_info.metrics_os) - .send(); -} diff --git a/autopush/src/db/commands.rs b/autopush/src/db/commands.rs deleted file mode 100644 index 2305ac88f..000000000 --- a/autopush/src/db/commands.rs +++ /dev/null @@ -1,527 +0,0 @@ -use std::collections::HashSet; -use std::fmt::{Debug, Display}; -use std::result::Result as StdResult; -use std::sync::Arc; -use uuid::Uuid; - -use cadence::{CountedExt, StatsdClient}; -use chrono::Utc; -use futures::{future, Future}; -use futures_backoff::retry_if; -use rusoto_core::RusotoError; -use rusoto_dynamodb::{ - AttributeValue, BatchWriteItemError, DeleteItemError, DeleteItemInput, DeleteItemOutput, - DynamoDb, DynamoDbClient, GetItemError, GetItemInput, GetItemOutput, ListTablesInput, - ListTablesOutput, PutItemError, PutItemInput, PutItemOutput, QueryError, QueryInput, - UpdateItemError, UpdateItemInput, UpdateItemOutput, -}; - -use autopush_common::errors::{ApcError, ApcErrorKind, Result}; -use autopush_common::notification::Notification; -use autopush_common::util::timing::sec_since_epoch; - -use super::models::{DynamoDbNotification, DynamoDbUser}; -use super::util::generate_last_connect; -use super::{HelloResponse, MAX_EXPIRY, USER_RECORD_VERSION}; -use crate::MyFuture; - -macro_rules! retryable_error { - ($name:ident, $type:ty, $property:ident) => { - pub fn $name(err: &RusotoError<$type>) -> bool { - match err { - RusotoError::Service($property::InternalServerError(_)) - | RusotoError::Service($property::ProvisionedThroughputExceeded(_)) => true, - _ => false, - } - } - }; -} - -retryable_error!( - retryable_batchwriteitem_error, - BatchWriteItemError, - BatchWriteItemError -); -retryable_error!(retryable_query_error, QueryError, QueryError); -retryable_error!(retryable_delete_error, DeleteItemError, DeleteItemError); -retryable_error!(retryable_getitem_error, GetItemError, GetItemError); -retryable_error!(retryable_putitem_error, PutItemError, PutItemError); -retryable_error!(retryable_updateitem_error, UpdateItemError, UpdateItemError); - -#[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) - }) -} - -/// A blocking list_tables call only called during initialization -/// (prior to an any active tokio executor) -pub fn list_tables_sync( - ddb: &DynamoDbClient, - start_key: Option, -) -> Result { - let input = ListTablesInput { - exclusive_start_table_name: start_key, - limit: Some(100), - }; - ddb.list_tables(input) - .sync() - .map_err(|_| ApcErrorKind::DatabaseError("Unable to list tables".into()).into()) -} - -/// Pull all pending messages for the user from storage -pub fn fetch_topic_messages( - ddb: DynamoDbClient, - metrics: Arc, - table_name: &str, - uaid: &Uuid, - limit: u32, -) -> impl Future { - let attr_values = hashmap! { - ":uaid".to_string() => val!(S => uaid.as_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() - }; - - retry_if(move || ddb.query(input.clone()), retryable_query_error) - .map_err(|_| ApcErrorKind::MessageFetch.into()) - .and_then(move |output| { - let mut notifs: Vec = - output.items.map_or_else(Vec::new, |items| { - debug!("Got response of: {:?}", items); - items - .into_iter() - .inspect(|i| debug!("Item: {:?}", i)) - .filter_map(|item| { - let item2 = item.clone(); - ok_or_inspect(serde_dynamodb::from_hashmap(item), |e| { - conversion_err(&metrics, e, item2, "serde_dynamodb_from_hashmap") - }) - }) - .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 - let messages = notifs - .into_iter() - .filter_map(|ddb_notif| { - let ddb_notif2 = ddb_notif.clone(); - ok_or_inspect(ddb_notif.into_notif(), |e| { - conversion_err(&metrics, e, ddb_notif2, "into_notif") - }) - }) - .collect(); - Ok(FetchMessageResponse { - timestamp, - messages, - }) - }) -} - -/// Pull messages older than a given timestamp for a given user. -/// This also returns the latest message timestamp. -pub fn fetch_timestamp_messages( - ddb: DynamoDbClient, - metrics: Arc, - table_name: &str, - uaid: &Uuid, - timestamp: Option, - limit: usize, -) -> impl Future { - let range_key = if let Some(ts) = timestamp { - format!("02:{ts}:z") - } else { - "01;".to_string() - }; - let attr_values = hashmap! { - ":uaid".to_string() => val!(S => uaid.as_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() - }; - - retry_if(move || ddb.query(input.clone()), retryable_query_error) - .map_err(|_| ApcErrorKind::MessageFetch.into()) - .and_then(move |output| { - let messages = output.items.map_or_else(Vec::new, |items| { - debug!("Got response of: {:?}", items); - items - .into_iter() - .filter_map(|item| { - let item2 = item.clone(); - ok_or_inspect(serde_dynamodb::from_hashmap(item), |e| { - conversion_err(&metrics, e, item2, "serde_dynamodb_from_hashmap") - }) - }) - .filter_map(|ddb_notif: DynamoDbNotification| { - let ddb_notif2 = ddb_notif.clone(); - ok_or_inspect(ddb_notif.into_notif(), |e| { - conversion_err(&metrics, e, ddb_notif2, "into_notif") - }) - }) - .collect() - }); - let timestamp = messages.iter().filter_map(|m| m.sortkey_timestamp).max(); - Ok(FetchMessageResponse { - timestamp, - messages, - }) - }) -} - -/// Drop all user information from the Router table. -pub fn drop_user( - ddb: DynamoDbClient, - uaid: &Uuid, - router_table_name: &str, -) -> impl Future { - let input = DeleteItemInput { - table_name: router_table_name.to_string(), - key: ddb_item! { uaid: s => uaid.as_simple().to_string() }, - ..Default::default() - }; - retry_if( - move || ddb.delete_item(input.clone()), - retryable_delete_error, - ) - .map_err(|_| ApcErrorKind::DatabaseError("Error dropping user".into()).into()) -} - -/// Get the user information from the Router table. -pub fn get_uaid( - ddb: DynamoDbClient, - 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.as_simple().to_string() }, - ..Default::default() - }; - retry_if(move || ddb.get_item(input.clone()), retryable_getitem_error).map_err(|e| { - error!("get_uaid: {:?}", e.to_string()); - ApcErrorKind::DatabaseError("Error fetching user".into()).into() - }) -} - -/// Register a user into the Router table. -pub fn register_user( - ddb: DynamoDbClient, - user: &DynamoDbUser, - router_table: &str, -) -> impl Future { - let item = match serde_dynamodb::to_hashmap(user) { - Ok(item) => item, - Err(e) => { - return future::Either::A(future::err( - ApcErrorKind::DatabaseError(e.to_string()).into(), - )) - } - }; - 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), - }; - - future::Either::B( - retry_if( - move || { - debug!("🧑🏼 Registering user into {}: {:?}", router_table, 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() - }) - }, - retryable_putitem_error, - ) - .map_err(|_| ApcErrorKind::DatabaseError("Error registering user".to_string()).into()), - ) -} - -/// Update the user's message month (Note: This is legacy for DynamoDB, but may still -/// be used by Stand Alone systems.) -pub fn update_user_message_month( - ddb: DynamoDbClient, - 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.as_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.clone()) - .and_then(|_| future::ok(())) - }, - retryable_updateitem_error, - ) - .map_err(|_e| ApcErrorKind::DatabaseError("Error updating user message month".into()).into()) -} - -/// Return all known Channels for a given User. -pub fn all_channels( - ddb: DynamoDbClient, - uaid: &Uuid, - message_table_name: &str, -) -> impl Future, Error = ApcError> { - let input = GetItemInput { - table_name: message_table_name.to_string(), - consistent_read: Some(true), - key: ddb_item! { - uaid: s => uaid.as_simple().to_string(), - chidmessageid: s => " ".to_string() - }, - ..Default::default() - }; - - retry_if(move || ddb.get_item(input.clone()), retryable_getitem_error) - .and_then(|output| { - let channels = output - .item - .and_then(|item| { - serde_dynamodb::from_hashmap(item) - .ok() - .and_then(|notif: DynamoDbNotification| notif.chids) - }) - .unwrap_or_default(); - future::ok(channels) - }) - .or_else(|_err| future::ok(HashSet::new())) -} - -/// Save the current list of Channels for a given user. -pub fn save_channels( - ddb: DynamoDbClient, - 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.as_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.clone()) - .and_then(|_| future::ok(())) - }, - retryable_updateitem_error, - ) - .map_err(|_e| ApcErrorKind::DatabaseError("Error saving channels".into()).into()) -} - -/// Remove a specific channel from the list of known channels for a given User -pub fn unregister_channel_id( - ddb: DynamoDbClient, - uaid: &Uuid, - channel_id: &Uuid, - message_table_name: &str, -) -> impl Future { - let chid = channel_id.as_hyphenated().to_string(); - let attr_values = hashmap! { - ":channel_id".to_string() => val!(SS => [chid]), - }; - let update_item = UpdateItemInput { - key: ddb_item! { - uaid: s => uaid.as_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.clone()), - retryable_updateitem_error, - ) - .map_err(|_e| ApcErrorKind::DatabaseError("Error unregistering channel".into()).into()) -} - -/// Respond with user information for a given user. -#[allow(clippy::too_many_arguments)] -pub fn lookup_user( - ddb: DynamoDbClient, - metrics: Arc, - uaid: &Uuid, - connected_at: u64, - router_url: &str, - router_table_name: &str, - message_table_names: &[String], - current_message_month: &str, -) -> 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 router_url = router_url.to_string(); - let response = response.and_then(move |data| -> MyFuture<_> { - let mut hello_response = HelloResponse { - message_month: cur_month.clone(), - connected_at, - ..Default::default() - }; - let user = handle_user_result( - &cur_month, - &messages_tables, - connected_at, - router_url, - data, - &mut hello_response, - ); - match user { - Ok(user) => { - trace!("🧑 returning user: {:?}", user.uaid); - Box::new(future::ok((hello_response, Some(user)))) - } - Err((false, _)) => { - trace!("🧑 handle_user_result false, _: {:?}", uaid2); - Box::new(future::ok((hello_response, None))) - } - Err((true, code)) => { - trace!("🧑 handle_user_result true, {}: {:?}", uaid2, 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))) - .map_err(|_e| ApcErrorKind::DatabaseError("Unable to drop user".into()).into()); - 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: &str, - 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().ok_or((true, 104))?; - if !messages_tables.contains(&user_month) { - return Err((true, 105)); - } - hello_response.check_storage = true; - hello_response.rotate_message_table = user_month != *cur_month; - hello_response.message_month = user_month; - hello_response.old_record_version = 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) -} - -/// Like `Result::ok`, convert from `Result` to `Option` but applying a -/// function to the Err value -fn ok_or_inspect(result: StdResult, op: F) -> Option -where - F: FnOnce(E), -{ - match result { - Ok(t) => Some(t), - Err(e) => { - op(e); - None - } - } -} - -/// Log/metric errors during conversions to Notification -fn conversion_err(metrics: &StatsdClient, err: E, item: F, name: &'static str) -where - E: Display, - F: Debug, -{ - error!("Failed {}, item: {:?}, conversion: {}", name, item, err); - metrics - .incr_with_tags("ua.notification_read.error") - .with_tag("conversion", name) - .send(); -} diff --git a/autopush/src/db/macros.rs b/autopush/src/db/macros.rs deleted file mode 100644 index bb944e0f2..000000000 --- a/autopush/src/db/macros.rs +++ /dev/null @@ -1,113 +0,0 @@ -/// 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_export] -macro_rules! val { - (B => $val:expr) => { - AttributeValue { - b: Some($val), - ..Default::default() - } - }; - (S => $val:expr) => { - AttributeValue { - s: Some($val.to_string()), - ..Default::default() - } - }; - (SS => $val:expr) => { - AttributeValue { - ss: Some($val.iter().map(|v| v.to_string()).collect()), - ..Default::default() - } - }; - (N => $val:expr) => { - AttributeValue { - n: Some($val.to_string()), - ..Default::default() - } - }; -} - -/// Create a **HashMap** from a list of key-value pairs -/// -/// ## Example -/// -/// ```ignore -/// use autopush_common::hashmap; -/// -/// let map = hashmap!{ -/// "a" => 1, -/// "b" => 2, -/// }; -/// assert_eq!(map["a"], 1); -/// assert_eq!(map["b"], 2); -/// assert_eq!(map.get("c"), None); -/// ``` -#[macro_export] -macro_rules! hashmap { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$($crate::hashmap!(@single $rest)),*])); - - ($($key:expr => $value:expr,)+) => { $crate::hashmap!($($key => $value),+) }; - ($($key:expr => $value:expr),*) => { - { - let _cap = $crate::hashmap!(@count $($key),*); - let mut _map = ::std::collections::HashMap::with_capacity(_cap); - $( - _map.insert($key, $value); - )* - _map - } - }; -} - -/// Shorthand for specifying a dynamodb item -#[macro_export] -macro_rules! ddb_item { - ($($p:tt: $t:tt => $x:expr),*) => { - { - use rusoto_dynamodb::AttributeValue; - $crate::hashmap!{ - $( - String::from(stringify!($p)) => AttributeValue { - $t: Some($x), - ..Default::default() - }, - )* - } - } - } -} diff --git a/autopush/src/db/mod.rs b/autopush/src/db/mod.rs deleted file mode 100644 index beb223b2c..000000000 --- a/autopush/src/db/mod.rs +++ /dev/null @@ -1,597 +0,0 @@ -use std::collections::HashSet; -use std::env; -use std::sync::Arc; -use uuid::Uuid; - -use cadence::{Counted, CountedExt, StatsdClient}; -use futures::{future, Future}; -use futures_backoff::retry_if; -use rusoto_core::{HttpClient, Region}; -use rusoto_credential::StaticProvider; -use rusoto_dynamodb::{ - AttributeValue, BatchWriteItemInput, DeleteItemInput, DynamoDb, DynamoDbClient, PutItemInput, - PutRequest, UpdateItemInput, UpdateItemOutput, WriteRequest, -}; - -#[macro_use] -mod macros; -mod commands; -mod models; -mod util; - -use autopush_common::errors::{ApcError, ApcErrorKind, Result}; -use autopush_common::notification::Notification; -use autopush_common::util::timing::sec_since_epoch; - -use self::commands::{ - retryable_batchwriteitem_error, retryable_delete_error, retryable_putitem_error, - retryable_updateitem_error, FetchMessageResponse, -}; -pub use self::models::{DynamoDbNotification, DynamoDbUser}; -use crate::MyFuture; - -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 old_record_version: bool, - pub rotate_message_table: bool, - pub connected_at: u64, - // Exists when we didn't register this user during HELLO - pub deferred_user_registration: Option, -} - -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 }, -} - -#[derive(Clone)] -pub struct DynamoStorage { - ddb: DynamoDbClient, - metrics: Arc, - router_table_name: String, - pub message_table_names: Vec, - pub current_message_month: String, -} - -impl DynamoStorage { - pub fn from_settings( - message_table_name: &str, - router_table_name: &str, - metrics: Arc, - ) -> Result { - debug!( - "Checking tables: message = {:?} & router = {:?}", - &message_table_name, &router_table_name - ); - let ddb = if let Ok(endpoint) = env::var("AWS_LOCAL_DYNAMODB") { - DynamoDbClient::new_with( - HttpClient::new().map_err(|e| { - ApcErrorKind::GeneralError(format!("TLS initialization error {e:?}")) - })?, - StaticProvider::new_minimal("BogusKey".to_string(), "BogusKey".to_string()), - Region::Custom { - name: "us-east-1".to_string(), - endpoint, - }, - ) - } else { - DynamoDbClient::new(Region::default()) - }; - - let mut message_table_names = list_message_tables(&ddb, message_table_name) - .map_err(|_| ApcErrorKind::DatabaseError("Failed to locate message tables".into()))?; - // Valid message months are the current and last 2 months - message_table_names.sort_unstable_by(|a, b| b.cmp(a)); - message_table_names.truncate(3); - message_table_names.reverse(); - let current_message_month = message_table_names - .last() - .ok_or("No last message month found") - .map_err(|_e| ApcErrorKind::GeneralError("No last message month found".into()))? - .to_string(); - - Ok(Self { - ddb, - metrics, - router_table_name: router_table_name.to_owned(), - message_table_names, - current_message_month, - }) - } - - 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.as_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.clone()), - retryable_updateitem_error, - ) - .map_err(|e| ApcErrorKind::DatabaseError(e.to_string()).into()) - } - - pub fn hello( - &self, - connected_at: u64, - uaid: Option<&Uuid>, - router_url: &str, - defer_registration: bool, - ) -> impl Future { - trace!( - "🧑🏼 uaid {:?}, defer_registration: {:?}", - &uaid, - &defer_registration - ); - let response: MyFuture<(HelloResponse, Option)> = if let Some(uaid) = uaid { - commands::lookup_user( - self.ddb.clone(), - self.metrics.clone(), - uaid, - connected_at, - router_url, - &self.router_table_name, - &self.message_table_names, - &self.current_message_month, - ) - } else { - Box::new(future::ok(( - HelloResponse { - message_month: self.current_message_month.clone(), - connected_at, - ..Default::default() - }, - None, - ))) - }; - let ddb = self.ddb.clone(); - let router_url = router_url.to_string(); - let router_table_name = self.router_table_name.clone(); - - response.and_then(move |(mut hello_response, user_opt)| { - trace!( - "💬 Hello Response: {:?}, {:?}", - hello_response.uaid, - 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; - trace!("🧑 UAID = {:?}", &uaid); - let mut err_response = hello_response.clone(); - err_response.connected_at = connected_at; - if !defer_registration { - future::Either::A( - commands::register_user(ddb, &user, &router_table_name) - .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) - }), - ) - } else { - debug!("Deferring user registration {:?}", &uaid); - hello_response.uaid = Some(uaid); - hello_response.deferred_user_registration = Some(user); - future::Either::B(Box::new(future::ok(hello_response))) - } - }) - } - - pub fn register_channel( - &self, - uaid: &Uuid, - channel_id: &Uuid, - message_month: &str, - endpoint: &str, - register_user: Option<&DynamoDbUser>, - ) -> MyFuture { - let ddb = self.ddb.clone(); - let mut chids = HashSet::new(); - let endpoint = endpoint.to_owned(); - chids.insert(channel_id.as_hyphenated().to_string()); - - if let Some(user) = register_user { - trace!( - "💬 Endpoint Request: User not yet registered... {:?}", - &user.uaid - ); - let uaid2 = *uaid; - let message_month2 = message_month.to_owned(); - let response = commands::register_user(ddb.clone(), user, &self.router_table_name) - .and_then(move |_| { - trace!("✍️ Saving channels: {:#?}", chids); - commands::save_channels(ddb, &uaid2, chids, &message_month2) - .and_then(move |_| { - trace!("✉️ sending endpoint: {}", endpoint); - future::ok(RegisterResponse::Success { endpoint }) - }) - .or_else(move |r| { - trace!("--- failed to register channel. {:?}", r); - future::ok(RegisterResponse::Error { - status: 503, - error_msg: "Failed to register channel".to_string(), - }) - }) - }); - return Box::new(response); - }; - trace!("❓ Continuing register..."); - 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, uaid: &Uuid) -> impl Future { - commands::drop_user(self.ddb.clone(), uaid, &self.router_table_name) - .and_then(|_| future::ok(())) - .map_err(|_| ApcErrorKind::DatabaseError("Unable to drop user record".into()).into()) - } - - pub fn unregister_channel( - &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, - ) -> impl Future { - let uaid = *uaid; - let ddb = self.ddb.clone(); - let ddb2 = self.ddb.clone(); - let cur_month = self.current_message_month.to_string(); - let cur_month2 = cur_month.clone(); - let router_table_name = self.router_table_name.clone(); - - 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(())) - .map_err(|_e| ApcErrorKind::DatabaseError("Unable to migrate user".into()).into()) - } - - /// Store a single message - #[allow(dead_code)] - pub fn store_message( - &self, - uaid: &Uuid, - message_month: String, - message: Notification, - ) -> impl Future { - let topic = message.topic.is_some().to_string(); - let ddb = self.ddb.clone(); - let put_item = PutItemInput { - item: serde_dynamodb::to_hashmap(&DynamoDbNotification::from_notif(uaid, message)) - .unwrap(), - table_name: message_month, - ..Default::default() - }; - let metrics = self.metrics.clone(); - retry_if( - move || ddb.put_item(put_item.clone()), - retryable_putitem_error, - ) - .and_then(move |_| { - let mut metric = metrics.incr_with_tags("notification.message.stored"); - // TODO: include `internal` if meta is set. - metric = metric.with_tag("topic", &topic); - metric.send(); - future::ok(()) - }) - .map_err(|_| ApcErrorKind::DatabaseError("Error saving notification".into()).into()) - } - - /// 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 metrics = self.metrics.clone(); - let put_items: Vec = messages - .into_iter() - .filter_map(|n| { - // eventually include `internal` if `meta` defined. - metrics - .incr_with_tags("notification.message.stored") - .with_tag("topic", &n.topic.is_some().to_string()) - .send(); - 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() - }; - - retry_if( - move || ddb.batch_write_item(batch_input.clone()), - retryable_batchwriteitem_error, - ) - .and_then(|_| future::ok(())) - .map_err(|err| { - debug!("Error saving notification: {:?}", err); - err - }) - // TODO: Use Sentry to capture/report this error - .map_err(|_e| ApcErrorKind::DatabaseError("Error saving notifications".into()).into()) - } - - /// 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 topic = notif.topic.is_some().to_string(); - let ddb = self.ddb.clone(); - let metrics = self.metrics.clone(); - let delete_input = DeleteItemInput { - table_name: table_name.to_string(), - key: ddb_item! { - uaid: s => uaid.as_simple().to_string(), - chidmessageid: s => notif.chidmessageid() - }, - ..Default::default() - }; - - retry_if( - move || ddb.delete_item(delete_input.clone()), - retryable_delete_error, - ) - .and_then(move |_| { - let mut metric = metrics.incr_with_tags("notification.message.deleted"); - // TODO: include `internal` if meta is set. - metric = metric.with_tag("topic", &topic); - metric.send(); - future::ok(()) - }) - .map_err(|_| ApcErrorKind::DatabaseError("Error deleting notification".into()).into()) - } - - /// Check to see if we have pending messages and return them if we do. - 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_topic_messages( - self.ddb.clone(), - self.metrics.clone(), - table_name, - uaid, - 11, - )) - } else { - Box::new(future::ok(Default::default())) - }; - let uaid = *uaid; - let table_name = table_name.to_string(); - let ddb = self.ddb.clone(); - let metrics = self.metrics.clone(); - let rmetrics = metrics.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); - rmetrics - .count_with_tags("notification.message.retrieved", resp.messages.len() as i64) - .with_tag("topic", "true") - .send(); - 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, - metrics, - table_name.as_ref(), - &uaid, - timestamp, - 10, - )) - } 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); - rmetrics - .count_with_tags("notification.message.retrieved", resp.messages.len() as i64) - .with_tag("topic", "false") - .send(); - Ok(CheckStorageResponse { - include_topic: false, - messages: resp.messages, - timestamp, - }) - }); - Box::new(next_query) - }) - } - - pub fn get_user( - &self, - uaid: &Uuid, - ) -> impl Future, Error = ApcError> { - let ddb = self.ddb.clone(); - let result = commands::get_uaid(ddb, uaid, &self.router_table_name).and_then(|result| { - future::result( - result - .item - .map(|item| { - let user = serde_dynamodb::from_hashmap(item); - user.map_err(|_| { - ApcErrorKind::DatabaseError("Error deserializing".into()).into() - }) - }) - .transpose(), - ) - }); - Box::new(result) - } - - /// Get the set of channel IDs for a user - #[allow(dead_code)] - pub fn get_user_channels( - &self, - uaid: &Uuid, - message_table: &str, - ) -> impl Future, Error = ApcError> { - commands::all_channels(self.ddb.clone(), uaid, message_table).and_then(|channels| { - channels - .into_iter() - .map(|channel| channel.parse().map_err(|e| ApcErrorKind::from(e).into())) - .collect::>() - }) - } - - /// Remove the node ID from a user in the router table. - /// The node ID will only be cleared if `connected_at` matches up - /// with the item's `connected_at`. - #[allow(dead_code)] - pub fn remove_node_id( - &self, - uaid: &Uuid, - node_id: String, - connected_at: u64, - ) -> impl Future { - let ddb = self.ddb.clone(); - let update_item = UpdateItemInput { - key: ddb_item! { uaid: s => uaid.as_simple().to_string() }, - update_expression: Some("REMOVE node_id".to_string()), - condition_expression: Some("(node_id = :node) and (connected_at = :conn)".to_string()), - expression_attribute_values: Some(hashmap! { - ":node".to_string() => val!(S => node_id), - ":conn".to_string() => val!(N => connected_at.to_string()) - }), - table_name: self.router_table_name.clone(), - ..Default::default() - }; - - retry_if( - move || ddb.update_item(update_item.clone()), - retryable_updateitem_error, - ) - .and_then(|_| future::ok(())) - .map_err(|_| ApcErrorKind::DatabaseError("Error removing node ID".into()).into()) - } -} - -/// Get the list of current, valid message tables (Note: This is legacy for DynamoDB, but may still -/// be used for Stand Alone systems ) -pub fn list_message_tables(ddb: &DynamoDbClient, prefix: &str) -> Result> { - let mut names: Vec = Vec::new(); - let mut start_key = None; - loop { - let result = commands::list_tables_sync(ddb, start_key)?; - 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) -} diff --git a/autopush/src/db/models.rs b/autopush/src/db/models.rs deleted file mode 100644 index f0a47f622..000000000 --- a/autopush/src/db/models.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::result::Result as StdResult; - -use lazy_static::lazy_static; -use regex::RegexSet; -use serde::Serializer; -use serde_derive::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::db::util::generate_last_connect; -use autopush_common::errors::*; -use autopush_common::notification::{ - Notification, STANDARD_NOTIFICATION_PREFIX, TOPIC_NOTIFICATION_PREFIX, -}; -use autopush_common::util::timing::ms_since_epoch; -use autopush_common::util::InsertOpt; - -use super::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.as_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, -} - -#[allow(clippy::implicit_hasher)] -impl From for HashMap { - fn from(val: NotificationHeaders) -> Self { - let mut map = Self::new(); - map.insert_opt("crypto_key", val.crypto_key); - map.insert_opt("encryption", val.encryption); - map.insert_opt("encryption_key", val.encryption_key); - map.insert_opt("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, Eq, 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, - // Router-specific data - pub router_data: Option>, - // 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 { - let uaid = Uuid::new_v4(); - //trace!(">>> Setting default uaid: {:?}", &uaid); - Self { - uaid, - connected_at: ms_since_epoch(), - router_type: "webpush".to_string(), - router_data: None, - last_connect: Some(generate_last_connect()), - node_id: None, - record_version: Some(USER_RECORD_VERSION), - current_month: None, - } - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct DynamoDbNotification { - // DynamoDB - #[serde(serialize_with = "uuid_serializer")] - uaid: Uuid, - // DynamoDB - // Format: - // Topic Messages: - // {TOPIC_NOTIFICATION_PREFIX}:{channel id}:{topic} - // New Messages: - // {STANDARD_NOTIFICATION_PREFIX}:{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: u64, - // 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, -} - -/// Ensure that the default for 'stored' is true. -impl Default for DynamoDbNotification { - fn default() -> Self { - Self { - uaid: Uuid::default(), - chidmessageid: String::default(), - current_timestamp: None, - chids: None, - timestamp: None, - expiry: 0, - ttl: None, - data: None, - headers: None, - updateid: None, - } - } -} - -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(ApcErrorKind::GeneralError("Invalid chidmessageid".into()).into()); - } - - let v: Vec<&str> = key.split(':').collect(); - match v[0] { - TOPIC_NOTIFICATION_PREFIX => { - if v.len() != 3 { - return Err(ApcErrorKind::GeneralError("Invalid topic key".into()).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, - }) - } - STANDARD_NOTIFICATION_PREFIX => { - if v.len() != 3 { - return Err(ApcErrorKind::GeneralError("Invalid topic key".into()).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(ApcErrorKind::GeneralError("Invalid topic key".into()).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") - .map_err(|e| ApcErrorKind::GeneralError(e.to_string()))?; - - Ok(Notification { - channel_id: key.channel_id, - version, - ttl: self.ttl.unwrap_or(0), - timestamp: self - .timestamp - .ok_or("No timestamp found") - .map_err(|e| ApcErrorKind::GeneralError(e.to_string()))?, - 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.chidmessageid(), - timestamp: Some(val.timestamp), - 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 autopush_common::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.as_hyphenated()); - 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.as_hyphenated()); - 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 &["02j3i2o", "03:ffas:wef", "01::mytopic", "02:oops:ohnoes"] { - let key = DynamoDbNotification::parse_sort_key(val); - assert!(key.is_err()); - } - } -} diff --git a/autopush/src/db/util.rs b/autopush/src/db/util.rs deleted file mode 100644 index 05c28e0dc..000000000 --- a/autopush/src/db/util.rs +++ /dev/null @@ -1,15 +0,0 @@ -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/autopush/src/http.rs b/autopush/src/http.rs deleted file mode 100644 index c2f04c13f..000000000 --- a/autopush/src/http.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! 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::{str, sync::Arc}; - -use futures::future::Either; - -use futures::future::ok; -use futures::{Future, Stream}; -use hyper::{self, service::Service, Body, Method, StatusCode}; -use uuid::Uuid; - -use crate::server::registry::ClientRegistry; - -pub struct Push(pub Arc); - -impl Service for Push { - type ReqBody = Body; - type ResBody = Body; - type Error = hyper::Error; - type Future = Box, Error = hyper::Error> + Send>; - - fn call(&mut self, req: hyper::Request) -> Self::Future { - trace!("⏩ **** In notif handler..."); - let mut response = hyper::Response::builder(); - let req_path = req.uri().path().to_string(); - let path_vec: Vec<&str> = req_path.split('/').collect(); - if path_vec.len() != 3 { - response.status(StatusCode::NOT_FOUND); - return Box::new(ok(response.body(Body::empty()).unwrap())); - } - 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.status(StatusCode::BAD_REQUEST); - return Box::new(ok(response.body(Body::empty()).unwrap())); - } - }; - let clients = Arc::clone(&self.0); - debug!("⏩ trying {} => {}", req.method(), &uaid); - // Handle incoming routed push notifications from endpoints. - match (req.method(), method_name, uaid) { - (&Method::PUT, "push", uaid) => { - trace!("⏩ PUT /push/ {}", uaid); - // Due to consumption of body as a future we must return here - let body = req.into_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) { - Either::A(clients.notify(uaid, msg).then(move |result| { - let body = if result.is_ok() { - response.status(StatusCode::OK); - Body::empty() - } else { - response.status(StatusCode::NOT_FOUND); - Body::from("Client not available.") - }; - Ok(response.body(body).unwrap()) - })) - } else { - Either::B(ok(response - .status(hyper::StatusCode::BAD_REQUEST) - .body("Unable to decode body payload".into()) - .unwrap())) - } - })); - } - (&Method::PUT, "notif", uaid) => { - trace!("⏩ PUT /notif/ {}", uaid); - return Box::new(clients.check_storage(uaid).then(move |result| { - let body = if result.is_ok() { - response.status(StatusCode::OK); - Body::empty() - } else { - response.status(StatusCode::NOT_FOUND); - Body::from("Client not available.") - }; - Ok(response.body(body).unwrap()) - })); - } - (_, "push", _) | (_, "notif", _) => { - response.status(StatusCode::METHOD_NOT_ALLOWED); - } - _ => { - response.status(StatusCode::NOT_FOUND); - } - }; - Box::new(ok(response.body(Body::empty()).unwrap())) - } -} diff --git a/autopush/src/main.rs b/autopush/src/main.rs deleted file mode 100644 index 1b67fb90e..000000000 --- a/autopush/src/main.rs +++ /dev/null @@ -1,108 +0,0 @@ -extern crate slog; -#[macro_use] -extern crate slog_scope; -#[macro_use] -extern crate serde_derive; - -use std::time::Duration; -use std::{env, os::raw::c_int, thread}; - -use docopt::Docopt; - -use autopush_common::errors::{ApcError, ApcErrorKind, Result}; -use futures::{future::Either, Future, IntoFuture}; -use tokio_core::reactor::{Handle, Timeout}; - -mod client; -mod db; -mod http; -mod megaphone; -mod server; -mod settings; - -use crate::server::{AppState, AutopushServer}; -use crate::settings::Settings; - -pub type MyFuture = Box>; - -const USAGE: &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<()> { - env_logger::init(); - let signal = notify(&[signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM])?; - 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 app_state = AppState::from_settings(settings)?; - let server = AutopushServer::new(app_state); - server.start(); - signal.recv().unwrap(); - server - .stop() - .map_err(|_e| ApcErrorKind::GeneralError("Failed to shutdown properly".into()).into()) -} - -/// Create a new channel subscribed to the given signals -fn notify(signals: &[c_int]) -> Result> { - let (s, r) = crossbeam_channel::bounded(100); - let mut signals = signal_hook::iterator::Signals::new(signals)?; - thread::spawn(move || { - for signal in signals.forever() { - if s.send(signal).is_err() { - break; - } - } - }); - Ok(r) -} - -/// 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(ApcErrorKind::GeneralError("timed out".to_owned()).into()) - } - Err(Either::B((e, _item))) => Err(e.into()), - })) -} diff --git a/autopush/src/megaphone.rs b/autopush/src/megaphone.rs deleted file mode 100644 index fc6154e59..000000000 --- a/autopush/src/megaphone.rs +++ /dev/null @@ -1,406 +0,0 @@ -use std::collections::HashMap; -use std::time::Duration; - -use serde_derive::{Deserialize, Serialize}; - -use autopush_common::errors::{ApcErrorKind, Result}; - -use crate::server::protocol::BroadcastValue; - -// A Broadcast entry Key in a BroadcastRegistry -type BroadcastKey = u32; - -// Broadcasts a client is subscribed to and the last change seen -#[derive(Debug, Default)] -pub struct BroadcastSubs { - broadcast_list: Vec, - change_count: u32, -} - -#[derive(Debug)] -struct BroadcastRegistry { - lookup: HashMap, - table: Vec, -} - -// Return result of the first delta call for a client given a full list of broadcast id's and -// versions -#[derive(Debug)] -pub struct BroadcastSubsInit(pub BroadcastSubs, pub Vec); - -impl BroadcastRegistry { - fn new() -> BroadcastRegistry { - BroadcastRegistry { - lookup: HashMap::new(), - table: Vec::new(), - } - } - - // Add's a new broadcast to the lookup table, returns the existing key if the broadcast already - // exists - fn add_broadcast(&mut self, broadcast_id: String) -> BroadcastKey { - if let Some(v) = self.lookup.get(&broadcast_id) { - return *v; - } - let i = self.table.len() as BroadcastKey; - self.table.push(broadcast_id.clone()); - self.lookup.insert(broadcast_id, i); - i - } - - fn lookup_id(&self, key: BroadcastKey) -> Option { - self.table.get(key as usize).cloned() - } - - fn lookup_key(&self, broadcast_id: &str) -> Option { - self.lookup.get(broadcast_id).copied() - } -} - -// An individual broadcast and the current change count -#[derive(Debug)] -struct BroadcastRevision { - change_count: u32, - broadcast: BroadcastKey, -} - -// A provided Broadcast/Version used for `BroadcastSubsInit`, client comparisons, and outgoing -// deltas -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct Broadcast { - broadcast_id: String, - version: String, -} - -impl Broadcast { - /// Errors out a broadcast for broadcasts that weren't found - pub fn error(self) -> Broadcast { - Broadcast { - broadcast_id: self.broadcast_id, - version: "Broadcast not found".to_string(), - } - } -} - -// Handy From impls for common hashmap to/from conversions -impl From<(String, String)> for Broadcast { - fn from(val: (String, String)) -> Broadcast { - Broadcast { - broadcast_id: val.0, - version: val.1, - } - } -} - -impl From for (String, BroadcastValue) { - fn from(bcast: Broadcast) -> (String, BroadcastValue) { - (bcast.broadcast_id, BroadcastValue::Value(bcast.version)) - } -} - -impl Broadcast { - pub fn from_hashmap(val: HashMap) -> Vec { - val.into_iter().map(|v| v.into()).collect() - } - - pub fn vec_into_hashmap(broadcasts: Vec) -> HashMap { - broadcasts.into_iter().map(|v| v.into()).collect() - } -} - -// BroadcastChangeTracker tracks the broadcasts, their change_count, and the broadcast lookup -// registry -#[derive(Debug)] -pub struct BroadcastChangeTracker { - broadcast_list: Vec, - broadcast_registry: BroadcastRegistry, - broadcast_versions: HashMap, - change_count: u32, -} - -#[derive(Deserialize)] -pub struct MegaphoneAPIResponse { - pub broadcasts: HashMap, -} - -impl BroadcastChangeTracker { - /// Creates a new `BroadcastChangeTracker` initialized with the provided `broadcasts`. - pub fn new(broadcasts: Vec) -> BroadcastChangeTracker { - let mut tracker = BroadcastChangeTracker { - broadcast_list: Vec::new(), - broadcast_registry: BroadcastRegistry::new(), - broadcast_versions: HashMap::new(), - change_count: 0, - }; - for srv in broadcasts { - let key = tracker.broadcast_registry.add_broadcast(srv.broadcast_id); - tracker.broadcast_versions.insert(key, srv.version); - } - tracker - } - - /// Creates a new `BroadcastChangeTracker` 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_broadcasts(url: &str, token: &str) -> reqwest::Result { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(1)) - .build()?; - let MegaphoneAPIResponse { broadcasts } = client - .get(url) - .header("Authorization", token.to_string()) - .send()? - .error_for_status()? - .json()?; - let broadcasts = Broadcast::from_hashmap(broadcasts); - Ok(BroadcastChangeTracker::new(broadcasts)) - } - - /// Add a new broadcast to the BroadcastChangeTracker, triggering a change_count increase. - /// Note: If the broadcast already exists, it will be updated instead. - pub fn add_broadcast(&mut self, broadcast: Broadcast) -> u32 { - if let Ok(change_count) = self.update_broadcast(broadcast.clone()) { - trace!("📢 returning change count {}", &change_count); - return change_count; - } - self.change_count += 1; - let key = self - .broadcast_registry - .add_broadcast(broadcast.broadcast_id); - self.broadcast_versions.insert(key, broadcast.version); - self.broadcast_list.push(BroadcastRevision { - change_count: self.change_count, - broadcast: key, - }); - self.change_count - } - - /// Update a `broadcast` to a new revision, triggering a change_count increase. - /// - /// Returns an error if the `broadcast` was never initialized/added. - pub fn update_broadcast(&mut self, broadcast: Broadcast) -> Result { - let b_id = broadcast.broadcast_id.clone(); - let old_count = self.change_count; - let key = self - .broadcast_registry - .lookup_key(&broadcast.broadcast_id) - .ok_or_else(|| ApcErrorKind::BroadcastError("Broadcast not found".into()))?; - - if let Some(ver) = self.broadcast_versions.get_mut(&key) { - if *ver == broadcast.version { - return Ok(self.change_count); - } - *ver = broadcast.version; - } else { - trace!("📢 Not found: {}", &b_id); - return Err(ApcErrorKind::BroadcastError("Broadcast not found".into()).into()); - } - - trace!("📢 New version of {}", &b_id); - // Check to see if this broadcast has been updated since initialization - let bcast_index = self - .broadcast_list - .iter() - .enumerate() - .filter_map(|(i, bcast)| { - if bcast.broadcast == key { - Some(i) - } else { - None - } - }) - .next(); - self.change_count += 1; - if let Some(bcast_index) = bcast_index { - trace!("📢 {} index: {}", &b_id, &bcast_index); - let mut bcast = self.broadcast_list.remove(bcast_index); - bcast.change_count = self.change_count; - self.broadcast_list.push(bcast); - } else { - trace!("📢 adding broadcast list for {}", &b_id); - self.broadcast_list.push(BroadcastRevision { - change_count: self.change_count, - broadcast: key, - }) - } - if old_count != self.change_count { - trace!("📢 New Change available"); - } - Ok(self.change_count) - } - - /// Returns the new broadcast versions since the provided `client_set`. - pub fn change_count_delta(&self, client_set: &mut BroadcastSubs) -> Option> { - if self.change_count <= client_set.change_count { - return None; - } - let mut bcast_delta = Vec::new(); - for bcast in self.broadcast_list.iter().rev() { - if bcast.change_count <= client_set.change_count { - break; - } - if !client_set.broadcast_list.contains(&bcast.broadcast) { - continue; - } - if let Some(ver) = self.broadcast_versions.get(&bcast.broadcast) { - if let Some(bcast_id) = self.broadcast_registry.lookup_id(bcast.broadcast) { - bcast_delta.push(Broadcast { - broadcast_id: bcast_id, - version: (*ver).clone(), - }); - } - } - } - client_set.change_count = self.change_count; - if bcast_delta.is_empty() { - None - } else { - Some(bcast_delta) - } - } - - /// Returns a delta for `broadcasts` that are out of date with the latest version and a - /// the collection of broadcast subscriptions. - pub fn broadcast_delta(&self, broadcasts: &[Broadcast]) -> BroadcastSubsInit { - let mut bcast_list = Vec::new(); - let mut bcast_delta = Vec::new(); - for bcast in broadcasts.iter() { - if let Some(bcast_key) = self.broadcast_registry.lookup_key(&bcast.broadcast_id) { - if let Some(ver) = self.broadcast_versions.get(&bcast_key) { - if *ver != bcast.version { - bcast_delta.push(Broadcast { - broadcast_id: bcast.broadcast_id.clone(), - version: (*ver).clone(), - }); - } - } - bcast_list.push(bcast_key); - } - } - BroadcastSubsInit( - BroadcastSubs { - broadcast_list: bcast_list, - change_count: self.change_count, - }, - bcast_delta, - ) - } - - /// Update a `BroadcastSubs` to account for new broadcasts. - /// - /// Returns broadcasts that have changed. - pub fn subscribe_to_broadcasts( - &self, - broadcast_subs: &mut BroadcastSubs, - broadcasts: &[Broadcast], - ) -> Option> { - let mut bcast_delta = self.change_count_delta(broadcast_subs).unwrap_or_default(); - for bcast in broadcasts.iter() { - if let Some(bcast_key) = self.broadcast_registry.lookup_key(&bcast.broadcast_id) { - if let Some(ver) = self.broadcast_versions.get(&bcast_key) { - if *ver != bcast.version { - bcast_delta.push(Broadcast { - broadcast_id: bcast.broadcast_id.clone(), - version: (*ver).clone(), - }); - } - } - broadcast_subs.broadcast_list.push(bcast_key) - } - } - if bcast_delta.is_empty() { - None - } else { - Some(bcast_delta) - } - } - - /// Check a broadcast list and return unknown broadcast id's with their appropriate error - pub fn missing_broadcasts(&self, broadcasts: &[Broadcast]) -> Vec { - broadcasts - .iter() - .filter_map(|b| { - if self - .broadcast_registry - .lookup_key(&b.broadcast_id) - .is_none() - { - Some(b.clone().error()) - } else { - None - } - }) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_broadcast_base() -> Vec { - vec![ - Broadcast { - broadcast_id: String::from("bcasta"), - version: String::from("rev1"), - }, - Broadcast { - broadcast_id: String::from("bcastb"), - version: String::from("revalha"), - }, - ] - } - - #[test] - fn test_broadcast_change_tracker() { - let broadcasts = make_broadcast_base(); - let desired_broadcasts = broadcasts.clone(); - let mut tracker = BroadcastChangeTracker::new(broadcasts); - let BroadcastSubsInit(mut broadcast_subs, delta) = - tracker.broadcast_delta(&desired_broadcasts); - assert_eq!(delta.len(), 0); - assert_eq!(broadcast_subs.change_count, 0); - assert_eq!(broadcast_subs.broadcast_list.len(), 2); - - tracker - .update_broadcast(Broadcast { - broadcast_id: String::from("bcasta"), - version: String::from("rev2"), - }) - .ok(); - let delta = tracker.change_count_delta(&mut broadcast_subs); - assert!(delta.is_some()); - let delta = delta.unwrap(); - assert_eq!(delta.len(), 1); - } - - #[test] - fn test_broadcast_change_handles_new_broadcasts() { - let broadcasts = make_broadcast_base(); - let desired_broadcasts = broadcasts.clone(); - let mut tracker = BroadcastChangeTracker::new(broadcasts); - let BroadcastSubsInit(mut broadcast_subs, _) = tracker.broadcast_delta(&desired_broadcasts); - - tracker.add_broadcast(Broadcast { - broadcast_id: String::from("bcastc"), - version: String::from("revmega"), - }); - let delta = tracker.change_count_delta(&mut broadcast_subs); - assert!(delta.is_none()); - - let delta = tracker - .subscribe_to_broadcasts( - &mut broadcast_subs, - &[Broadcast { - broadcast_id: String::from("bcastc"), - version: String::from("revision_alpha"), - }], - ) - .unwrap(); - assert_eq!(delta.len(), 1); - assert_eq!(delta[0].version, String::from("revmega")); - assert_eq!(broadcast_subs.change_count, 1); - assert_eq!(tracker.broadcast_list.len(), 1); - } -} diff --git a/autopush/src/server/dispatch.rs b/autopush/src/server/dispatch.rs deleted file mode 100644 index bc4cc31c3..000000000 --- a/autopush/src/server/dispatch.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! 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::{try_ready, Future, Poll}; -use tokio_core::net::TcpStream; -use tokio_io::AsyncRead; - -use autopush_common::errors::{ApcError, ApcErrorKind}; - -use crate::server::tls::MaybeTlsStream; -use crate::server::webpush_io::WebpushIo; - -pub struct Dispatch { - socket: Option>, - data: BytesMut, -} - -pub enum RequestType { - Websocket, - Status, - LogCheck, - LBHeartBeat, - Version, -} - -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 = ApcError; - - fn poll(&mut self) -> Poll<(WebpushIo, RequestType), ApcError> { - 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(ApcErrorKind::GeneralError("early eof".into()).into()); - } - let ty = { - let mut headers = [httparse::EMPTY_HEADER; 32]; - 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(path) if path.starts_with("/status") || path == "/__heartbeat__" => { - RequestType::Status - } - Some("/__lbheartbeat__") => RequestType::LBHeartBeat, - Some("/__version__") => RequestType::Version, - // legacy: - Some(path) if path.starts_with("/v1/err/crit") => RequestType::LogCheck, - // standardized: - Some("/_error") => RequestType::LogCheck, - _ => { - debug!("unknown http request {:?}", req); - return Err( - ApcErrorKind::GeneralError("unknown http request".into()).into() - ); - } - } - } - }; - - let tcp = self.socket.take().unwrap(); - return Ok((WebpushIo::new(tcp, self.data.take()), ty).into()); - } - } -} diff --git a/autopush/src/server/metrics.rs b/autopush/src/server/metrics.rs deleted file mode 100644 index 9d412f215..000000000 --- a/autopush/src/server/metrics.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Metrics tie-ins - -use std::net::UdpSocket; - -use cadence::{BufferedUdpMetricSink, NopMetricSink, QueuingMetricSink, StatsdClient}; - -use autopush_common::errors::Result; - -use crate::server::AppState; - -/// Create a cadence StatsdClient from the given options -pub fn metrics_from_state(state: &AppState) -> Result { - let builder = if let Some(statsd_host) = state.statsd_host.as_ref() { - let socket = UdpSocket::bind("0.0.0.0:0")?; - socket.set_nonblocking(true)?; - - let host = (statsd_host.as_str(), state.statsd_port); - let udp_sink = BufferedUdpMetricSink::from(host, socket)?; - let sink = QueuingMetricSink::from(udp_sink); - StatsdClient::builder("autopush", sink) - } else { - StatsdClient::builder("autopush", NopMetricSink) - }; - Ok(builder - .with_error_handler(|err| error!("Metrics send error: {}", err)) - .build()) -} diff --git a/autopush/src/server/middleware/mod.rs b/autopush/src/server/middleware/mod.rs deleted file mode 100644 index fd1010dc1..000000000 --- a/autopush/src/server/middleware/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Actix middleware - -pub mod sentry2; diff --git a/autopush/src/server/middleware/sentry2.rs b/autopush/src/server/middleware/sentry2.rs deleted file mode 100644 index 8fd44cda7..000000000 --- a/autopush/src/server/middleware/sentry2.rs +++ /dev/null @@ -1,301 +0,0 @@ -/// TODO: Eventually move this to autopush-common, as well as handle a number of the -/// issues with modern sentry: -/// e.g. -/// * resolve bytes limit -/// * handle pulling `extra` data -use std::borrow::Cow; -use std::pin::Pin; -use std::sync::Arc; - -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::http::StatusCode; -use actix_web::Error; -use futures_util::future::{ok, Future, Ready}; -use futures_util::FutureExt; - -use sentry_core::protocol::{self, ClientSdkPackage, Event, Request}; -use sentry_core::{Hub, SentryFutureExt}; - -// Taken from sentry-actix - -/// Reports certain failures to Sentry. -#[derive(Clone)] -pub struct Sentry { - hub: Option>, - emit_header: bool, - capture_server_errors: bool, - start_transaction: bool, -} - -impl Default for Sentry { - fn default() -> Self { - Sentry { - hub: None, - emit_header: false, - capture_server_errors: true, - start_transaction: false, - } - } -} - -impl Sentry { - #[allow(dead_code)] - /// Creates a new sentry middleware which starts a new performance monitoring transaction for each request. - pub fn with_transaction() -> Sentry { - Sentry { - start_transaction: true, - ..Sentry::default() - } - } -} - -impl Transform for Sentry -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Transform = SentryMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(SentryMiddleware { - service, - inner: self.clone(), - }) - } -} - -/// The middleware for individual services. -pub struct SentryMiddleware { - service: S, - inner: Sentry, -} - -impl Service for SentryMiddleware -where - S: Service, Error = Error>, - S::Future: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = Pin>>>; - - fn poll_ready( - &self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let inner = self.inner.clone(); - // XXX Sentry Modification - // crate::server is going to be specific to the given app, so we can't put this in common. - let state = req - .app_data::>() - .cloned(); - // XXX - let hub = Arc::new(Hub::new_from_top( - inner.hub.clone().unwrap_or_else(Hub::main), - )); - let client = hub.client(); - let track_sessions = client.as_ref().map_or(false, |client| { - let options = client.options(); - options.auto_session_tracking - && options.session_mode == sentry_core::SessionMode::Request - }); - if track_sessions { - hub.start_session(); - } - let with_pii = client - .as_ref() - .map_or(false, |client| client.options().send_default_pii); - - let (mut tx, sentry_req) = sentry_request_from_http(&req, with_pii); - - let transaction = if inner.start_transaction { - let name = std::mem::take(&mut tx) - .unwrap_or_else(|| format!("{} {}", req.method(), req.uri())); - - let headers = req.headers().iter().flat_map(|(header, value)| { - value.to_str().ok().map(|value| (header.as_str(), value)) - }); - - // TODO: Break apart user agent? - // XXX Sentry modification - /* - if let Some(ua_val) = req.headers().get("UserAgent") { - if let Ok(ua_str) = ua_val.to_str() { - let ua_info = crate::util::user_agent::UserAgentInfo::from(ua_val.to_str().unwrap_or_default()); - sentry::configure_scope(|scope| { - scope::add_tag("browser_version", ua_info.browser_version); - }) - } - } - */ - let ctx = sentry_core::TransactionContext::continue_from_headers( - &name, - "http.server", - headers, - ); - Some(hub.start_transaction(ctx)) - } else { - None - }; - - let parent_span = hub.configure_scope(|scope| { - let parent_span = scope.get_span(); - if let Some(transaction) = transaction.as_ref() { - scope.set_span(Some(transaction.clone().into())); - } else { - scope.set_transaction(tx.as_deref()); - } - scope.add_event_processor(move |event| Some(process_event(event, &sentry_req))); - parent_span - }); - - let fut = self.service.call(req).bind_hub(hub.clone()); - - async move { - // Service errors - let mut res: Self::Response = match fut.await { - Ok(res) => res, - Err(e) => { - // XXX Sentry Modification - if let Some(api_err) = e.as_error::() { - // if it's not reportable, , and we have access to the metrics, record it as a metric. - if !api_err.kind.is_sentry_event() { - // XXX - Modified sentry - // The error (e.g. VapidErrorKind::InvalidKey(String)) might be too cardinal, - // but we may need that information to debug a production issue. We can - // add an info here, temporarily turn on info level debugging on a given server, - // capture it, and then turn it off before we run out of money. - info!("Sending error to metrics: {:?}", api_err.kind); - if let Some(state) = state { - if let Some(label) = api_err.kind.metric_label() { - state.metrics.incr(&format!("api_error.{}", label)).is_ok(); - }; - } - debug!("Not reporting error (service error): {:?}", e); - return Err(e); - } - } - - if inner.capture_server_errors { - hub.capture_error(&e); - } - - if let Some(transaction) = transaction { - if transaction.get_status().is_none() { - let status = protocol::SpanStatus::UnknownError; - transaction.set_status(status); - } - transaction.finish(); - hub.configure_scope(|scope| scope.set_span(parent_span)); - } - return Err(e); - } - }; - - // Response errors - if inner.capture_server_errors && res.response().status().is_server_error() { - if let Some(e) = res.response().error() { - let event_id = hub.capture_error(e); - - if inner.emit_header { - res.response_mut().headers_mut().insert( - "x-sentry-event".parse().unwrap(), - event_id.simple().to_string().parse().unwrap(), - ); - } - } - } - - if let Some(transaction) = transaction { - if transaction.get_status().is_none() { - let status = map_status(res.status()); - transaction.set_status(status); - } - transaction.finish(); - hub.configure_scope(|scope| scope.set_span(parent_span)); - } - - Ok(res) - } - .boxed_local() - } -} - -fn map_status(status: StatusCode) -> protocol::SpanStatus { - match status { - StatusCode::UNAUTHORIZED => protocol::SpanStatus::Unauthenticated, - StatusCode::FORBIDDEN => protocol::SpanStatus::PermissionDenied, - StatusCode::NOT_FOUND => protocol::SpanStatus::NotFound, - StatusCode::TOO_MANY_REQUESTS => protocol::SpanStatus::ResourceExhausted, - status if status.is_client_error() => protocol::SpanStatus::InvalidArgument, - StatusCode::NOT_IMPLEMENTED => protocol::SpanStatus::Unimplemented, - StatusCode::SERVICE_UNAVAILABLE => protocol::SpanStatus::Unavailable, - status if status.is_server_error() => protocol::SpanStatus::InternalError, - StatusCode::CONFLICT => protocol::SpanStatus::AlreadyExists, - status if status.is_success() => protocol::SpanStatus::Ok, - _ => protocol::SpanStatus::UnknownError, - } -} - -/// Build a Sentry request struct from the HTTP request -fn sentry_request_from_http(request: &ServiceRequest, with_pii: bool) -> (Option, Request) { - let transaction = if let Some(name) = request.match_name() { - Some(String::from(name)) - } else { - request.match_pattern() - }; - - let mut sentry_req = Request { - url: format!( - "{}://{}{}", - request.connection_info().scheme(), - request.connection_info().host(), - request.uri() - ) - .parse() - .ok(), - method: Some(request.method().to_string()), - headers: request - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string())) - .collect(), - ..Default::default() - }; - - // If PII is enabled, include the remote address - if with_pii { - if let Some(remote) = request.connection_info().peer_addr() { - sentry_req.env.insert("REMOTE_ADDR".into(), remote.into()); - } - }; - - (transaction, sentry_req) -} - -/// Add request data to a Sentry event -fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> { - // Request - if event.request.is_none() { - event.request = Some(request.clone()); - } - - // SDK - if let Some(sdk) = event.sdk.take() { - let mut sdk = sdk.into_owned(); - sdk.packages.push(ClientSdkPackage { - name: "sentry-actix".into(), - version: env!("CARGO_PKG_VERSION").into(), - }); - event.sdk = Some(Cow::Owned(sdk)); - } - event -} diff --git a/autopush/src/server/mod.rs b/autopush/src/server/mod.rs deleted file mode 100644 index 975793ec0..000000000 --- a/autopush/src/server/mod.rs +++ /dev/null @@ -1,1009 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::collections::HashMap; -use std::env; -use std::io; -use std::net::SocketAddr; -use std::panic; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; - -use cadence::StatsdClient; -use chrono::Utc; -use fernet::{Fernet, MultiFernet}; -use futures::sync::oneshot; -use futures::{task, try_ready}; -use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; -use hyper::{server::conn::Http, StatusCode}; -use openssl::ssl::SslAcceptor; -use sentry::{self, capture_message}; -use serde_json::{self, json}; -use tokio_core::net::TcpListener; -use tokio_core::reactor::{Core, Handle, Timeout}; -use tokio_tungstenite::{accept_hdr_async, WebSocketStream}; -use tungstenite::handshake::server::Request; -use tungstenite::{self, Message}; - -use autopush_common::errors::{ApcError, ApcErrorKind, Result}; -use autopush_common::logging; -use autopush_common::notification::Notification; - -use crate::client::Client; -use crate::db::DynamoStorage; -use crate::megaphone::{ - Broadcast, BroadcastChangeTracker, BroadcastSubs, BroadcastSubsInit, MegaphoneAPIResponse, -}; -use crate::server::dispatch::{Dispatch, RequestType}; -use crate::server::metrics::metrics_from_state; -use crate::server::protocol::{BroadcastValue, ClientMessage, ServerMessage}; -use crate::server::rc::RcObject; -use crate::server::registry::ClientRegistry; -use crate::server::webpush_io::WebpushIo; -use crate::settings::Settings; -use crate::{http, timeout, MyFuture}; - -mod dispatch; -mod metrics; -pub mod protocol; -mod rc; -pub mod registry; -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 { - app_state: Arc, - shutdown_handles: Cell>>, - _guard: Option, -} - -impl AutopushServer { - pub fn new(app_state: AppState) -> Self { - let guard = if let Ok(dsn) = env::var("SENTRY_DSN") { - let guard = sentry::init(( - dsn, - sentry::ClientOptions { - release: sentry::release_name!(), - ..autopush_common::sentry::client_options() - }, - )); - /* - Sentry 0.29+ automatically enables `PanicIntegration`. - see https://docs.rs/sentry-panic/latest/sentry_panic/ - */ - Some(guard) - } else { - None - }; - Self { - app_state: Arc::new(app_state), - shutdown_handles: Cell::new(None), - _guard: guard, - } - } - - pub fn start(&self) { - logging::init_logging( - !self.app_state.human_logs, - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION"), - ) - .expect("init_logging failed"); - let handles = Server::start(&self.app_state).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(ApcErrorKind::Thread(err).into()); - } - } - } - logging::reset_logging(); - result - } -} - -pub struct AppState { - pub router_port: u16, - pub port: u16, - pub 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_name: 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, - pub msg_limit: u32, -} - -impl AppState { - pub fn from_settings(settings: Settings) -> Result { - let crypto_key = &settings.crypto_key; - if !(crypto_key.starts_with('[') && crypto_key.ends_with(']')) { - return Err( - ApcErrorKind::GeneralError("Missing AUTOPUSH__CRYPTO_KEY set".into()).into(), - ); - } - let crypto_key = &crypto_key[1..crypto_key.len() - 1]; - debug!("Fernet keys: {:?}", &crypto_key); - let fernets: Vec = crypto_key - .split(',') - .map(|s| s.trim().to_string()) - .map(|key| { - Fernet::new(&key) - .unwrap_or_else(|| panic!("Invalid AUTOPUSH__CRYPTO_KEY: {:?}", key)) - }) - .collect(); - let fernet = MultiFernet::new(fernets); - - let router_url = settings.router_url(); - let endpoint_url = settings.endpoint_url(); - Ok(Self { - 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_name: settings.message_tablename, - 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, - msg_limit: settings.msg_limit, - }) - } -} - -/// The main AutoConnect server -pub struct Server { - /// List of known Clients, mapped by UAID, for this node. - pub clients: Arc, - /// Handle to the Broadcast change monitor - broadcaster: RefCell, - /// Handle to the current Database Client - pub ddb: DynamoStorage, - /// Count of open cells - open_connections: Cell, - /// OBSOLETE - tls_acceptor: Option, - /// Application Communal Data - pub app_state: Arc, - /// tokio reactor core handle - pub handle: Handle, - /// analytics reporting - pub metrics: Arc, -} - -impl Server { - /// Creates a new server handle used by Megaphone and other services. - /// - /// This will spawn a new server with the [`state`](autoconnect-settings::AppState) 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(app_state: &Arc) -> Result> { - let mut shutdown_handles = vec![]; - - let (inittx, initrx) = oneshot::channel(); - let (donetx, donerx) = oneshot::channel(); - - let state = app_state.clone(); - let thread = thread::spawn(move || { - let (srv, mut core) = match Server::new(&state) { - 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.app_state.router_port)); - debug!("Starting router: {:?}", addr); - 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(Arc::clone(&srv.clients))) - .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()), - } - } - - #[allow(clippy::single_char_add_str)] - fn new(app_state: &Arc) -> Result<(Rc, Core)> { - let core = Core::new()?; - let broadcaster = if let Some(ref megaphone_url) = app_state.megaphone_api_url { - let megaphone_token = app_state - .megaphone_api_token - .as_ref() - .expect("Megaphone API requires a Megaphone API Token to be set"); - BroadcastChangeTracker::with_api_broadcasts(megaphone_url, megaphone_token) - .expect("Unable to initialize megaphone with provided URL") - } else { - BroadcastChangeTracker::new(Vec::new()) - }; - let metrics = Arc::new(metrics_from_state(app_state)?); - - let srv = Rc::new(Server { - app_state: app_state.clone(), - broadcaster: RefCell::new(broadcaster), - ddb: DynamoStorage::from_settings( - &app_state.message_table_name, - &app_state.router_table_name, - metrics.clone(), - )?, - clients: Arc::new(ClientRegistry::default()), - open_connections: Cell::new(0), - handle: core.handle(), - tls_acceptor: tls::configure(app_state), - metrics, - }); - let addr = SocketAddr::from(([0, 0, 0, 0], srv.app_state.port)); - debug!("Starting server: {:?}", &addr); - let ws_listener = TcpListener::bind(&addr, &srv.handle)?; - - let handle = core.handle(); - let srv2 = srv.clone(); - let ws_srv = - ws_listener - .incoming() - .map_err(ApcErrorKind::from) - .for_each(move |(socket, addr)| { - // Make sure we're not handling too many clients before we start the - // websocket handshake. - let max = srv.app_state.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); - - // 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.app_state.open_handshake_timeout, &handle); - let srv2 = srv.clone(); - let handle2 = handle.clone(); - - // 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::LBHeartBeat => { - write_json(socket, StatusCode::OK, serde_json::Value::from("")) - } - RequestType::Version => write_version_file(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).map_err(|_e| { - ApcErrorKind::GeneralError("failed to accept client".into()) - }); - let ws = - timeout(ws, srv2.app_state.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| { - trace!("🏓 starting ping manager"); - PingManager::new(&srv2, ws, uarx).map_err(|_e| { - ApcErrorKind::GeneralError( - "failed to make ping handler".into(), - ) - .into() - }) - }) - .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 { - // No need to log ConnectionClosed (denotes the - // connection closed normally) - if !matches!( - e.kind, - ApcErrorKind::Ws(tungstenite::error::Error::ConnectionClosed) - ) { - debug!("🤫 {}: {}", addr, e.to_string()); - } - } - Ok(()) - })); - - Ok(()) - }); - - if let Some(ref megaphone_url) = app_state.megaphone_api_url { - let megaphone_token = app_state - .megaphone_api_token - .as_ref() - .expect("Megaphone API requires a Megaphone API Token to be set"); - let fut = MegaphoneUpdater::new( - megaphone_url, - megaphone_token, - app_state.megaphone_poll_interval, - &srv2, - ) - .expect("Unable to start megaphone updater"); - core.handle().spawn(fut.then(|res| { - trace!("📢 megaphone result: {:?}", res.map(drop)); - Ok(()) - })); - } - core.handle().spawn(ws_srv.then(|res| { - debug!("srv res: {:?}", res.map(drop)); - Ok(()) - })); - - Ok((srv2, core)) - } - - /// Initialize broadcasts for a newly connected client - pub fn broadcast_init( - &self, - desired_broadcasts: &[Broadcast], - ) -> (BroadcastSubs, HashMap) { - trace!("📢Initialized broadcasts"); - let bc = self.broadcaster.borrow(); - let BroadcastSubsInit(broadcast_subs, broadcasts) = bc.broadcast_delta(desired_broadcasts); - let mut response = Broadcast::vec_into_hashmap(broadcasts); - let missing = bc.missing_broadcasts(desired_broadcasts); - if !missing.is_empty() { - response.insert( - "errors".to_string(), - BroadcastValue::Nested(Broadcast::vec_into_hashmap(missing)), - ); - } - (broadcast_subs, response) - } - - /// Calculate whether there's new broadcast versions to go out - pub fn broadcast_delta(&self, broadcast_subs: &mut BroadcastSubs) -> Option> { - trace!("📢 Checking broadcast_delta"); - self.broadcaster.borrow().change_count_delta(broadcast_subs) - } - - /// Process a broadcast list, adding new broadcasts to be tracked and locating missing ones - /// Returns an appropriate response for use by the prototocol - pub fn process_broadcasts( - &self, - broadcast_subs: &mut BroadcastSubs, - broadcasts: &[Broadcast], - ) -> Option> { - let bc = self.broadcaster.borrow(); - let mut response: HashMap = HashMap::new(); - let missing = bc.missing_broadcasts(broadcasts); - if !missing.is_empty() { - response.insert( - "errors".to_string(), - BroadcastValue::Nested(Broadcast::vec_into_hashmap(missing)), - ); - } - if let Some(delta) = bc.subscribe_to_broadcasts(broadcast_subs, broadcasts) { - response.extend(Broadcast::vec_into_hashmap(delta)); - }; - if response.is_empty() { - None - } else { - Some(response) - } - } -} - -/* -STATE MACHINE -*/ -enum MegaphoneState { - Waiting, - Requesting(MyFuture), -} - -struct MegaphoneUpdater { - srv: Rc, - api_url: String, - api_token: String, - state: MegaphoneState, - timeout: Timeout, - poll_interval: Duration, - client: reqwest::r#async::Client, -} - -impl MegaphoneUpdater { - fn new( - uri: &str, - token: &str, - poll_interval: Duration, - srv: &Rc, - ) -> io::Result { - let client = reqwest::r#async::Client::builder() - .timeout(Duration::from_secs(1)) - .build() - .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 = ApcError; - - fn poll(&mut self) -> Poll<(), ApcError> { - loop { - let new_state = match self.state { - MegaphoneState::Waiting => { - try_ready!(self.timeout.poll()); - trace!("📢Sending megaphone API request"); - let fut = self - .client - .get(&self.api_url) - .header("Authorization", self.api_token.clone()) - .send() - .and_then(|response| response.error_for_status()) - .and_then(|mut response| response.json()) - .map_err(|_| { - ApcErrorKind::GeneralError( - "Unable to query/decode the API query".into(), - ) - .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 })) => { - trace!("📢Fetched broadcasts: {:?}", broadcasts); - let mut broadcaster = self.srv.broadcaster.borrow_mut(); - for srv in Broadcast::from_hashmap(broadcasts) { - let vv = broadcaster.add_broadcast(srv); - trace!("📢 add_broadcast = {}", vv); - // TODO: Notify that Ping required? - } - } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(error) => { - error!("📢Failed to get response, queue again {error:?}"); - capture_message( - &format!("Failed to get response, queue again {error:?}"), - sentry::Level::Warning, - ); - } - }; - self.timeout.reset(at); - MegaphoneState::Waiting - } - }; - self.state = new_state; - } - } -} - -enum WaitingFor { - SendPing, - Pong, - Close, -} - -/* -STATE MACHINE -*/ -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, - ) -> 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)); - trace!("🏓Ping interval {:?}", &srv.app_state.auto_ping_interval); - Ok(PingManager { - timeout: Timeout::new(srv.app_state.auto_ping_interval, &srv.handle)?, - waiting: WaitingFor::SendPing, - socket: socket.clone(), - client: CloseState::Exchange(Client::new(socket, srv, uarx)), - srv: srv.clone(), - }) - } -} - -impl Future for PingManager { - type Item = (); - type Error = ApcError; - - fn poll(&mut self) -> Poll<(), ApcError> { - let mut socket = self.socket.borrow_mut(); - loop { - trace!("🏓 PingManager Poll loop"); - if socket.ws_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_ws_ping()?.is_ready() { - trace!("🏓 Time to ping"); - // If we just sent a broadcast, reset the ping interval and clear the delta - if socket.broadcast_delta.is_some() { - trace!("📢 Pending"); - let at = Instant::now() + self.srv.app_state.auto_ping_interval; - self.timeout.reset(at); - socket.broadcast_delta = None; - self.waiting = WaitingFor::SendPing - } else { - let at = Instant::now() + self.srv.app_state.auto_ping_timeout; - self.timeout.reset(at); - self.waiting = WaitingFor::Pong - } - } else { - break; - } - } - debug_assert!(!socket.ws_ping); - match self.waiting { - WaitingFor::SendPing => { - trace!( - "🏓Checking pong timeout:{} pong recv'd:{}", - socket.ws_pong_timeout, - socket.ws_pong_received - ); - debug_assert!(!socket.ws_pong_timeout); - debug_assert!(!socket.ws_pong_received); - match self.timeout.poll()? { - Async::Ready(()) => { - trace!("🏓scheduling a ws ping to get sent"); - socket.ws_ping = true; - } - Async::NotReady => { - trace!("🏓not ready yet"); - break; - } - } - } - WaitingFor::Pong => { - if socket.ws_pong_received { - // If we received a pong, then switch us back to waiting - // to send out a ping - trace!("🏓ws pong received, going back to sending a ping"); - debug_assert!(!socket.ws_pong_timeout); - let at = Instant::now() + self.srv.app_state.auto_ping_interval; - self.timeout.reset(at); - self.waiting = WaitingFor::SendPing; - socket.ws_pong_received = false; - } else if socket.ws_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 - trace!("🏓waiting for socket to see ws 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. - trace!("🏓waited too long for a ws pong"); - socket.ws_pong_timeout = true; - } else { - break; - } - } - WaitingFor::Close => { - debug_assert!(!socket.ws_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(ApcErrorKind::GeneralError( - "close handshake took too long".into(), - ) - .into()); - } - } - } - } - - // Be sure to always flush out any buffered messages/pings - socket.poll_complete().map_err(|_e| { - ApcErrorKind::GeneralError("failed routine `poll_complete` call".into()) - })?; - 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 self.socket.borrow_mut().close(), - } - - self.client = CloseState::Closing; - if let Some(dur) = self.srv.app_state.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, - ws_pong_received: bool, - ws_ping: bool, - ws_pong_timeout: bool, - broadcast_delta: Option>, -} - -impl WebpushSocket { - fn new(t: T) -> WebpushSocket { - WebpushSocket { - inner: t, - ws_pong_received: false, - ws_ping: false, - ws_pong_timeout: false, - broadcast_delta: None, - } - } - - fn send_ws_ping(&mut self) -> Poll<(), ApcError> - where - T: Sink, - ApcError: From, - { - trace!("🏓 checking ping"); - if self.ws_ping { - trace!("🏓 Ping present"); - let msg = if let Some(broadcasts) = self.broadcast_delta.clone() { - trace!("🏓sending a broadcast delta"); - let server_msg = ServerMessage::Broadcast { - broadcasts: Broadcast::vec_into_hashmap(broadcasts), - }; - let s = server_msg.to_json()?; - Message::Text(s) - } else { - trace!("🏓sending a ws ping"); - Message::Ping(Vec::new()) - }; - match self.inner.start_send(msg)? { - AsyncSink::Ready => { - trace!("🏓ws ping sent"); - self.ws_ping = false; - } - AsyncSink::NotReady(_) => { - trace!("🏓ws ping not ready to be sent"); - return Ok(Async::NotReady); - } - } - } else { - trace!("🏓No Ping"); - } - Ok(Async::Ready(())) - } -} - -impl Stream for WebpushSocket -where - T: Stream, - ApcError: From, -{ - type Item = ClientMessage; - type Error = ApcError; - - fn poll(&mut self) -> Poll, ApcError> { - 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.ws_pong_timeout { - return Err(ApcErrorKind::PongTimeout.into()); - } - return Ok(Async::NotReady); - } - }; - match msg { - Message::Text(ref s) => { - trace!("🢤 text message {}", s); - let msg = s - .parse() - .map_err(|_e| ApcErrorKind::InvalidClientMessage(s.to_owned()))?; - return Ok(Some(msg).into()); - } - - Message::Binary(_) => { - return Err( - ApcErrorKind::InvalidClientMessage("binary content".to_string()).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.ws_pong_received = true; - self.ws_pong_timeout = false; - task::current().notify(); - } - - Message::Close(_) => return Err(tungstenite::Error::ConnectionClosed.into()), - } - } - } -} - -impl Sink for WebpushSocket -where - T: Sink, - ApcError: From, -{ - type SinkItem = ServerMessage; - type SinkError = ApcError; - - fn start_send(&mut self, msg: ServerMessage) -> StartSend { - if self.send_ws_ping()?.is_not_ready() { - return Ok(AsyncSink::NotReady(msg)); - } - let s = msg - .to_json() - .map_err(|_e| ApcErrorKind::GeneralError("failed to serialize".into()))?; - match self.inner.start_send(Message::Text(s))? { - AsyncSink::Ready => Ok(AsyncSink::Ready), - AsyncSink::NotReady(_) => Ok(AsyncSink::NotReady(msg)), - } - } - - /// Handle the poll completion. - fn poll_complete(&mut self) -> Poll<(), ApcError> { - try_ready!(self.send_ws_ping()); - Ok(self.inner.poll_complete()?) - } - - fn close(&mut self) -> Poll<(), ApcError> { - try_ready!(self.poll_complete()); - match self.inner.close() { - Ok(v) => Ok(v), - Err(e) => { - warn!("Close generated an error, possibly ok?"); - Err(From::from(e)) - } - } - } -} - -fn write_status(socket: WebpushIo) -> MyFuture<()> { - write_json( - socket, - StatusCode::OK, - json!({ - "status": "OK", - "version": env!("CARGO_PKG_VERSION"), - }), - ) -} - -/// Return a static copy of `version.json` from compile time. -pub fn write_version_file(socket: WebpushIo) -> MyFuture<()> { - write_json( - socket, - StatusCode::OK, - serde_json::Value::from(include_str!("../../../version.json")), - ) -} - -fn write_log_check(socket: WebpushIo) -> MyFuture<()> { - let status = StatusCode::IM_A_TEAPOT; - let code: u16 = status.into(); - - error!("Test Critical Message"; - "status_code" => code, - "errno" => 0, - ); - thread::spawn(|| { - panic!("LogCheck"); - }); - - write_json( - socket, - StatusCode::IM_A_TEAPOT, - 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 = Utc::now().to_rfc2822(), - len = body.len(), - body = body, - ); - Box::new( - tokio_io::io::write_all(socket, data.into_bytes()) - .map(|_| ()) - .map_err(|_e| { - ApcErrorKind::GeneralError("failed to write status response".into()).into() - }), - ) -} diff --git a/autopush/src/server/protocol.rs b/autopush/src/server/protocol.rs deleted file mode 100644 index c2a4a0d84..000000000 --- a/autopush/src/server/protocol.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Definition of Internal Router 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 -//! -use std::collections::HashMap; -use std::str::FromStr; - -use serde_derive::{Deserialize, Serialize}; -use uuid::Uuid; - -use autopush_common::notification::Notification; - -#[derive(Debug, Serialize)] -#[serde(untagged)] -pub enum BroadcastValue { - Value(String), - Nested(HashMap), -} - -#[derive(Default)] -// Used for the server to flag a webpush client to deliver a Notification or Check storage -pub enum ServerNotification { - CheckStorage, - Notification(Notification), - #[default] - Disconnect, -} - -#[derive(Debug, 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, - }, - - Ping, -} - -impl FromStr for ClientMessage { - type Err = serde_json::error::Error; - - fn from_str(s: &str) -> Result { - // parse empty object "{}" as a Ping - serde_json::from_str::>(s) - .map(|_| ClientMessage::Ping) - .or_else(|_| serde_json::from_str(s)) - } -} - -#[derive(Debug, Deserialize)] -pub struct ClientAck { - #[serde(rename = "channelID")] - pub channel_id: Uuid, - pub version: String, -} - -#[derive(Debug, 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), - - Ping, -} - -impl ServerMessage { - pub fn to_json(&self) -> Result { - match self { - // clients recognize {"messageType": "ping"} but traditionally both - // client/server send the empty object version - ServerMessage::Ping => Ok("{}".to_owned()), - _ => serde_json::to_string(self), - } - } -} diff --git a/autopush/src/server/rc.rs b/autopush/src/server/rc.rs deleted file mode 100644 index 8eb47a458..000000000 --- a/autopush/src/server/rc.rs +++ /dev/null @@ -1,53 +0,0 @@ -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<'_, T> { - 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/autopush/src/server/registry.rs b/autopush/src/server/registry.rs deleted file mode 100644 index 11268b595..000000000 --- a/autopush/src/server/registry.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::collections::HashMap; - -use futures::{ - future::{err, ok}, - Future, -}; -use futures_locks::RwLock; -use uuid::Uuid; - -use super::protocol::ServerNotification; -use super::Notification; -use crate::client::RegisteredClient; -use autopush_common::errors::{ApcError, ApcErrorKind}; - -/// `notify` + `check_storage` are used under hyper (http.rs) which `Send` -/// futures. -/// -/// `MySendFuture` is `Send` for this, similar to modern futures `BoxFuture`. -/// `MyFuture` isn't, similar to modern futures `BoxLocalFuture` -pub type MySendFuture = Box + Send>; - -#[derive(Default)] -pub struct ClientRegistry { - clients: RwLock>, -} - -impl ClientRegistry { - /// 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(&self, client: RegisteredClient) -> MySendFuture<()> { - debug!("Connecting a client!"); - Box::new( - self.clients - .write() - .and_then(|mut clients| { - if let Some(client) = clients.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"); - } - } - ok(()) - }) - .map_err(|_| ApcErrorKind::GeneralError("clients lock poisoned".into()).into()), - ) - - //.map_err(|_| "clients lock poisioned") - } - - /// A notification has come for the uaid - pub fn notify(&self, uaid: Uuid, notif: Notification) -> MySendFuture<()> { - let fut = self - .clients - .read() - .and_then(move |clients| { - debug!("Sending notification"); - if let Some(client) = clients.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(()) - }) - .map_err(|_| ApcErrorKind::GeneralError("User not connected".into()).into()); - Box::new(fut) - } - - /// A check for notification command has come for the uaid - pub fn check_storage(&self, uaid: Uuid) -> MySendFuture<()> { - let fut = self - .clients - .read() - .and_then(move |clients| { - if let Some(client) = clients.get(&uaid) { - let result = client.tx.unbounded_send(ServerNotification::CheckStorage); - if result.is_ok() { - debug!("Told client to check storage"); - return ok(()); - } - } - err(()) - }) - .map_err(|_| ApcErrorKind::GeneralError("User not connected".into()).into()); - Box::new(fut) - } - - /// The client specified by `uaid` has disconnected. - #[allow(clippy::clone_on_copy)] - pub fn disconnect(&self, uaid: &Uuid, uid: &Uuid) -> MySendFuture<()> { - debug!("Disconnecting client!"); - let uaidc = uaid.clone(); - let uidc = uid.clone(); - let fut = self - .clients - .write() - .and_then(move |mut clients| { - let client_exists = clients - .get(&uaidc) - .map_or(false, |client| client.uid == uidc); - if client_exists { - clients.remove(&uaidc).expect("Couldn't remove client?"); - return ok(()); - } - err(()) - }) - .map_err(|_| ApcErrorKind::GeneralError("User not connected".into()).into()); - Box::new(fut) - } -} diff --git a/autopush/src/server/tls.rs b/autopush/src/server/tls.rs deleted file mode 100644 index 7f29f920c..000000000 --- a/autopush/src/server/tls.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! 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 crate::MyFuture; - -use autopush_common::errors::*; -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 crate::server::{AppState, Server}; - -/// 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(app_state: &AppState) -> Option { - let key = match app_state.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( - app_state - .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) = app_state.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) - .unwrap_or_else(|_| panic!("failed to open {path:?}")) - .read_to_end(&mut out) - .unwrap_or_else(|_| panic!("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) - .map_err(|_e| { - ApcErrorKind::GeneralError("failed to accept TLS socket".into()).into() - }), - ), - 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/autopush/src/server/webpush_io.rs b/autopush/src/server/webpush_io.rs deleted file mode 100644 index cdd945361..000000000 --- a/autopush/src/server/webpush_io.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! 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::backtrace; -use std::io::{self, Read, Write}; - -use bytes::BytesMut; -use futures::Poll; -use tokio_core::net::TcpStream; -use tokio_io::{AsyncRead, AsyncWrite}; - -use crate::server::tls::MaybeTlsStream; - -pub struct WebpushIo { - tcp: MaybeTlsStream, - header_to_read: Option, -} - -impl WebpushIo { - pub fn new(tcp: MaybeTlsStream, header: BytesMut) -> Self { - Self { - 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; - let res = self.tcp.read(buf); - if res.is_err() { - if let Some(e) = res.as_ref().err() { - if e.kind() == std::io::ErrorKind::WouldBlock { - // quietly report the error. The socket closed early. - trace!("🢤 Detected WouldBlock, connection terminated abruptly"); - } else { - // report the backtrace because it can be difficult to determine - // what the caller is. - warn!("🢤 ERR: {:?}\n{:?}", &e, backtrace::Backtrace::capture()); - } - } - } else { - trace!("🢤 {:?}", &res); - } - res - } -} - -// 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 { - trace!("🢧 {:?}", String::from_utf8_lossy(buf)); - self.tcp.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - trace!("🚽"); - 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/autopush/src/settings.rs b/autopush/src/settings.rs deleted file mode 100644 index 143b5b9e5..000000000 --- a/autopush/src/settings.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::io; -use std::net::ToSocketAddrs; - -use config::{Config, ConfigError, Environment, File}; -use fernet::Fernet; -use lazy_static::lazy_static; -use serde_derive::Deserialize; - -const ENV_PREFIX: &str = "autopush"; - -lazy_static! { - static ref HOSTNAME: String = mozsvc_common::get_hostname() - .expect("Couldn't get_hostname") - .into_string() - .expect("Couldn't convert get_hostname"); - static ref RESOLVED_HOSTNAME: String = resolve_ip(&HOSTNAME) - .unwrap_or_else(|_| panic!("Failed to resolve hostname: {}", *HOSTNAME)); -} - -/// Resolve a hostname to its IP if possible -fn resolve_ip(hostname: &str) -> io::Result { - Ok((hostname, 0) - .to_socket_addrs()? - .next() - .map_or_else(|| hostname.to_owned(), |addr| addr.ip().to_string())) -} - -/// Indicate whether the port should be included for the given scheme -fn include_port(scheme: &str, port: u16) -> bool { - !((scheme == "http" && port == 80) || (scheme == "https" && port == 443)) -} - -#[derive(Debug, Deserialize)] -#[serde(default)] -pub struct Settings { - 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: String, - 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, - pub msg_limit: u32, -} - -impl Default for Settings { - fn default() -> Self { - Self { - port: 8080, - hostname: None, - resolve_hostname: false, - router_port: 8081, - router_hostname: None, - router_tablename: "router".to_owned(), - message_tablename: "message".to_owned(), - router_ssl_key: None, - router_ssl_cert: None, - router_ssl_dh_param: None, - auto_ping_interval: 300.0, - auto_ping_timeout: 4.0, - max_connections: 0, - close_handshake_timeout: 0, - endpoint_scheme: "http".to_owned(), - endpoint_hostname: "localhost".to_owned(), - endpoint_port: 8082, - crypto_key: format!("[{}]", Fernet::generate_key()), - statsd_host: "localhost".to_owned(), - statsd_port: 8125, - aws_ddb_endpoint: None, - megaphone_api_url: None, - megaphone_api_token: None, - megaphone_poll_interval: 30, - human_logs: false, - msg_limit: 100, - } - } -} - -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::builder(); - - // Merge the configs from the files - for filename in filenames { - s = s.add_source(File::with_name(filename)); - } - - // Merge the environment overrides - s = s.add_source(Environment::with_prefix(ENV_PREFIX).separator("__")); - // s = s.add_source(Environment::with_prefix(ENV_PREFIX)); - - let built = s.build()?; - built.try_deserialize::() - } - - pub fn router_url(&self) -> String { - let router_scheme = if self.router_ssl_key.is_none() { - "http" - } else { - "https" - }; - let url = format!( - "{}://{}", - router_scheme, - self.router_hostname - .as_ref() - .map_or_else(|| self.get_hostname(), String::clone), - ); - if include_port(router_scheme, self.router_port) { - format!("{}:{}", url, self.router_port) - } else { - url - } - } - - pub fn endpoint_url(&self) -> String { - let url = format!("{}://{}", self.endpoint_scheme, self.endpoint_hostname,); - if include_port(&self.endpoint_scheme, self.endpoint_port) { - format!("{}:{}", url, self.endpoint_port) - } else { - url - } - } - - fn get_hostname(&self) -> String { - if let Some(ref hostname) = self.hostname { - if self.resolve_hostname { - resolve_ip(hostname) - .unwrap_or_else(|_| panic!("Failed to resolve provided hostname: {hostname}")) - } else { - hostname.clone() - } - } else if self.resolve_hostname { - RESOLVED_HOSTNAME.clone() - } else { - HOSTNAME.clone() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_router_url() { - let mut settings = Settings { - router_hostname: Some("testname".to_string()), - router_port: 80, - ..Default::default() - }; - let url = settings.router_url(); - assert_eq!("http://testname", url); - - settings.router_port = 8080; - let url = settings.router_url(); - assert_eq!("http://testname:8080", url); - - settings.router_port = 443; - settings.router_ssl_key = Some("key".to_string()); - let url = settings.router_url(); - assert_eq!("https://testname", url); - - settings.router_port = 8080; - let url = settings.router_url(); - assert_eq!("https://testname:8080", url); - } - - #[test] - fn test_endpoint_url() { - let mut settings = Settings { - endpoint_hostname: "testname".to_string(), - endpoint_port: 80, - endpoint_scheme: "http".to_string(), - ..Default::default() - }; - let url = settings.endpoint_url(); - assert_eq!("http://testname", url); - - settings.endpoint_port = 8080; - let url = settings.endpoint_url(); - assert_eq!("http://testname:8080", url); - - settings.endpoint_port = 443; - settings.endpoint_scheme = "https".to_string(); - let url = settings.endpoint_url(); - assert_eq!("https://testname", url); - - settings.endpoint_port = 8080; - let url = settings.endpoint_url(); - assert_eq!("https://testname:8080", url); - } - - #[test] - fn test_default_settings() { - // Test that the Config works the way we expect it to. - use std::env; - let port = format!("{ENV_PREFIX}__PORT").to_uppercase(); - let msg_limit = format!("{ENV_PREFIX}__MSG_LIMIT").to_uppercase(); - let fernet = format!("{ENV_PREFIX}__CRYPTO_KEY").to_uppercase(); - - let v1 = env::var(&port); - let v2 = env::var(&msg_limit); - env::set_var(&port, "9123"); - env::set_var(&msg_limit, "123"); - env::set_var(&fernet, "[mqCGb8D-N7mqx6iWJov9wm70Us6kA9veeXdb8QUuzLQ=]"); - let settings = Settings::with_env_and_config_files(&Vec::new()).unwrap(); - assert_eq!(settings.endpoint_hostname, "localhost".to_owned()); - assert_eq!(&settings.port, &9123); - assert_eq!(&settings.msg_limit, &123); - assert_eq!( - &settings.crypto_key, - "[mqCGb8D-N7mqx6iWJov9wm70Us6kA9veeXdb8QUuzLQ=]" - ); - - // reset (just in case) - if let Ok(p) = v1 { - trace!("Resetting {}", &port); - env::set_var(&port, p); - } else { - env::remove_var(&port); - } - if let Ok(p) = v2 { - trace!("Resetting {}", msg_limit); - env::set_var(&msg_limit, p); - } else { - env::remove_var(&msg_limit); - } - env::remove_var(&fernet); - } -} diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index de0bd87ee..e02de095c 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -72,7 +72,6 @@ "autoconnect_ws", "autoconnect_ws_sm", "autoendpoint", - "autopush", "autopush_common", ] log_string = [f"{x}=trace" for x in modules]