Files
Stalwart/tests/src/jmap/push_subscription.rs

414 lines
13 KiB
Rust

/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use base64::{engine::general_purpose, Engine};
use common::{config::server::Servers, listener::SessionData, Core};
use directory::backend::internal::manage::ManageDirectory;
use ece::EcKeyComponents;
use hyper::{body, header::CONTENT_ENCODING, server::conn::http1, service::service_fn, StatusCode};
use hyper_util::rt::TokioIo;
use jmap::{
api::{
http::{fetch_body, ToHttpResponse},
HtmlResponse, StateChangeResponse,
},
push::ece::ece_encrypt,
};
use jmap_client::{mailbox::Role, push_subscription::Keys};
use jmap_proto::types::{id::Id, type_state::DataType};
use store::ahash::AHashSet;
use tokio::sync::mpsc;
use utils::config::Config;
use crate::{
add_test_certs,
jmap::{assert_is_empty, mailbox::destroy_all_mailboxes, test_account_login},
AssertConfig,
};
use super::JMAPTest;
const SERVER: &str = r#"
[server]
hostname = "'jmap-push.example.org'"
http.url = "'https://127.0.0.1:9000'"
[server.listener.jmap]
bind = ['127.0.0.1:9000']
protocol = 'http'
tls.implicit = true
[server.socket]
reuse-addr = true
[certificate.default]
cert = '%{file:{CERT}}%'
private-key = '%{file:{PK}}%'
default = true
"#;
pub async fn test(params: &mut JMAPTest) {
println!("Running Push Subscription tests...");
// Create test account
let server = params.server.clone();
params
.directory
.create_test_user_with_email("jdoe@example.com", "12345", "John Doe")
.await;
let account_id = Id::from(
server
.core
.storage
.data
.get_or_create_account_id("jdoe@example.com")
.await
.unwrap(),
);
params.client.set_default_account_id(account_id);
let client = test_account_login("jdoe@example.com", "12345").await;
// Create channels
let (event_tx, mut event_rx) = mpsc::channel::<PushMessage>(100);
// Create subscription keys
let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret().unwrap();
let pubkey = keypair.pub_as_raw().unwrap();
let keys = Keys::new(&pubkey, &auth_secret);
let push_server = Arc::new(PushServer {
keypair: keypair.raw_components().unwrap(),
auth_secret: auth_secret.to_vec(),
tx: event_tx,
fail_requests: false.into(),
});
// Start mock push server
let mut settings = Config::new(add_test_certs(SERVER)).unwrap();
settings.resolve_all_macros().await;
let mock_core = Core::parse(&mut settings, Default::default(), Default::default())
.await
.into_shared();
settings.errors.clear();
settings.warnings.clear();
let mut servers = Servers::parse(&mut settings);
servers.parse_tcp_acceptors(&mut settings, mock_core.clone());
// Start JMAP server
let manager = SessionManager::from(push_server.clone());
servers.bind_and_drop_priv(&mut settings);
settings.assert_no_errors();
let _shutdown_tx = servers.spawn(|server, acceptor, shutdown_rx| {
server.spawn(manager.clone(), mock_core.clone(), acceptor, shutdown_rx);
});
// Register push notification (no encryption)
let push_id = client
.push_subscription_create("123", "https://127.0.0.1:9000/push", None)
.await
.unwrap()
.take_id();
// Expect push verification
let verification = expect_push(&mut event_rx).await.unwrap_verification();
assert_eq!(verification.push_subscription_id, push_id);
// Update verification code
client
.push_subscription_verify(&push_id, verification.verification_code)
.await
.unwrap();
// Create a mailbox and expect a state change
let mailbox_id = client
.mailbox_create("PushSubscription Test", None::<String>, Role::None)
.await
.unwrap()
.take_id();
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
// Receive states just for the requested types
client
.push_subscription_update_types(&push_id, [jmap_client::TypeState::Email].into())
.await
.unwrap();
client
.mailbox_update_sort_order(&mailbox_id, 123)
.await
.unwrap();
expect_nothing(&mut event_rx).await;
// Destroy subscription
client.push_subscription_destroy(&push_id).await.unwrap();
// Only one verification per minute is allowed
let push_id = client
.push_subscription_create("invalid", "https://127.0.0.1:9000/push", None)
.await
.unwrap()
.take_id();
expect_nothing(&mut event_rx).await;
client.push_subscription_destroy(&push_id).await.unwrap();
// Register push notification (with encryption)
let push_id = client
.push_subscription_create(
"123",
"https://127.0.0.1:9000/push?skip_checks=true", // skip_checks only works in cfg(test)
keys.into(),
)
.await
.unwrap()
.take_id();
// Expect push verification
let verification = expect_push(&mut event_rx).await.unwrap_verification();
assert_eq!(verification.push_subscription_id, push_id);
// Update verification code
client
.push_subscription_verify(&push_id, verification.verification_code)
.await
.unwrap();
// Failed deliveries should be re-attempted
push_server.fail_requests.store(true, Ordering::Relaxed);
client
.mailbox_update_sort_order(&mailbox_id, 101)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
push_server.fail_requests.store(false, Ordering::Relaxed);
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
// Make a mailbox change and expect state change
client
.mailbox_rename(&mailbox_id, "My Mailbox")
.await
.unwrap();
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
//expect_nothing(&mut event_rx).await;
// Multiple change updates should be grouped and pushed in intervals
for num in 0..5 {
client
.mailbox_update_sort_order(&mailbox_id, num)
.await
.unwrap();
}
assert_state(&mut event_rx, &account_id, &[DataType::Mailbox]).await;
expect_nothing(&mut event_rx).await;
// Destroy mailbox
client.push_subscription_destroy(&push_id).await.unwrap();
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
expect_nothing(&mut event_rx).await;
destroy_all_mailboxes(params).await;
assert_is_empty(server).await;
}
#[derive(Clone)]
pub struct SessionManager {
pub inner: Arc<PushServer>,
}
impl From<Arc<PushServer>> for SessionManager {
fn from(inner: Arc<PushServer>) -> Self {
SessionManager { inner }
}
}
pub struct PushServer {
keypair: EcKeyComponents,
auth_secret: Vec<u8>,
tx: mpsc::Sender<PushMessage>,
fail_requests: AtomicBool,
}
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum PushMessage {
StateChange(StateChangeResponse),
Verification(PushVerification),
}
impl PushMessage {
pub fn unwrap_state_change(self) -> StateChangeResponse {
match self {
PushMessage::StateChange(state_change) => state_change,
_ => panic!("Expected StateChange"),
}
}
pub fn unwrap_verification(self) -> PushVerification {
match self {
PushMessage::Verification(verification) => verification,
_ => panic!("Expected Verification"),
}
}
}
#[derive(serde::Deserialize, Debug)]
enum PushVerificationType {
PushVerification,
}
#[derive(serde::Deserialize, Debug)]
struct PushVerification {
#[serde(rename = "@type")]
_type: PushVerificationType,
#[serde(rename = "pushSubscriptionId")]
pub push_subscription_id: String,
#[serde(rename = "verificationCode")]
pub verification_code: String,
}
impl common::listener::SessionManager for SessionManager {
#[allow(clippy::manual_async_fn)]
fn handle<T: common::listener::SessionStream>(
self,
session: SessionData<T>,
) -> impl std::future::Future<Output = ()> + Send {
async move {
let push = self.inner;
let _ = http1::Builder::new()
.keep_alive(false)
.serve_connection(
TokioIo::new(session.stream),
service_fn(|mut req: hyper::Request<body::Incoming>| {
let push = push.clone();
async move {
if push.fail_requests.load(Ordering::Relaxed) {
return Ok(HtmlResponse::with_status(
StatusCode::TOO_MANY_REQUESTS,
"too many requests".to_string(),
)
.into_http_response());
}
let is_encrypted = req
.headers()
.get(CONTENT_ENCODING)
.map_or(false, |encoding| {
encoding.to_str().unwrap() == "aes128gcm"
});
let body = fetch_body(&mut req, 1024 * 1024).await.unwrap();
let message = serde_json::from_slice::<PushMessage>(&if is_encrypted {
ece::decrypt(
&push.keypair,
&push.auth_secret,
&general_purpose::URL_SAFE.decode(body).unwrap(),
)
.unwrap()
} else {
body
})
.unwrap();
//println!("Push received ({}): {:?}", is_encrypted, message);
push.tx.send(message).await.unwrap();
Ok::<_, hyper::Error>(
HtmlResponse::new("ok".to_string()).into_http_response(),
)
}
}),
)
.await;
}
}
#[allow(clippy::manual_async_fn)]
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
async {}
}
}
async fn expect_push(event_rx: &mut mpsc::Receiver<PushMessage>) -> PushMessage {
match tokio::time::timeout(Duration::from_millis(1500), event_rx.recv()).await {
Ok(Some(push)) => {
//println!("Push received: {:?}", push);
push
}
result => {
panic!("Timeout waiting for push: {:?}", result);
}
}
}
async fn expect_nothing(event_rx: &mut mpsc::Receiver<PushMessage>) {
match tokio::time::timeout(Duration::from_millis(1000), event_rx.recv()).await {
Err(_) => {}
message => {
panic!("Received a message when expecting nothing: {:?}", message);
}
}
}
async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state: &[DataType]) {
assert_eq!(
expect_push(event_rx)
.await
.unwrap_state_change()
.changed
.get(id)
.unwrap()
.iter()
.map(|x| x.0)
.collect::<AHashSet<&DataType>>(),
state.iter().collect::<AHashSet<&DataType>>()
);
}
#[test]
fn ece_roundtrip() {
for len in [1, 2, 5, 16, 256, 1024, 2048, 4096, 1024 * 1024] {
let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret().unwrap();
let bytes: Vec<u8> = (0..len).map(|_| store::rand::random::<u8>()).collect();
let encrypted_bytes =
ece_encrypt(&keypair.pub_as_raw().unwrap(), &auth_secret, &bytes).unwrap();
let decrypted_bytes = ece::decrypt(
&keypair.raw_components().unwrap(),
&auth_secret,
&encrypted_bytes,
)
.unwrap();
assert_eq!(bytes, decrypted_bytes, "len: {}", len);
}
}