push/lib.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5#![allow(unknown_lints)]
6#![warn(rust_2018_idioms)]
7//! # Rust Push Component
8//!
9//! This component helps an application to manage [WebPush](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) subscriptions,
10//! acting as an intermediary between Mozilla's [autopush service](https://autopush.readthedocs.io/en/latest/)
11//! and platform native push infrastructure such as [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) or [Amazon Device Messaging](https://developer.amazon.com/docs/adm/overview.html).
12//!
13//! ## Background Concepts
14//!
15//! ### WebPush Subscriptions
16//!
17//! A WebPush client manages a number of *subscriptions*, each of which is used to deliver push
18//! notifications to a different part of the app. For example, a web browser might manage a separate
19//! subscription for each website that has registered a [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), and an application that includes Firefox Accounts would manage
20//! a dedicated subscription on which to receive account state updates.
21//!
22//! Each subscription is identified by a unique *channel id*, which is a randomly-generated identifier.
23//! It's the responsibility of the application to know how to map a channel id to an appropriate function
24//! in the app to receive push notifications. Subscriptions also have an associated *scope* which is something
25//! to do which service workers that your humble author doesn't really understand :-/.
26//!
27//! When a subscription is created for a channel id, we allocate *subscription info* consisting of:
28//!
29//! * An HTTP endpoint URL at which push messages can be submitted.
30//! * A cryptographic key and authentication secret with which push messages can be encrypted.
31//!
32//! This subscription info is distributed to other services that want to send push messages to
33//! the application.
34//!
35//! The HTTP endpoint is provided by Mozilla's [autopush service](https://autopush.readthedocs.io/en/latest/),
36//! and we use the [rust-ece](https://github.com/mozilla/rust-ece) to manage encryption with the cryptographic keys.
37//!
38//! Here's a helpful diagram of how the *subscription* flow works at a high level across the moving parts:
39//! 
40//!
41//! ### AutoPush Bridging
42//!
43//! Our target consumer platforms each have their own proprietary push-notification infrastructure,
44//! such as [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) for Android
45//! and the [Apple Push Notification Service](https://developer.apple.com/notifications/) for iOS.
46//! Mozilla's [autopush service](https://autopush.readthedocs.io/en/latest/) provides a bridge between
47//! these different mechanisms and the WebPush standard so that they can be used with a consistent
48//! interface.
49//!
50//! This component acts a client of the [Push Service Bridge HTTP Interface](https://autopush.readthedocs.io/en/latest/http.html#push-service-bridge-http-interface).
51//!
52//! We assume two things about the consuming application:
53//! * It has registered with the autopush service and received a unique `app_id` identifying this registration.
54//! * It has registered with whatever platform-specific notification infrastructure is appropriate, and is
55//! able to obtain a `token` corresponding to its native push notification state.
56//!
57//! On first use, this component will register itself as an *application instance* with the autopush service, providing the `app_id` and `token` and receiving a unique `uaid` ("user-agent id") to identify its
58//! connection to the server.
59//!
60//! As the application adds or removes subscriptions using the API of this component, it will:
61//! * Manage a local database of subscriptions and the corresponding cryptographic material.
62//! * Make corresponding HTTP API calls to update the state associated with its `uaid` on the autopush server.
63//!
64//! Periodically, the application should call a special `verify_connection` method to check whether
65//! the state on the autopush server matches the local state and take any corrective action if it
66//! differs.
67//!
68//! For local development and debugging, it is possible to run a local instance of the autopush
69//! bridge service; see [this google doc](https://docs.google.com/document/d/18L_g2hIj_1mncF978A_SHXN4udDQLut5P_ZHYZEwGP8) for details.
70//!
71//! ## API
72//!
73//! ## Initialization
74//!
75//! Calls are handled by the `PushManager`, which provides a handle for future calls.
76//!
77//! example:
78//! ```kotlin
79//!
80//! import mozilla.appservices.push.(PushManager, BridgeTypes)
81//!
82//! // The following are mock calls for fetching application level configuration options.
83//! // "SenderID" is the native OS push message application identifier. See Native
84//! // messaging documentation for details.
85//! val sender_id = SystemConfigurationOptions.get("SenderID")
86//!
87//! // The "bridge type" is the identifier for the native OS push message system.
88//! // (e.g. FCM for Google Firebase Cloud Messaging, ADM for Amazon Direct Messaging,
89//! // etc.)
90//! val bridge_type = BridgeTypes.FCM
91//!
92//! // The "registration_id" is the native OS push message user registration number.
93//! // Native push message registration usually happens at application start, and returns
94//! // an opaque user identifier string. See Native messaging documentation for details.
95//! val registration_id = NativeMessagingSystem.register(sender_id)
96//!
97//! val push_manager = PushManager(
98//! sender_id,
99//! bridge_type,
100//! registration_id
101//! )
102//!
103//! // It is strongly encouraged that the connection is verified at least once a day.
104//! // This will ensure that the server and UA have matching information regarding
105//! // subscriptions. This call usually returns quickly, but may take longer if the
106//! // UA has a large number of subscriptions and things have fallen out of sync.
107//!
108//! for change in push_manager.verify_connection() {
109//! // fetch the subscriber from storage using the change[0] and
110//! // notify them with a `pushsubscriptionchange` message containing the new
111//! // endpoint change[1]
112//! }
113//!
114//! ```
115//!
116//! ## New subscription
117//!
118//! Before messages can be delivered, a new subscription must be requested. The subscription info block contains all the information a remote subscription provider service will need to encrypt and transmit a message to this user agent.
119//!
120//! example:
121//! ```kotlin
122//!
123//! // Each new request must have a unique "channel" identifier. This channel helps
124//! // later identify recipients and aid in routing. A ChannelID is a UUID4 value.
125//! // the "scope" is the ServiceWorkerRegistration scope. This will be used
126//! // later for push notification management.
127//! val channelID = GUID.randomUUID()
128//!
129//! val subscription_info = push_manager.subscribe(channelID, endpoint_scope)
130//!
131//! // the published subscription info has the following JSON format:
132//! // {"endpoint": subscription_info.endpoint,
133//! // "keys": {
134//! // "auth": subscription_info.keys.auth,
135//! // "p256dh": subscription_info.keys.p256dh
136//! // }}
137//! ```
138//!
139//! ## End a subscription
140//!
141//! A user may decide to no longer receive a given subscription. To remove a given subscription, pass the associated channelID
142//!
143//! ```kotlin
144//! push_manager.unsubscribe(channelID) // Terminate a single subscription
145//! ```
146//!
147//! If the user wishes to terminate all subscriptions, send and empty string for channelID
148//!
149//! ```kotlin
150//! push_manager.unsubscribe("") // Terminate all subscriptions for a user
151//! ```
152//!
153//! If this function returns `false` the subsequent `verify_connection` may result in new channel endpoints.
154//!
155//! ## Decrypt an incoming subscription message
156//!
157//! An incoming subscription body will contain a number of metadata elements along with the body of the message. Due to platform differences, how that metadata is provided may //! vary, however the most common form is that the messages "payload" looks like.
158//!
159//! ```javascript
160//! {"chid": "...", // ChannelID
161//! "con": "...", // Encoding form
162//! "enc": "...", // Optional encryption header
163//! "crypto-key": "...", // Optional crypto key header
164//! "body": "...", // Encrypted message body
165//! }
166//! ```
167//! These fields may be included as a sub-hash, or may be intermingled with other data fields. If you have doubts or concerns, please contact the Application Services team guidance
168//!
169//! Based on the above payload, an example call might look like:
170//!
171//! ```kotlin
172//! val result = manager.decrypt(
173//! channelID = payload["chid"].toString(),
174//! body = payload["body"].toString(),
175//! encoding = payload["con"].toString(),
176//! salt = payload.getOrElse("enc", "").toString(),
177//! dh = payload.getOrElse("dh", "").toString()
178//! )
179//! // result returns a byte array. You may need to convert to a string
180//! return result.toString(Charset.forName("UTF-8"))
181//!```
182
183uniffi::include_scaffolding!("push");
184// All implementation detail lives in the `internal` module
185mod internal;
186use std::{collections::HashMap, sync::Mutex};
187mod error;
188
189use error_support::handle_error;
190pub use internal::config::{BridgeType, Protocol as PushHttpProtocol, PushConfiguration};
191use internal::crypto::Crypto;
192use internal::{communications::ConnectHttp, push_manager::DecryptResponse};
193
194pub use error::{debug, ApiResult, PushApiError, PushError};
195use internal::storage::Store;
196
197/// Object representing the PushManager used to manage subscriptions
198///
199/// The `PushManager` object is the main interface provided by this crate
200/// it allow consumers to manage push subscriptions. It exposes methods that
201/// interact with the [`autopush server`](https://autopush.readthedocs.io/en/latest/)
202/// and persists state representing subscriptions.
203pub struct PushManager {
204 // We serialize all access on a mutex for thread safety
205 // TODO: this can improved by making the locking more granular
206 // and moving the mutex down to ensure `internal::PushManager`
207 // is Sync + Send
208 internal: Mutex<internal::PushManager<ConnectHttp, Crypto, Store>>,
209}
210
211impl PushManager {
212 /// Creates a new [`PushManager`] object, not subscribed to any
213 /// channels
214 ///
215 /// # Arguments
216 /// - `config`: [`PushConfiguration`] the configuration for this instance of PushManager
217 ///
218 /// # Errors
219 /// Returns an error in the following cases:
220 /// - PushManager is unable to open the `database_path` given
221 /// - PushManager is unable to establish a connection to the autopush server
222 #[handle_error(PushError)]
223 pub fn new(config: PushConfiguration) -> ApiResult<Self> {
224 debug!(
225 "PushManager server_host: {}, http_protocol: {}",
226 config.server_host, config.http_protocol
227 );
228 Ok(Self {
229 internal: Mutex::new(internal::PushManager::new(config)?),
230 })
231 }
232
233 /// Subscribes to a new channel and gets the Subscription Info block
234 ///
235 /// # Arguments
236 /// - `channel_id` - Channel ID (UUID4) for new subscription, either pre-generated or "" and one will be created.
237 /// - `scope` - Site scope string (defaults to "" for no site scope string).
238 /// - `server_key` - optional VAPID public key to "lock" subscriptions (defaults to "" for no key)
239 ///
240 /// # Returns
241 /// A Subscription response that includes the following:
242 /// - A URL that can be used to deliver push messages
243 /// - A cryptographic key that can be used to encrypt messages
244 /// that would then be decrypted using the [`PushManager::decrypt`] function
245 ///
246 /// # Errors
247 /// Returns an error in the following cases:
248 /// - PushManager was unable to access its persisted storage
249 /// - An error occurred sending a subscription request to the autopush server
250 /// - An error occurred generating or deserializing the cryptographic keys
251 #[handle_error(PushError)]
252 pub fn subscribe(
253 &self,
254 scope: &str,
255 server_key: &Option<String>,
256 ) -> ApiResult<SubscriptionResponse> {
257 self.internal
258 .lock()
259 .unwrap()
260 .subscribe(scope, server_key.as_deref())
261 }
262
263 /// Retrieves an existing push subscription
264 ///
265 /// # Arguments
266 /// - `scope` - Site scope string
267 ///
268 /// # Returns
269 /// A Subscription response that includes the following:
270 /// - A URL that can be used to deliver push messages
271 /// - A cryptographic key that can be used to encrypt messages
272 /// that would then be decrypted using the [`PushManager::decrypt`] function
273 ///
274 /// # Errors
275 /// Returns an error in the following cases:
276 /// - PushManager was unable to access its persisted storage
277 /// - An error occurred generating or deserializing the cryptographic keys
278 #[handle_error(PushError)]
279 pub fn get_subscription(&self, scope: &str) -> ApiResult<Option<SubscriptionResponse>> {
280 self.internal.lock().unwrap().get_subscription(scope)
281 }
282
283 /// Unsubscribe from given channelID, ending that subscription for the user.
284 ///
285 /// # Arguments
286 /// - `channel_id` - Channel ID (UUID) for subscription to remove
287 ///
288 /// # Returns
289 /// Returns a boolean. Boolean is False if the subscription was already
290 /// terminated in the past.
291 ///
292 /// # Errors
293 /// Returns an error in the following cases:
294 /// - The PushManager does not contain a valid UAID
295 /// - An error occurred sending an unsubscribe request to the autopush server
296 /// - An error occurred accessing the PushManager's persisted storage
297 #[handle_error(PushError)]
298 pub fn unsubscribe(&self, channel_id: &str) -> ApiResult<bool> {
299 self.internal.lock().unwrap().unsubscribe(channel_id)
300 }
301
302 /// Unsubscribe all channels for the user
303 ///
304 /// # Errors
305 /// Returns an error in the following cases:
306 /// - The PushManager does not contain a valid UAID
307 /// - An error occurred sending an unsubscribe request to the autopush server
308 /// - An error occurred accessing the PushManager's persisted storage
309 #[handle_error(PushError)]
310 pub fn unsubscribe_all(&self) -> ApiResult<()> {
311 self.internal.lock().unwrap().unsubscribe_all()
312 }
313
314 /// Updates the Native OS push registration ID.
315 ///
316 /// # Arguments:
317 /// - `new_token` - the new Native OS push registration ID
318 /// # Errors
319 /// Return an error in the following cases:
320 /// - The PushManager does not contain a valid UAID
321 /// - An error occurred sending an update request to the autopush server
322 /// - An error occurred accessing the PushManager's persisted storage
323 #[handle_error(PushError)]
324 pub fn update(&self, new_token: &str) -> ApiResult<()> {
325 self.internal.lock().unwrap().update(new_token)
326 }
327
328 /// Verifies the connection state
329 ///
330 /// **NOTE**: This does not resubscribe to any channels
331 /// it only returns the list of channels that the client should
332 /// re-subscribe to.
333 ///
334 /// # Arguments
335 /// - `force_verify`: Force verification and ignore the rate limiter
336 ///
337 /// # Returns
338 /// Returns a list of [`PushSubscriptionChanged`]
339 /// indicating the channels the consumer the client should re-subscribe
340 /// to. If the list is empty, the client's connection was verified
341 /// successfully, and the client does not need to resubscribe
342 ///
343 /// # Errors
344 /// Return an error in the following cases:
345 /// - The PushManager does not contain a valid UAID
346 /// - An error occurred sending an channel list retrieval request to the autopush server
347 /// - An error occurred accessing the PushManager's persisted storage
348 #[handle_error(PushError)]
349 pub fn verify_connection(&self, force_verify: bool) -> ApiResult<Vec<PushSubscriptionChanged>> {
350 self.internal
351 .lock()
352 .unwrap()
353 .verify_connection(force_verify)
354 }
355
356 /// Decrypts a raw push message.
357 ///
358 /// This accepts the content of a Push Message (from websocket or via Native Push systems).
359 /// # Arguments:
360 /// - `channel_id` - the ChannelID (included in the envelope of the message)
361 /// - `body` - The encrypted body of the message
362 /// - `encoding` - The Content Encoding "enc" field of the message (defaults to "aes128gcm")
363 /// - `salt` - The "salt" field (if present in the raw message, defaults to "")
364 /// - `dh` - The "dh" field (if present in the raw message, defaults to "")
365 ///
366 /// # Returns
367 /// Decrypted message body as a signed byte array
368 /// they byte array is signed to allow consumers (Kotlin only at the time of this documentation)
369 /// to work easily with the message. (They can directly call `.toByteArray` on it)
370 ///
371 /// # Errors
372 /// Returns an error in the following cases:
373 /// - The PushManager does not contain a valid UAID
374 /// - There are no records associated with the UAID the [`PushManager`] contains
375 /// - An error occurred while decrypting the message
376 /// - An error occurred accessing the PushManager's persisted storage
377 #[handle_error(PushError)]
378 pub fn decrypt(&self, payload: HashMap<String, String>) -> ApiResult<DecryptResponse> {
379 self.internal.lock().unwrap().decrypt(payload)
380 }
381}
382
383/// Key Information that can be used to encrypt payloads. These are encoded as base64
384/// so will need to be decoded before they can actually be used as keys.
385#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
386pub struct KeyInfo {
387 pub auth: String,
388 pub p256dh: String,
389}
390/// Subscription Information, the endpoint to send push messages to and
391/// the key information that can be used to encrypt payloads
392#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
393pub struct SubscriptionInfo {
394 pub endpoint: String,
395 pub keys: KeyInfo,
396}
397
398/// The subscription response object returned from [`PushManager::subscribe`]
399#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
400pub struct SubscriptionResponse {
401 pub channel_id: String,
402 pub subscription_info: SubscriptionInfo,
403}
404
405/// An dictionary describing the push subscription that changed, the caller
406/// will receive a list of [`PushSubscriptionChanged`] when calling
407/// [`PushManager::verify_connection`], one entry for each channel that the
408/// caller should resubscribe to
409#[derive(Debug, Clone)]
410pub struct PushSubscriptionChanged {
411 pub channel_id: String,
412 pub scope: String,
413}