Skip to main content

hi_apns/
lib.rs

1#![deny(warnings)]
2
3mod types;
4pub use self::types::*;
5
6mod error;
7use self::error::*;
8
9use std::cell::RefCell;
10use std::path::{Path, PathBuf};
11
12use curl::easy::{Easy2, Handler, HttpVersion, List, WriteError};
13use failure::Error;
14use reqwest::header;
15use uuid::Uuid;
16
17pub struct Client {
18    cli: reqwest::Client,
19    production: bool,
20}
21
22impl Client {
23    pub fn new(certs: &[u8], password: &str) -> Result<Self, Error> {
24        let cert = reqwest::Identity::from_pkcs12_der(certs, password).unwrap();
25
26        let cli = reqwest::Client::builder().identity(cert).build().unwrap();
27
28        Ok(Self {
29            cli,
30            production: false,
31        })
32    }
33
34    pub async fn send(&self, notification: Notification<'_>) -> Result<Response, SendError> {
35        let url = self.build_url(&notification.device_token);
36
37        let mut headers = header::HeaderMap::new();
38
39        headers.append(
40            "apns-topic",
41            header::HeaderValue::from_str(&notification.topic).unwrap(),
42        );
43
44        if let Some(id) = notification.id {
45            headers.append(
46                "apns-id",
47                header::HeaderValue::from_str(&id.to_string()).unwrap(),
48            );
49        }
50
51        if let Some(expire) = notification.expiration {
52            headers.append(
53                "apns-expiration                ",
54                header::HeaderValue::from_str(&expire.to_string()).unwrap(),
55            );
56        }
57
58        if let Some(collapse_id) = notification.collapse_id {
59            headers.append(
60                "apns-collapse-id                ",
61                header::HeaderValue::from_str(collapse_id.as_str()).unwrap(),
62            );
63        }
64
65        if let Some(priority) = notification.priority {
66            headers.append(
67                "apns-priority",
68                header::HeaderValue::from_str(&priority.to_uint().to_string()).unwrap(),
69            );
70        }
71
72        if let Some(push_type) = notification.apns_push_type {
73            headers.append(
74                "apns-push-type",
75                header::HeaderValue::from_str(push_type.as_str()).unwrap(),
76            );
77        }
78
79        let request = ApnsRequest {
80            aps: notification.payload,
81        };
82
83        let mut resp = self
84            .cli
85            .post(url)
86            .headers(headers)
87            .json(&request)
88            .send()
89            .await
90            .unwrap();
91
92        let status_code = resp.status();
93
94        let headers = std::mem::take(resp.headers_mut());
95
96        let apns_id = headers.get("apns-id").unwrap().clone();
97
98        let apns_id = apns_id.to_str().unwrap();
99
100        let mut resp = resp.json::<Response>().await.unwrap();
101
102        resp.apns_id = apns_id.to_string();
103        resp.status_code = status_code;
104
105        Ok(resp)
106    }
107
108    fn build_url(&self, device_token: &str) -> String {
109        let root = if self.production {
110            APN_URL_PRODUCTION
111        } else {
112            APN_URL_DEV
113        };
114        format!("{}/3/device/{}", root, device_token)
115    }
116}
117
118/// Writer used by curl.
119struct Collector(Vec<u8>);
120
121impl Handler for Collector {
122    fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
123        self.0.extend_from_slice(data);
124        Ok(data.len())
125    }
126}
127
128#[derive(Clone, Debug)]
129pub struct ProviderCertificate {
130    pub p12_path: PathBuf,
131    pub passphrase: Option<String>,
132}
133
134#[derive(Clone, Debug)]
135pub enum Auth {
136    ProviderCertificate(ProviderCertificate),
137}
138
139impl Auth {
140    fn as_cert(&self) -> &ProviderCertificate {
141        match self {
142            &Auth::ProviderCertificate(ref c) => c,
143        }
144    }
145}
146
147pub struct ApnsSync {
148    production: bool,
149    verbose: bool,
150    delivery_disabled: bool,
151    auth: Auth,
152    easy: RefCell<Easy2<Collector>>,
153}
154
155impl ApnsSync {
156    pub fn new(auth: Auth) -> Result<Self, Error> {
157        let mut easy = Easy2::new(Collector(Vec::new()));
158
159        easy.http_version(HttpVersion::V2)?;
160        // easy.connect_only(true)?;
161        // easy.url(APN_URL_PRODUCTION)?;
162
163        // Configure curl for client certificate.
164        {
165            let cert = auth.as_cert();
166
167            easy.ssl_cert(&cert.p12_path)?;
168            if let Some(ref pw) = cert.passphrase.as_ref() {
169                easy.key_password(&pw)?;
170            }
171        }
172
173        let apns = ApnsSync {
174            production: true,
175            verbose: false,
176            delivery_disabled: false,
177            auth,
178            easy: RefCell::new(easy),
179        };
180        Ok(apns)
181    }
182
183    pub fn with_certificate<P: AsRef<Path>>(
184        path: P,
185        passphrase: Option<String>,
186    ) -> Result<ApnsSync, Error> {
187        Self::new(Auth::ProviderCertificate(ProviderCertificate {
188            p12_path: path.as_ref().to_path_buf(),
189            passphrase,
190        }))
191    }
192
193    /// Enable/disable verbose debug logging to stderr.
194    pub fn set_verbose(&mut self, verbose: bool) {
195        self.verbose = verbose;
196    }
197
198    /// Set API endpoint to use (production or development sandbox).
199    pub fn set_production(&mut self, production: bool) {
200        self.production = production;
201    }
202
203    /// *ATTENTION*: This completely disables actual communication with the
204    /// APNS api.
205    ///
206    /// No connection will be established.
207    ///
208    /// Useful for integration tests in a larger application when nothing should
209    /// actually be sent.
210    pub fn disable_delivery_for_testing(&mut self) {
211        self.delivery_disabled = true;
212    }
213
214    /// Build the url for a device token.
215    fn build_url(&self, device_token: &str) -> String {
216        let root = if self.production {
217            APN_URL_PRODUCTION
218        } else {
219            APN_URL_DEV
220        };
221        format!("{}/3/device/{}", root, device_token)
222    }
223
224    /// Send a notification.
225    /// Returns the UUID (either the configured one, or the one returned by the
226    /// api).
227    pub fn send(&self, notification: Notification) -> Result<Uuid, SendError> {
228        let n = notification;
229
230        // Just always generate a uuid client side for simplicity.
231        let id = n.id.unwrap_or(Uuid::new_v4());
232
233        if self.delivery_disabled {
234            return Ok(id);
235        }
236
237        let url = self.build_url(&n.device_token);
238
239        // Add headers.
240
241        let mut headers = List::new();
242
243        // NOTE: if an option which requires a header is not set,
244        // the header is still added, but with an empty value,
245        // which instructs curl to drop the header.
246        // Otherwhise, headers from previous runs would stick around.
247
248        headers.append(&format!("apns-id:{}", id.to_string(),))?;
249        headers.append(&format!(
250            "apns-expiration:{}",
251            n.expiration
252                .map(|x| x.to_string())
253                .unwrap_or("".to_string())
254        ))?;
255        headers.append(&format!(
256            "apns-priority:{}",
257            n.priority
258                .map(|x| x.to_uint().to_string())
259                .unwrap_or("".to_string())
260        ))?;
261        headers.append(&format!("apns-topic:{}", n.topic))?;
262        headers.append(&format!(
263            "apns-collapse-id:{}",
264            n.collapse_id
265                .map(|x| x.as_str().to_string())
266                .unwrap_or("".to_string())
267        ))?;
268
269        let request = ApnsRequest { aps: n.payload };
270        let raw_request = serde_json::to_vec(&request)?;
271
272        let mut easy = self.easy.borrow_mut();
273
274        match &self.auth {
275            _ => {}
276        }
277
278        easy.verbose(self.verbose)?;
279        easy.http_headers(headers)?;
280        easy.post(true)?;
281        easy.post_fields_copy(&raw_request)?;
282        easy.url(&url)?;
283        easy.perform()?;
284
285        let status = easy.response_code()?;
286        if status != 200 {
287            // Request failed.
288            // Read json response with the error.
289            let response_data = easy.get_ref();
290            let reason = ErrorResponse::parse_payload(&response_data.0);
291            Err(ApiError { status, reason }.into())
292        } else {
293            Ok(id)
294        }
295    }
296}
297
298#[cfg(test)]
299mod test {
300    use super::*;
301    use std::env::var;
302
303    #[test]
304    fn test_cert() {
305        let cert_path = var("APNS_CERT_PATH").unwrap();
306        let cert_pw = Some(var("APNS_CERT_PW").unwrap());
307        let topic = var("APNS_TOPIC").unwrap();
308        let token = var("APNS_DEVICE_TOKEN").unwrap();
309
310        let mut apns = ApnsSync::with_certificate(cert_path, cert_pw).unwrap();
311        apns.set_verbose(true);
312        let n = NotificationBuilder::new(&topic, &token)
313            .title("title")
314            .build();
315        apns.send(n).unwrap();
316    }
317}