aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 1 | use std::{ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 2 | collections::HashMap, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 3 | path::{Path, PathBuf}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 4 | sync::{Mutex, MutexGuard, PoisonError}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 5 | time::Duration, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 6 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 7 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 8 | use axum::{ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 9 | Router, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 10 | body::Bytes, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 11 | extract::{DefaultBodyLimit, State}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 12 | http::{HeaderMap, StatusCode}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 13 | response::{IntoResponse, Response}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 14 | routing::{get, post}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 15 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 16 | use macltok::{HttpPusher, PushFormat, PushNotificationStatus, PusherKey}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 17 | use macltok_push_gateway::{GatewayDevice, GatewayNotification, notify_response, parse_notify}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 18 | use malice_core::push::{notice, unread_total}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 19 | use malice_push::{Message, Outcome, Sender, Subscription, Urgency}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 20 | use malice_static::push::{NOTIFY_PATH, json_response}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 21 | use rand_core::{OsRng, RngCore}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 22 | use serde::{Deserialize, Serialize}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 23 | use tokio::{sync::Semaphore, time::timeout}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 24 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 25 | use crate::{ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 26 | error::{ApiError, json_ok, no_content}, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 27 | routes::auth, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 28 | state::{AppState, RestoreError}, |
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 | pub const HOSTED_APP_ID: &str = "dev.malice.hosted"; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 32 | const CONCURRENT_NOTIFIES: usize = 8; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 33 | const RESOLVE_TIMEOUT: Duration = Duration::from_secs(20); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 34 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 35 | #[derive(Debug, Clone, Serialize, Deserialize)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 36 | struct Entry { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 37 | sid: String, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 38 | subscription: Subscription, |
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 | /// Browser subscriptions by the random pushkey each one's pusher carries, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 42 | /// kept in `push.json` beside the sessions. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 43 | pub struct HostedPush { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 44 | sender: Sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 45 | public_url: Option<String>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 46 | path: PathBuf, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 47 | entries: Mutex<HashMap<String, Entry>>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 48 | permits: Semaphore, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 49 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 50 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 51 | impl HostedPush { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 52 | pub fn new(data_dir: &Path, public_url: Option<String>, sender: Sender) -> Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 53 | let path = data_dir.join("push.json"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 54 | let entries = match std::fs::read(&path) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 55 | Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_else(|error| { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 56 | tracing::warn!(%error, "push subscriptions are unreadable, starting empty"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 57 | HashMap::new() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 58 | }), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 59 | Err(_) => HashMap::new(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 60 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 61 | Self { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 62 | sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 63 | public_url, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 64 | path, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 65 | entries: Mutex::new(entries), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 66 | permits: Semaphore::new(CONCURRENT_NOTIFIES), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 67 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 68 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 69 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 70 | /// The VAPID key lives in `data_dir`. Subscribing needs `MALICE_PUBLIC_URL`, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 71 | /// since the homeserver must reach the notify endpoint. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 72 | pub fn from_env(data_dir: &Path) -> Result<Self, String> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 73 | let public_url = malice_push::env::public_url(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 74 | if public_url.is_none() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 75 | tracing::info!("web push is off until MALICE_PUBLIC_URL is set"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 76 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 77 | let sender = malice_push::env::sender(data_dir, public_url.as_deref())?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 78 | Ok(Self::new(data_dir, public_url, sender)) |
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 | fn entries(&self) -> MutexGuard<'_, HashMap<String, Entry>> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 82 | self.entries.lock().unwrap_or_else(PoisonError::into_inner) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 83 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 84 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 85 | fn save(&self, entries: &HashMap<String, Entry>) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 86 | let result = serde_json::to_vec(entries) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 87 | .map_err(std::io::Error::other) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 88 | .and_then(|bytes| { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 89 | let temp = self.path.with_extension("json.tmp"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 90 | let mut options = std::fs::OpenOptions::new(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 91 | options.write(true).create(true).truncate(true); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 92 | #[cfg(unix)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 93 | { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 94 | use std::os::unix::fs::OpenOptionsExt; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 95 | options.mode(0o600); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 96 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 97 | let mut file = options.open(&temp)?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 98 | std::io::Write::write_all(&mut file, &bytes)?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 99 | file.sync_all()?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 100 | std::fs::rename(&temp, &self.path) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 101 | }); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 102 | if let Err(error) = result { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 103 | tracing::warn!(%error, "saving push subscriptions failed"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 104 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 105 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 106 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 107 | /// Files `subscription` under `sid`, keeping the pushkey if this session |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 108 | /// already had it. Returns the pushkey, and the session and pushkey that |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 109 | /// held the endpoint before when another session did. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 110 | fn claim(&self, sid: &str, subscription: Subscription) -> (String, Option<(String, String)>) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 111 | let mut entries = self.entries(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 112 | let held = entries |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 113 | .iter() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 114 | .find(|(_, entry)| entry.subscription.endpoint == subscription.endpoint) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 115 | .map(|(pushkey, entry)| (pushkey.clone(), entry.sid.clone())); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 116 | let (pushkey, displaced) = match held { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 117 | Some((pushkey, owner)) if owner == sid => (pushkey, None), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 118 | Some((pushkey, owner)) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 119 | entries.remove(&pushkey); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 120 | (new_pushkey(), Some((owner, pushkey))) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 121 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 122 | None => (new_pushkey(), None), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 123 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 124 | entries.insert( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 125 | pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 126 | Entry { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 127 | sid: sid.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 128 | subscription, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 129 | }, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 130 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 131 | self.save(&entries); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 132 | (pushkey, displaced) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 133 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 134 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 135 | fn get(&self, pushkey: &str) -> Option<Entry> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 136 | self.entries().get(pushkey).cloned() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 137 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 138 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 139 | fn find(&self, sid: &str, endpoint: &str) -> Option<String> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 140 | self.entries() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 141 | .iter() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 142 | .find(|(_, entry)| entry.sid == sid && entry.subscription.endpoint == endpoint) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 143 | .map(|(pushkey, _)| pushkey.clone()) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 144 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 145 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 146 | fn forget(&self, pushkey: &str) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 147 | let mut entries = self.entries(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 148 | if entries.remove(pushkey).is_some() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 149 | self.save(&entries); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 150 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 151 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 152 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 153 | /// The pushers a logout of `sid` should take down with it. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 154 | pub fn pusher_keys(&self, sid: &str) -> Vec<PusherKey> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 155 | self.entries() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 156 | .iter() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 157 | .filter(|(_, entry)| entry.sid == sid) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 158 | .map(|(pushkey, _)| PusherKey { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 159 | app_id: HOSTED_APP_ID.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 160 | pushkey: pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 161 | }) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 162 | .collect() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 163 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 164 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 165 | pub fn forget_session(&self, sid: &str) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 166 | let mut entries = self.entries(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 167 | let before = entries.len(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 168 | entries.retain(|_, entry| entry.sid != sid); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 169 | if entries.len() != before { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 170 | self.save(&entries); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 171 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 172 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 173 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 174 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 175 | fn new_pushkey() -> String { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 176 | let mut bytes = [0u8; 16]; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 177 | OsRng.fill_bytes(&mut bytes); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 178 | hex::encode(bytes) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 179 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 180 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 181 | #[derive(Debug, Deserialize)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 182 | struct SubscribeKeys { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 183 | p256dh: String, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 184 | auth: String, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 185 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 186 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 187 | #[derive(Debug, Deserialize)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 188 | struct SubscribeBody { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 189 | endpoint: String, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 190 | keys: SubscribeKeys, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 191 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 192 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 193 | #[derive(Debug, Deserialize)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 194 | struct UnsubscribeBody { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 195 | endpoint: String, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 196 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 197 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 198 | pub fn api_routes() -> Router<AppState> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 199 | Router::new() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 200 | .route("/push/vapid", get(vapid)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 201 | .route("/push/subscribe", post(subscribe)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 202 | .route("/push/unsubscribe", post(unsubscribe)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 203 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 204 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 205 | pub fn notify_route() -> Router<AppState> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 206 | Router::new() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 207 | .route(NOTIFY_PATH, post(notify)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 208 | .layer(DefaultBodyLimit::max(64 * 1024)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 209 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 210 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 211 | fn push_off() -> ApiError { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 212 | ApiError::new(StatusCode::SERVICE_UNAVAILABLE, "web push is not set up") |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 213 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 214 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 215 | /// The push service, the public URL to point pushers at, or why push is off. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 216 | fn configured(state: &AppState) -> Result<(&HostedPush, &str), ApiError> { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 217 | let push = state.push().ok_or_else(push_off)?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 218 | let url = push.public_url.as_deref().ok_or_else(push_off)?; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 219 | Ok((push, url)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 220 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 221 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 222 | async fn vapid(State(state): State<AppState>) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 223 | match configured(&state) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 224 | Ok((push, _)) => json_ok( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 225 | &serde_json::json!({ "public_key": push.sender.vapid_public_key() }), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 226 | None, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 227 | ), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 228 | Err(error) => error.into_response(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 229 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 230 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 231 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 232 | async fn subscribe( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 233 | State(state): State<AppState>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 234 | headers: HeaderMap, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 235 | body: axum::Json<SubscribeBody>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 236 | ) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 237 | let (push, public_url) = match configured(&state) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 238 | Ok(configured) => configured, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 239 | Err(error) => return error.into_response(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 240 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 241 | let (user, sid) = match auth(&state, &headers).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 242 | Ok(pair) => pair, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 243 | Err(error) => return error.into_response(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 244 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 245 | let subscription = Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 246 | endpoint: body.0.endpoint, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 247 | p256dh: body.0.keys.p256dh, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 248 | auth: body.0.keys.auth, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 249 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 250 | if !push.sender.allows(&subscription.endpoint) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 251 | return ApiError::bad_request("endpoint is not a known push service").into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 252 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 253 | if !subscription.keys_valid() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 254 | return ApiError::bad_request("subscription keys are not valid").into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 255 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 256 | let (pushkey, displaced) = push.claim(&sid, subscription); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 257 | if let Some((owner, pushkey)) = displaced { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 258 | let state = state.clone(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 259 | tokio::spawn(async move { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 260 | if let Ok(owner) = state.get_or_restore(&owner).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 261 | let _ = owner |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 262 | .handle |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 263 | .remove_pusher(PusherKey { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 264 | app_id: HOSTED_APP_ID.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 265 | pushkey, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 266 | }) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 267 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 268 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 269 | }); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 270 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 271 | let pusher = HttpPusher { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 272 | app_id: HOSTED_APP_ID.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 273 | pushkey: pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 274 | app_display_name: "malice".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 275 | device_display_name: "Browser".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 276 | lang: "en".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 277 | url: format!("{public_url}{NOTIFY_PATH}"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 278 | format: PushFormat::EventIdOnly, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 279 | data: serde_json::Map::new(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 280 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 281 | match user.handle.set_pusher(pusher).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 282 | Ok(()) => no_content(None), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 283 | Err(error) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 284 | push.forget(&pushkey); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 285 | ApiError::from(malice_core::error::CoreError::from(error)).into_response() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 286 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 287 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 288 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 289 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 290 | async fn unsubscribe( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 291 | State(state): State<AppState>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 292 | headers: HeaderMap, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 293 | body: axum::Json<UnsubscribeBody>, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 294 | ) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 295 | let Some(push) = state.push() else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 296 | return push_off().into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 297 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 298 | let (user, sid) = match auth(&state, &headers).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 299 | Ok(pair) => pair, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 300 | Err(error) => return error.into_response(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 301 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 302 | let Some(pushkey) = push.find(&sid, &body.0.endpoint) else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 303 | return no_content(None); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 304 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 305 | let removed = user |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 306 | .handle |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 307 | .remove_pusher(PusherKey { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 308 | app_id: HOSTED_APP_ID.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 309 | pushkey: pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 310 | }) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 311 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 312 | match removed { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 313 | Ok(()) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 314 | push.forget(&pushkey); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 315 | no_content(None) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 316 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 317 | Err(error) => ApiError::from(malice_core::error::CoreError::from(error)).into_response(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 318 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 319 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 320 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 321 | async fn notify(State(state): State<AppState>, body: Bytes) -> Response { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 322 | let Some(push) = state.push() else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 323 | return StatusCode::NOT_FOUND.into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 324 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 325 | let notification = match parse_notify(&body) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 326 | Ok(notification) => notification, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 327 | Err(error) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 328 | return ApiError::bad_request(error.to_string()).into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 329 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 330 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 331 | if !notification.is_event() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 332 | return json_response(StatusCode::OK, notify_response(&[])); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 333 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 334 | let Ok(_permit) = push.permits.acquire().await else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 335 | return StatusCode::SERVICE_UNAVAILABLE.into_response(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 336 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 337 | let mut rejected = Vec::new(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 338 | for device in ¬ification.devices { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 339 | if deliver(&state, push, ¬ification, device).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 340 | push.forget(&device.pushkey); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 341 | rejected.push(device.pushkey.clone()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 342 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 343 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 344 | json_response(StatusCode::OK, notify_response(&rejected)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 345 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 346 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 347 | /// Resolves and sends one device's push. True when its pushkey should be rejected. |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 348 | async fn deliver( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 349 | state: &AppState, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 350 | push: &HostedPush, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 351 | notification: &GatewayNotification, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 352 | device: &GatewayDevice, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 353 | ) -> bool { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 354 | let (Some(room_id), Some(event_id)) = (¬ification.room_id, ¬ification.event_id) else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 355 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 356 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 357 | if device.app_id != HOSTED_APP_ID { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 358 | return true; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 359 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 360 | let Some(entry) = push.get(&device.pushkey) else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 361 | return true; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 362 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 363 | let user = match state.get_or_restore(&entry.sid).await { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 364 | Ok(user) => user, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 365 | Err(RestoreError::Unavailable(error)) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 366 | tracing::warn!(%error, "push for a session that would not restore"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 367 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 368 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 369 | Err( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 370 | RestoreError::NotFound | RestoreError::Stale(_) | RestoreError::SoftLoggedOut { .. }, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 371 | ) => return true, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 372 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 373 | let status = match timeout( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 374 | RESOLVE_TIMEOUT, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 375 | user.handle.resolve_push_notification(room_id, event_id), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 376 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 377 | .await |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 378 | { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 379 | Ok(Ok(status)) => status, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 380 | Ok(Err(error)) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 381 | tracing::warn!(%error, "resolving a push failed"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 382 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 383 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 384 | Err(_) => { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 385 | tracing::warn!("resolving a push timed out"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 386 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 387 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 388 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 389 | let PushNotificationStatus::Event(resolved) = status else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 390 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 391 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 392 | let unread = unread_total(&user.mirror.snapshot(), &resolved.room); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 393 | let notice = notice(&resolved, unread); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 394 | let urgency = if notice.mention || resolved.room.direct { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 395 | Urgency::High |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 396 | } else { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 397 | Urgency::Normal |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 398 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 399 | let outcome = push |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 400 | .sender |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 401 | .send( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 402 | &entry.subscription, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 403 | &Message { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 404 | payload: ¬ice.to_bytes(push.public_url.as_deref().unwrap_or_default()), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 405 | urgency, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 406 | topic: ¬ice.room_id, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 407 | }, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 408 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 409 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 410 | if matches!(outcome, Outcome::Failed(_) | Outcome::Retryable) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 411 | tracing::warn!(?outcome, "web push delivery failed"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 412 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 413 | if !outcome.drops_subscription() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 414 | return false; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 415 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 416 | // continuwuity 26.9 ignores a notify response's rejected pushkeys |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 417 | let _ = user |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 418 | .handle |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 419 | .remove_pusher(PusherKey { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 420 | app_id: HOSTED_APP_ID.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 421 | pushkey: device.pushkey.clone(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 422 | }) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 423 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 424 | true |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 425 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 426 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 427 | #[cfg(test)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 428 | mod tests { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 429 | use axum::{body::Body, http::Request}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 430 | use http_body_util::BodyExt; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 431 | use malice_push::{Allowlist, Vapid}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 432 | use serde_json::{Value, json}; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 433 | use tower::ServiceExt; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 434 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 435 | use super::*; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 436 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 437 | fn hosted(dir: &tempfile::TempDir) -> HostedPush { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 438 | let sender = Sender::new( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 439 | Vapid::load_or_create(&dir.path().join("vapid.pk8")).unwrap(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 440 | "mailto:test@example.org".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 441 | Allowlist::default(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 442 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 443 | .unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 444 | HostedPush::new( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 445 | dir.path(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 446 | Some("https://malice.example".to_owned()), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 447 | sender, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 448 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 449 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 450 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 451 | fn subscription(endpoint: &str) -> Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 452 | Subscription { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 453 | endpoint: endpoint.to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 454 | p256dh: "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 455 | auth: "BTBZMqHH6r4Tts7J_aSIgg".to_owned(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 456 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 457 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 458 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 459 | #[test] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 460 | fn registry_persists_and_moves_endpoints_between_sessions() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 461 | let dir = tempfile::tempdir().unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 462 | let push = hosted(&dir); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 463 | let (first, displaced) = push.claim("alpha", subscription("https://fcm.googleapis.com/a")); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 464 | assert!(displaced.is_none()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 465 | let (again, _) = push.claim("alpha", subscription("https://fcm.googleapis.com/a")); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 466 | assert_eq!(first, again); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 467 | let (moved, displaced) = push.claim("beta", subscription("https://fcm.googleapis.com/a")); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 468 | assert_ne!(moved, first); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 469 | assert_eq!(displaced, Some(("alpha".to_owned(), first.clone()))); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 470 | push.claim("beta", subscription("https://fcm.googleapis.com/b")); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 471 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 472 | let reloaded = hosted(&dir); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 473 | assert_eq!(reloaded.pusher_keys("beta").len(), 2); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 474 | assert!(reloaded.pusher_keys("alpha").is_empty()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 475 | assert_eq!( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 476 | reloaded.find("beta", "https://fcm.googleapis.com/a"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 477 | Some(moved) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 478 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 479 | reloaded.forget_session("beta"); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 480 | assert!(hosted(&dir).pusher_keys("beta").is_empty()); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 481 | #[cfg(unix)] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 482 | { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 483 | use std::os::unix::fs::PermissionsExt; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 484 | let mode = std::fs::metadata(dir.path().join("push.json")) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 485 | .unwrap() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 486 | .permissions() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 487 | .mode(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 488 | assert_eq!(mode & 0o777, 0o600); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 489 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 490 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 491 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 492 | async fn notify(state: &AppState, notification: Value) -> (StatusCode, Value) { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 493 | let response = crate::routes::router(state.clone(), None) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 494 | .oneshot( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 495 | Request::post(NOTIFY_PATH) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 496 | .body(Body::from( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 497 | json!({ "notification": notification }).to_string(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 498 | )) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 499 | .unwrap(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 500 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 501 | .await |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 502 | .unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 503 | let status = response.status(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 504 | let body = response.into_body().collect().await.unwrap().to_bytes(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 505 | (status, serde_json::from_slice(&body).unwrap_or(Value::Null)) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 506 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 507 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 508 | #[tokio::test] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 509 | async fn notify_drops_count_only_pushes_and_rejects_unknown_pushers() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 510 | let dir = tempfile::tempdir().unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 511 | let push = hosted(&dir); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 512 | let (orphan, _) = push.claim( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 513 | "nosuchsession", |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 514 | subscription("https://fcm.googleapis.com/o"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 515 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 516 | let state = AppState::new(dir.path().to_owned(), [7; 32], false, false).with_push(push); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 517 | let device = |app_id: &str, pushkey: &str| json!({"app_id": app_id, "pushkey": pushkey}); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 518 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 519 | let (status, body) = notify( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 520 | &state, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 521 | json!({"counts": {"unread": 1}, "devices": [device(HOSTED_APP_ID, &orphan)]}), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 522 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 523 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 524 | assert_eq!(status, StatusCode::OK); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 525 | assert_eq!(body, json!({"rejected": []})); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 526 | assert_eq!(state.push().unwrap().pusher_keys("nosuchsession").len(), 1); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 527 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 528 | let (status, body) = notify( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 529 | &state, |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 530 | json!({ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 531 | "event_id": "$e:x", |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 532 | "room_id": "!r:x", |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 533 | "devices": [ |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 534 | device(HOSTED_APP_ID, "unknown"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 535 | device("dev.malice.device", "foreign"), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 536 | device(HOSTED_APP_ID, &orphan), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 537 | ], |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 538 | }), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 539 | ) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 540 | .await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 541 | assert_eq!(status, StatusCode::OK); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 542 | assert_eq!(body, json!({"rejected": ["unknown", "foreign", orphan]})); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 543 | assert!( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 544 | state |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 545 | .push() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 546 | .unwrap() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 547 | .pusher_keys("nosuchsession") |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 548 | .is_empty() |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 549 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 550 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 551 | let (status, _) = notify(&state, json!("not a notification")).await; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 552 | assert_eq!(status, StatusCode::BAD_REQUEST); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 553 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 554 | |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 555 | #[tokio::test] |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 556 | async fn vapid_needs_a_public_url() { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 557 | let dir = tempfile::tempdir().unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 558 | let mut push = hosted(&dir); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 559 | let key = push.sender.vapid_public_key().to_owned(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 560 | let state = |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 561 | AppState::new(dir.path().to_owned(), [7; 32], false, false).with_push(hosted(&dir)); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 562 | let get = |state: &AppState| { |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 563 | crate::routes::router(state.clone(), None) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 564 | .oneshot(Request::get("/api/push/vapid").body(Body::empty()).unwrap()) |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 565 | }; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 566 | let response = get(&state).await.unwrap(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 567 | assert_eq!(response.status(), StatusCode::OK); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 568 | let body = response.into_body().collect().await.unwrap().to_bytes(); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 569 | assert_eq!( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 570 | serde_json::from_slice::<Value>(&body).unwrap()["public_key"], |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 571 | key |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 572 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 573 | push.public_url = None; |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 574 | let state = AppState::new(dir.path().to_owned(), [7; 32], false, false).with_push(push); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 575 | assert_eq!( |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 576 | get(&state).await.unwrap().status(), |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 577 | StatusCode::SERVICE_UNAVAILABLE |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 578 | ); |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 579 | } |
aa3dce5114d0 | atagen | 2026-10-26 06:09 UTC | 580 | } |