simplepush_rs/
lib.rs

1//! # SimplePush: A client for sending notifications via the simplepush.io API.
2//!
3//! See <https://simplepush.io> for more details.
4extern crate crypto;
5extern crate rand;
6
7use base64::{engine::general_purpose::URL_SAFE, Engine as _};
8use crypto::buffer::{BufferResult, ReadBuffer, WriteBuffer};
9use crypto::digest::Digest;
10use crypto::sha1::Sha1;
11use crypto::{aes, blockmodes, buffer, symmetriccipher};
12use rand::Rng;
13use serde::Serialize;
14
15// Default encryption salt, you should really provide your own
16static DEFAULT_SALT: &str = "A9F361C70BCB6182";
17
18// API endpoint
19static API_URL: &str = "https://api.simplepush.io";
20
21/// SimplePush API payload
22#[derive(Serialize)]
23struct Payload {
24    key: String,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    title: Option<String>,
27    msg: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    event: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    actions: Option<Vec<String>>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    encrypted: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    iv: Option<String>,
36}
37
38/// SimplePush message data
39pub struct Message {
40    /// Your simplepush.io key
41    pub key: String,
42    /// Title of the message
43    pub title: Option<String>,
44    /// Message body
45    pub message: String,
46    /// The event the message should be associated with
47    pub event: Option<String>,
48    /// Actions for feedback message, only simple actions are supported at this time
49    pub actions: Option<Vec<String>>,
50    /// If true, the message will be sent with end-to-end encrypted using the provided password & salt
51    pub encrypt: bool,
52    /// Password if the message is to be encrypted
53    pub password: Option<String>,
54    /// If set, this salt will be used for encryption, otherwise the `DEFAULT_SALT` will be used
55    pub salt: Option<String>,
56}
57
58impl Message {
59    /// Create a new notification message
60    pub fn new(
61        key: &str,
62        title: Option<&str>,
63        message: &str,
64        event: Option<&str>,
65        actions: Option<Vec<&str>>,
66    ) -> Self {
67        Message {
68            key: String::from(key),
69            title: Self::stringify(title),
70            message: String::from(message),
71            event: Self::stringify(event),
72            actions: Self::stringify_vec(actions),
73            encrypt: false,
74            password: None,
75            salt: None,
76        }
77    }
78
79    /// Create a new notification message with encryption
80    pub fn new_with_encryption(
81        key: &str,
82        title: Option<&str>,
83        message: &str,
84        event: Option<&str>,
85        actions: Option<Vec<&str>>,
86        password: &str,
87        salt: Option<&str>,
88    ) -> Self {
89        Message {
90            key: String::from(key),
91            title: Self::stringify(title),
92            message: String::from(message),
93            event: Self::stringify(event),
94            actions: Self::stringify_vec(actions),
95            encrypt: true,
96            password: Some(String::from(password)),
97            salt: Self::stringify(salt.or(Some(DEFAULT_SALT))),
98        }
99    }
100
101    fn stringify(s: Option<&str>) -> Option<String> {
102        s.map(String::from)
103    }
104
105    fn stringify_vec(s: Option<Vec<&str>>) -> Option<Vec<String>> {
106        s.map(|t| t.iter().map(|s| String::from(*s)).collect())
107    }
108}
109
110/// A client for the simplepush.io API
111///
112/// See: <https://simplepush.io/api> for more details
113///
114pub struct SimplePush;
115
116impl SimplePush {
117    fn encrypt(
118        key: &[u8],
119        iv: &[u8],
120        buf: Vec<u8>,
121    ) -> Result<String, symmetriccipher::SymmetricCipherError> {
122        let mut encryptor =
123            aes::cbc_encryptor(aes::KeySize::KeySize128, key, iv, blockmodes::PkcsPadding);
124        let mut final_result = Vec::<u8>::new();
125        let mut read_buffer = buffer::RefReadBuffer::new(&buf);
126        let mut buffer = [0; 4096];
127        let mut write_buffer = buffer::RefWriteBuffer::new(&mut buffer);
128
129        loop {
130            let result = encryptor.encrypt(&mut read_buffer, &mut write_buffer, true)?;
131            final_result.extend(
132                write_buffer
133                    .take_read_buffer()
134                    .take_remaining()
135                    .iter()
136                    .copied(),
137            );
138
139            match result {
140                BufferResult::BufferUnderflow => break,
141                BufferResult::BufferOverflow => {}
142            }
143        }
144
145        Ok(URL_SAFE.encode(final_result))
146    }
147
148    fn process_message(message: &Message) -> Payload {
149        let message_iv: Option<String>;
150        let encrypted: Option<bool>;
151        let msg: String;
152        let title: Option<String>;
153        let actions: Option<Vec<String>>;
154
155        if message.encrypt {
156            let salt = message.salt.to_owned().expect("salt was None");
157            let password = message.password.to_owned().expect("password was None");
158            let mut hasher = Sha1::new();
159            hasher.input_str(format!("{}{}", password, salt).as_str());
160
161            let mut key = [0u8; 40];
162            hasher.result(&mut key);
163
164            let mut iv = [0u8; 16];
165            let mut rng = rand::rngs::OsRng;
166            rng.fill(&mut iv[..]);
167
168            msg = SimplePush::encrypt(&key[0..16], &iv, message.message.to_owned().into_bytes())
169                .expect("encryption failed!");
170
171            title = message.title.to_owned().map(|t| {
172                SimplePush::encrypt(&key[0..16], &iv, t.into_bytes()).expect("encryption failed")
173            });
174
175            actions = message.actions.to_owned().map(|t| {
176                t.iter()
177                    .map(|s| {
178                        SimplePush::encrypt(&key[0..16], &iv, s.clone().into_bytes())
179                            .expect("encryption failed")
180                    })
181                    .collect()
182            });
183
184            message_iv = Some(SimplePush::hexify(iv.to_vec()).to_ascii_uppercase());
185            encrypted = Some(true);
186        } else {
187            msg = message.message.to_owned();
188            title = message.title.to_owned();
189            actions = message.actions.to_owned();
190            encrypted = None;
191            message_iv = None;
192        }
193
194        Payload {
195            key: message.key.to_owned(),
196            title,
197            msg,
198            event: message.event.to_owned(),
199            actions,
200            encrypted: encrypted.map(|v| v.to_string()),
201            iv: message_iv,
202        }
203    }
204
205    fn hexify(bytes: Vec<u8>) -> String {
206        let strs: Vec<String> = bytes.iter().map(|b| format!("{:02X}", b)).collect();
207        strs.join("")
208    }
209
210    fn validate(message: &Message) -> Result<(), String> {
211        if message.key.is_empty() {
212            return Err(String::from("key is required"));
213        }
214
215        if message.title.is_none() && message.message.is_empty() {
216            return Err(String::from("a message or title is required"));
217        }
218
219        if message.encrypt && message.password.is_none()
220            || message.password.as_ref().is_some_and(|p| p.is_empty())
221        {
222            return Err(String::from("password is required for encryption"));
223        }
224
225        Ok(())
226    }
227
228    /// Sends a notification message through the simplepush.io API
229    ///
230    /// Sending a notification
231    ///
232    /// ```no_run
233    ///    use simplepush_rs::{Message, SimplePush};
234    ///    let _ = SimplePush::send(Message::new(
235    ///         "SIMPLE_PUSH_KEY",
236    ///         Some("title"),
237    ///         "test message",
238    ///         Some("alert"),
239    ///         Some(vec!["yes", "no"]),
240    ///     ));
241    ///```
242    ///
243    /// Sending a notification with encryption
244    ///
245    /// ```no_run
246    ///    use simplepush_rs::{Message, SimplePush};
247    ///    let _ = SimplePush::send(Message::new_with_encryption(
248    ///         "SIMPLE_PUSH_KEY",
249    ///         Some("title"),
250    ///         "test message",
251    ///         Some("alert"),
252    ///         Some(vec!["yes", "no"]),
253    ///         "ENCRYPTION_KEY",
254    ///         Some("SALT"),
255    ///     ));
256    ///```
257    ///
258    pub fn send(message: Message) -> Result<(), String> {
259        SimplePush::validate(&message)?;
260
261        let client = reqwest::blocking::Client::new();
262        let response = client
263            .post(format!("{}/send", API_URL))
264            .json(&SimplePush::process_message(&message))
265            .send();
266        match response {
267            Ok(_) => {
268                Ok(())
269            }
270            Err(e) => {
271                Err(e.to_string())
272            }
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_empty_key() {
283        let result = SimplePush::send(Message::new("", Some("title"), "message", None, None));
284        assert!(result.is_err_and(|e| e == *"key is required"));
285    }
286
287    #[test]
288    fn test_empty_message() {
289        let result = SimplePush::send(Message::new("key", None, "", None, None));
290        assert!(result.is_err_and(|e| e == *"a message or title is required"));
291    }
292
293    #[test]
294    fn test_empty_password_with_encryption() {
295        let result = SimplePush::send(Message::new_with_encryption(
296            "key", None, "message", None, None, "", None,
297        ));
298        assert!(result.is_err_and(|e| e == *"password is required for encryption"));
299    }
300}