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//! ![A Sequence diagram showing how the different parts of push interact](https://mozilla.github.io/application-services/book/diagrams/Push-Component-Subscription-flow.png "Sequence diagram")
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}