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(¬ification.device_token);
36
37 let mut headers = header::HeaderMap::new();
38
39 headers.append(
40 "apns-topic",
41 header::HeaderValue::from_str(¬ification.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
118struct 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 {
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 pub fn set_verbose(&mut self, verbose: bool) {
195 self.verbose = verbose;
196 }
197
198 pub fn set_production(&mut self, production: bool) {
200 self.production = production;
201 }
202
203 pub fn disable_delivery_for_testing(&mut self) {
211 self.delivery_disabled = true;
212 }
213
214 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 pub fn send(&self, notification: Notification) -> Result<Uuid, SendError> {
228 let n = notification;
229
230 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 let mut headers = List::new();
242
243 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 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}