aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 1 | use aws_lc_rs::agreement::{ECDH_P256, PrivateKey}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 2 | use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 3 | use malice_push::{Allowlist, Message, Outcome, Sender, Subscription, Urgency, Vapid, ece}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 4 | use wiremock::{ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 5 | Mock, MockServer, ResponseTemplate, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 6 | matchers::{header, method, path}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 7 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 8 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 9 | struct Browser { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 10 | private: PrivateKey, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 11 | auth: [u8; 16], |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 12 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 13 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 14 | impl Browser { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 15 | fn new() -> Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 16 | Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 17 | private: PrivateKey::generate(&ECDH_P256).unwrap(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 18 | auth: [3; 16], |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 19 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 20 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 21 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 22 | fn subscription(&self, endpoint: String) -> Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 23 | Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 24 | endpoint, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 25 | p256dh: URL_SAFE_NO_PAD.encode(self.private.compute_public_key().unwrap().as_ref()), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 26 | auth: URL_SAFE_NO_PAD.encode(self.auth), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 27 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 28 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 29 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 30 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 31 | async fn sender(server: &MockServer, dir: &tempfile::TempDir) -> Sender { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 32 | let authority = server.address().to_string(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 33 | Sender::new( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 34 | Vapid::load_or_create(&dir.path().join("vapid.pk8")).unwrap(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 35 | "mailto:test@example.org".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 36 | Allowlist::with_extra([authority]), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 37 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 38 | .unwrap() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 39 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 40 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 41 | #[tokio::test] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 42 | async fn delivers_encrypted_and_reports_gone_retry_and_refusal() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 43 | let server = MockServer::start().await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 44 | Mock::given(method("POST")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 45 | .and(path("/push/ok")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 46 | .and(header("content-encoding", "aes128gcm")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 47 | .and(header("urgency", "high")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 48 | .and(header("ttl", "43200")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 49 | .respond_with(ResponseTemplate::new(201)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 50 | .expect(1) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 51 | .mount(&server) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 52 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 53 | Mock::given(method("POST")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 54 | .and(path("/push/gone")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 55 | .respond_with(ResponseTemplate::new(410)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 56 | .mount(&server) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 57 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 58 | Mock::given(method("POST")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 59 | .and(path("/push/flaky")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 60 | .respond_with(ResponseTemplate::new(503)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 61 | .up_to_n_times(1) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 62 | .with_priority(1) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 63 | .mount(&server) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 64 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 65 | Mock::given(method("POST")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 66 | .and(path("/push/flaky")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 67 | .respond_with(ResponseTemplate::new(201)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 68 | .with_priority(2) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 69 | .mount(&server) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 70 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 71 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 72 | let dir = tempfile::tempdir().unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 73 | let sender = sender(&server, &dir).await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 74 | let browser = Browser::new(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 75 | let message = Message { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 76 | payload: br#"{"web_push":8030}"#, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 77 | urgency: Urgency::High, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 78 | topic: "!room:example.org", |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 79 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 80 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 81 | let ok = browser.subscription(format!("{}/push/ok", server.uri())); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 82 | assert_eq!(sender.send(&ok, &message).await, Outcome::Delivered); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 83 | let requests = server.received_requests().await.unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 84 | let request = &requests[0]; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 85 | let authorization = request.headers["authorization"].to_str().unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 86 | assert!(authorization.starts_with("vapid t=")); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 87 | assert!(authorization.ends_with(&format!("k={}", sender.vapid_public_key()))); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 88 | assert_eq!(request.headers["topic"].to_str().unwrap().len(), 32); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 89 | let plain = ece::decrypt(&browser.private, &browser.auth, &request.body).unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 90 | assert_eq!(plain, message.payload); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 91 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 92 | let gone = browser.subscription(format!("{}/push/gone", server.uri())); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 93 | let outcome = sender.send(&gone, &message).await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 94 | assert_eq!(outcome, Outcome::Gone); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 95 | assert!(outcome.drops_subscription()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 96 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 97 | let flaky = browser.subscription(format!("{}/push/flaky", server.uri())); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 98 | assert_eq!(sender.send(&flaky, &message).await, Outcome::Delivered); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 99 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 100 | let elsewhere = browser.subscription("https://example.org/push".to_owned()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 101 | assert_eq!(sender.send(&elsewhere, &message).await, Outcome::Refused); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 102 | let bad_keys = Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 103 | p256dh: "AAAA".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 104 | ..ok |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 105 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 106 | assert_eq!(sender.send(&bad_keys, &message).await, Outcome::Refused); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 107 | } |