From 23339a525060f2973ae66e1b99559167d7cce916 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 30 May 2016 11:56:21 +0200 Subject: [PATCH 01/21] Refactoring to have persistent connecton and error handling Use Solicit instead of Hyper to get an async connection, that is kept alive between the requests. Handling errors given by Apple. --- src/lib.rs | 2 +- src/provider.rs | 129 +++++++++++++++++++++++++++--------------------- src/response.rs | 25 +++++----- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4261c13c..dd438cd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod payload; pub mod device_token; pub mod response; -pub use provider::Provider; +pub use provider::{Provider, AsyncResponse}; pub use notification::{Notification, NotificationOptions}; pub use payload::{Payload, APS, APSAlert, APSLocalizedAlert}; pub use device_token::DeviceToken; diff --git a/src/provider.rs b/src/provider.rs index bfd4cf5f..7cbbd0db 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,24 +1,17 @@ use notification::*; use response::*; -// Time and serialization -use time::{Tm, Timespec, at}; -use rustc_serialize::json::*; -// Standard lib +use openssl::ssl::{SslContext, SslMethod, SSL_VERIFY_NONE}; +use openssl::x509::X509; +use openssl::crypto::pkey::PKey; +use time::{Tm, Timespec, at, precise_time_ns}; +use rustc_serialize::json::{Json, Object}; use std::str; use std::fmt::Display; use std::result::Result; use std::thread; use std::fs::File; use std::io::Read; -// Solicit -use solicit::http::client::tls::TlsConnector; -use solicit::http::Header; -use solicit::client::Client; -use solicit::http::ALPN_PROTOCOLS; -// Open SSL -use openssl::ssl::*; -use openssl::x509::X509; -use openssl::crypto::pkey::PKey; +use std::sync::mpsc::Receiver; static DEVELOPMENT: &'static str = "api.development.push.apple.com"; static PRODUCTION: &'static str = "api.push.apple.com"; @@ -50,14 +43,14 @@ impl Provider { let connector = TlsConnector::with_context(host, &ctx); let client = Client::with_connector(connector).unwrap(); - Provider { - client: client, - } + Provider { client: client, } + } + + pub fn push(&self, notification: Notification) -> AsyncResponse { + AsyncResponse::new(self.request(notification), precise_time_ns()) } - pub fn push(&self, notification: Notification, handler: F) - where F: Send + 'static + FnOnce(Result) - { + fn request(&self, notification: Notification) -> Option>> { let path = format!("/3/device/{}", notification.device_token).into_bytes(); let body = notification.payload.to_string().into_bytes(); let mut headers = Vec::new(); @@ -75,48 +68,72 @@ impl Provider { headers.push(Provider::create_header("apns-topic", apns_topic)); } - let this = self.client.clone(); - thread::spawn(move || { - let resp = this.post(&path, headers.as_slice(), body).unwrap(); - let res = match resp.recv() { - Ok(http_response) => { - let status = Provider::fetch_status(http_response.status_code().ok()); - let apns_id = Provider::fetch_apns_id(http_response.headers); - let json = str::from_utf8(&http_response.body).ok().and_then(|v| Json::from_str(v).ok()); - let object = json.as_ref().and_then(|v| v.as_object()); - let reason = Provider::fetch_reason(object); - let timestamp = Provider::fetch_timestamp(object); - if status == APNSStatus::Success { - Ok(Response { - status: status, - reason: reason, - timestamp: timestamp, - apns_id: apns_id, - }) - } else { - Err(Response { + self.client.post(&path, headers.as_slice(), body) + } + + fn create_header<'a, T: Display>(key: &'a str, value: T) -> Header<'a, 'a> { + Header::new(key.as_bytes(), format!("{}", value).into_bytes()) + } + +} + +pub type ResponseChannel = Receiver>; + +pub struct AsyncResponse { + rx: Option, + pub requested_at: u64, +} + +impl AsyncResponse { + pub fn new(rx: Option, requested_at: u64) -> AsyncResponse { + AsyncResponse { rx: rx, requested_at: requested_at } + } + + pub fn recv_timeout(&self, timeout: Duration) -> Result { + if let Some(ref rx) = self.rx { + let now = Instant::now(); + + while now.elapsed() < timeout { + match rx.try_recv() { + Ok(http_response) => { + let status = AsyncResponse::fetch_status(http_response.status_code().ok()); + let apns_id = AsyncResponse::fetch_apns_id(http_response.headers); + let json = str::from_utf8(&http_response.body).ok().and_then(|v| Json::from_str(v).ok()); + let object = json.as_ref().and_then(|v| v.as_object()); + let reason = AsyncResponse::fetch_reason(object); + let timestamp = AsyncResponse::fetch_timestamp(object); + + let response = Response { status: status, reason: reason, timestamp: timestamp, apns_id: apns_id, - }) - } - }, - Err(_) => { - Err(Response { - status: APNSStatus::Timeout, - reason: None, - timestamp: None, - apns_id: None, - }) - }, - }; - handler(res); - }); - } + }; - fn create_header<'a, T: Display>(key: &'a str, value: T) -> Header<'a, 'a> { - Header::new(key.as_bytes(), format!("{}", value).into_bytes()) + if response.status == APNSStatus::Success { + return Ok(response); + } else { + return Err(response); + } + }, + _ => thread::park_timeout(Duration::from_millis(10)), + } + } + + Err(Response { + status: APNSStatus::Timeout, + reason: None, + timestamp: None, + apns_id: None, + }) + } else { + Err(Response { + status: APNSStatus::MissingChannel, + reason: None, + timestamp: None, + apns_id: None, + }) + } } fn fetch_status(code: Option) -> APNSStatus { diff --git a/src/response.rs b/src/response.rs index f42b29f3..382f648b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -96,18 +96,18 @@ impl Error for APNSError { /// The HTTP status code. #[derive(Debug, PartialEq)] pub enum APNSStatus { - Success = 200, // Success - BadRequest = 400, // Bad request - Forbidden = 403, // There was an error with the certificate. - MethodNotAllowed = 405, // The request used a bad method value. Only POST requests are support - Unregistered = 410, // The device token is no longer active for the topic. - PayloadTooLarge = 413, // The notification payload was too large. - TooManyRequests = 429, // The server received too many requests for the same device token. - InternalServerError = 500, // Internal server error - ServiceUnavailable = 503, // The server is shutting down and unavailable. - - Timeout = 998, // The request timed out - Unknown = 999, // Unknown error + Success = 200, // Success + BadRequest = 400, // Bad request + Forbidden = 403, // There was an error with the certificate. + MethodNotAllowed = 405, // The request used a bad method value. Only POST requests are support + Unregistered = 410, // The device token is no longer active for the topic. + PayloadTooLarge = 413, // The notification payload was too large. + TooManyRequests = 429, // The server received too many requests for the same device token. + InternalServerError = 500, // Internal server error + ServiceUnavailable = 503, // The server is shutting down and unavailable. + MissingChannel = 997, // The response channel died before getting a response + Timeout = 998, // The request timed out + Unknown = 999, // Unknown error } /// The response of request. @@ -130,3 +130,4 @@ pub struct Response { /// a later timestamp with your provider. pub timestamp: Option, } + From 33a96c201871c2b33dc211f3dd1660850592825f Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 6 Jun 2016 19:08:39 +0200 Subject: [PATCH 02/21] Make APNSError and APNSStatus public --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index dd438cd5..d7b2f3d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,4 +14,4 @@ pub use provider::{Provider, AsyncResponse}; pub use notification::{Notification, NotificationOptions}; pub use payload::{Payload, APS, APSAlert, APSLocalizedAlert}; pub use device_token::DeviceToken; -pub use response::{Response, APNSStatus, APNSError}; +pub use response::{Response, APNSError, APNSStatus}; From ffd056855019bd3e9b873345a48a8d1989845970 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 9 Jun 2016 15:43:42 +0200 Subject: [PATCH 03/21] Make some of the payload values optional, add category field --- src/payload.rs | 79 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/payload.rs b/src/payload.rs index 9ade2aa4..2631d948 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -9,9 +9,42 @@ pub struct Payload<'a> { pub aps: APS<'a>, } -impl<'a> Payload<'a> { - pub fn new(alert: APSAlert, badge: Option, sound: S) -> Payload<'a> - where S: Into> +pub struct APS { + pub alert: Option, + + // The number to display as the badge of the app icon. + pub badge: Option, + + // The name of a sound file in the app bundle or in the Library/Sounds folder of + // the app’s data container. + pub sound: Option, + + // Provide this key with a value of 1 to indicate that new content is available. + pub content_available: Option, + + // Provide this key with a string value that represents the identifier property. + pub category: Option, +} + +pub enum APSAlert { + Plain(String), + Localized(APSLocalizedAlert), +} + +pub struct APSLocalizedAlert { + pub title: String, + pub body: String, + pub title_loc_key: Option, + pub title_loc_args: Option>, + pub action_loc_key: Option, + pub loc_key: String, + pub loc_args: Vec, + pub launch_image: Option, +} + +impl Payload { + pub fn new(alert: APSAlert, badge: u32, sound: S, category: Option) -> Payload + where S: Into { Payload { aps: APS { @@ -19,7 +52,7 @@ impl<'a> Payload<'a> { badge: badge, sound: Some(sound.into()), content_available: None, - category: None, + category: category, }, } } @@ -155,25 +188,35 @@ pub struct APSLocalizedAlert { impl ToJson for APSLocalizedAlert { fn to_json(&self) -> Json { let mut d = BTreeMap::new(); + d.insert("title".to_string(), self.title.to_json()); d.insert("body".to_string(), self.body.to_json()); - d.insert("title-loc-key".to_string(), match self.title_loc_key { - Some(ref k) => k.to_json(), - None => Json::Null, - }); - d.insert("title-loc-args".to_string(), match self.title_loc_args { - Some(ref a) => a.to_json(), - None => Json::Null, - }); - d.insert("action-loc-key".to_string(), match self.action_loc_key { - Some(ref k) => k.to_json(), - None => Json::Null, - }); + + if let Some(ref title_loc_key) = self.title_loc_key { + d.insert("title-loc-key".to_string(), title_loc_key.to_json()); + } else { + d.insert("title-loc-key".to_string(), Json::Null); + } + + if let Some(ref title_loc_args) = self.title_loc_args { + d.insert("title-loc-args".to_string(), title_loc_args.to_json()); + } else { + d.insert("title-loc-args".to_string(), Json::Null); + } + + if let Some(ref action_loc_key) = self.action_loc_key { + d.insert("action-loc-key".to_string(), action_loc_key.to_json()); + } else { + d.insert("action-loc-key".to_string(), Json::Null); + } + d.insert("loc-key".to_string(), self.loc_key.to_json()); d.insert("loc-args".to_string(), self.loc_args.to_json()); - if let Some(ref i) = self.launch_image { - d.insert("launch-image".to_string(), i.to_json()); + + if let Some(ref launch_image) = self.launch_image { + d.insert("launch-image".to_string(), launch_image.to_json()); } + Json::Object(d) } } From 361076adcf7ee6c2ac8f9f2f8f43857b7f547515 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Fri, 1 Jul 2016 18:00:21 +0200 Subject: [PATCH 04/21] Make loc-key and loc-args optional --- src/payload.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/payload.rs b/src/payload.rs index 2631d948..29cdb266 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -37,8 +37,8 @@ pub struct APSLocalizedAlert { pub title_loc_key: Option, pub title_loc_args: Option>, pub action_loc_key: Option, - pub loc_key: String, - pub loc_args: Vec, + pub loc_key: Option, + pub loc_args: Option>, pub launch_image: Option, } @@ -210,8 +210,17 @@ impl ToJson for APSLocalizedAlert { d.insert("action-loc-key".to_string(), Json::Null); } - d.insert("loc-key".to_string(), self.loc_key.to_json()); - d.insert("loc-args".to_string(), self.loc_args.to_json()); + if let Some(ref loc_key) = self.loc_key { + d.insert("loc-key".to_string(), loc_key.to_json()); + } else { + d.insert("loc-key".to_string(), Json::Null); + } + + if let Some(ref loc_args) = self.loc_args { + d.insert("loc-args".to_string(), loc_args.to_json()); + } else { + d.insert("loc-args".to_string(), Json::Null); + } if let Some(ref launch_image) = self.launch_image { d.insert("launch-image".to_string(), launch_image.to_json()); From 1ff4743ae7a9100d5ea07eef34a8026b40cca7ac Mon Sep 17 00:00:00 2001 From: Grzegorz Kocur Date: Tue, 19 Jul 2016 12:07:21 +0200 Subject: [PATCH 05/21] Added http2 negotiation using ALPN extension --- Cargo.toml | 5 +++++ src/provider.rs | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ccc89d03..3de3c04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,8 @@ features = ["tlsv1_2", "npn", "alpn"] git = "https://github.com/aagahi/solicit" default-features = true features = ["tls"] + +[dependencies.openssl] +default-features = true +features = ["alpn"] +version = "~0.7" diff --git a/src/provider.rs b/src/provider.rs index 7cbbd0db..c5cac804 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -36,9 +36,8 @@ impl Provider { ctx.set_cipher_list("DEFAULT").unwrap(); ctx.set_certificate(&x509).unwrap(); ctx.set_private_key(&pkey).unwrap(); - ctx.set_options(SSL_OP_NO_COMPRESSION); - ctx.set_alpn_protocols(ALPN_PROTOCOLS); - ctx.set_npn_protocols(ALPN_PROTOCOLS); + ctx.set_verify(SSL_VERIFY_NONE, None); + ctx.set_alpn_protocols(&[b"h2"]); let connector = TlsConnector::with_context(host, &ctx); let client = Client::with_connector(connector).unwrap(); From 65118c413af8e939d509e24ecba768b1244bd108 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Wed, 17 Aug 2016 16:48:42 +0200 Subject: [PATCH 06/21] Add possibility to include custom data in the payload --- src/payload.rs | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/payload.rs b/src/payload.rs index 29cdb266..ac23c1c1 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -2,11 +2,9 @@ use std::borrow::Cow; use std::collections::BTreeMap; use rustc_serialize::json::{Json, ToJson}; -/// Each remote notification includes a payload. -/// The payload contains information about how the system should alert the user as well -/// as any custom data you provide. -pub struct Payload<'a> { - pub aps: APS<'a>, +pub struct Payload { + pub aps: APS, + pub custom: Option, } pub struct APS { @@ -42,8 +40,13 @@ pub struct APSLocalizedAlert { pub launch_image: Option, } +pub struct CustomData { + pub key: String, + pub body: Json, +} + impl Payload { - pub fn new(alert: APSAlert, badge: u32, sound: S, category: Option) -> Payload + pub fn new(alert: APSAlert, badge: u32, sound: S, category: Option, custom_data: Option) -> Payload where S: Into { Payload { @@ -54,24 +57,11 @@ impl Payload { content_available: None, category: category, }, + custom: custom_data, } } - pub fn new_action_notification(alert: APSAlert, badge: Option, sound: S, category: S) -> Payload<'a> - where S: Into> - { - Payload { - aps: APS { - alert: Some(alert), - badge: badge, - sound: Some(sound.into()), - content_available: None, - category: Some(category.into()), - }, - } - } - - pub fn new_silent_notification() -> Payload<'a> { + pub fn new_silent_notification(custom_data: Option) -> Payload { Payload { aps: APS { alert: None, @@ -80,6 +70,7 @@ impl Payload { content_available: Some(1), category: None, }, + custom: custom_data, } } @@ -96,6 +87,11 @@ impl<'a> ToJson for Payload<'a> { fn to_json(&self) -> Json { let mut d = BTreeMap::new(); d.insert("aps".to_string(), self.aps.to_json()); + + if let Some(ref custom) = self.custom { + d.insert(custom.key.to_string(), custom.body.clone()); + } + Json::Object(d) } } From 9821dd038ce513fcba04f62ffbc97aba3cbccc3c Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Wed, 17 Aug 2016 17:18:30 +0200 Subject: [PATCH 07/21] Give public access to CustomData struct --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d7b2f3d1..ab874e0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ pub mod response; pub use provider::{Provider, AsyncResponse}; pub use notification::{Notification, NotificationOptions}; -pub use payload::{Payload, APS, APSAlert, APSLocalizedAlert}; +pub use payload::{APS, APSAlert, APSLocalizedAlert, Payload, CustomData}; pub use device_token::DeviceToken; pub use response::{Response, APNSError, APNSStatus}; From ec113ecebb91aa7af42af6b843347d9009acfa8b Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 19 Sep 2016 14:05:19 +0200 Subject: [PATCH 08/21] Update solicit --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3de3c04c..68b7f689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ version = "~0.7" features = ["tlsv1_2", "npn", "alpn"] [dependencies.solicit] -git = "https://github.com/aagahi/solicit" +git = "https://github.com/pimeys/solicit" default-features = true features = ["tls"] [dependencies.openssl] default-features = true features = ["alpn"] -version = "~0.7" +version = "~0.6" From dd5fb981d5a268246892b74c4d41647b1c74921d Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 19 Sep 2016 14:21:46 +0200 Subject: [PATCH 09/21] Go back to pimeys solicit --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 68b7f689..a6718009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,4 @@ features = ["tls"] [dependencies.openssl] default-features = true features = ["alpn"] -version = "~0.6" +version = "~0.7" From 750b4c7a48626e410009a4aa60f47225d646d6b4 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 26 Jan 2017 11:05:40 +0100 Subject: [PATCH 10/21] Test --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 39c7a593..a2008d9c 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,5 @@ provider.push(notification, |result| { ## License [MIT License](https://github.com/tkabit/apns2/blob/master/LICENSE) +||||||| merged common ancestors +======= From 61ac8ae22052b78e0075435249f2c55ec1268714 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 26 Jan 2017 11:07:47 +0100 Subject: [PATCH 11/21] Revert "Test" This reverts commit 496296e40c53ab67d5ea0ea55f43a572c8cafc28. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index a2008d9c..d62b2bfd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Build Status](https://travis-ci.org/polyitan/apns2.svg?branch=master)](https://travis-ci.org/polyitan/apns2) # apns2 HTTP/2 Apple Push Notification Service for Rust @@ -47,5 +46,3 @@ provider.push(notification, |result| { ## License [MIT License](https://github.com/tkabit/apns2/blob/master/LICENSE) -||||||| merged common ancestors -======= From 1e71c5178dec682a558cb94a98cc384d768808ce Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 30 Jan 2017 19:10:47 +0100 Subject: [PATCH 12/21] Token-based authentication --- .gitignore | 5 + Cargo.toml | 11 +- examples/certificate_client.rs | 55 +++++++ examples/token_client.rs | 63 ++++++++ src/apns_token.rs | 96 ++++++++++++ src/client/certificate.rs | 60 ++++++++ src/client/error.rs | 31 ++++ src/client/headers.rs | 30 ++++ src/client/mod.rs | 8 + src/client/response.rs | 273 +++++++++++++++++++++++++++++++++ src/client/token.rs | 44 ++++++ src/lib.rs | 10 +- src/provider.rs | 201 ------------------------ src/response.rs | 133 ---------------- 14 files changed, 675 insertions(+), 345 deletions(-) create mode 100644 examples/certificate_client.rs create mode 100644 examples/token_client.rs create mode 100644 src/apns_token.rs create mode 100644 src/client/certificate.rs create mode 100644 src/client/error.rs create mode 100644 src/client/headers.rs create mode 100644 src/client/mod.rs create mode 100644 src/client/response.rs create mode 100644 src/client/token.rs delete mode 100644 src/provider.rs diff --git a/.gitignore b/.gitignore index 2f06a53c..c069ef8c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock + +# Certificate files +*.der +*.pem +*.key diff --git a/Cargo.toml b/Cargo.toml index a6718009..1c0c22fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,15 +15,18 @@ rustc-serialize = "~0.3" time = "~0.1" [dependencies.openssl] +default-features = true version = "~0.7" features = ["tlsv1_2", "npn", "alpn"] +[dev-dependencies] +argparse = "*" + [dependencies.solicit] git = "https://github.com/pimeys/solicit" default-features = true features = ["tls"] -[dependencies.openssl] -default-features = true -features = ["alpn"] -version = "~0.7" +[dependencies.btls] +features = ["gnutls-ecdsa"] +git = "https://gitlab.com/ilari_l/btls.git" diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs new file mode 100644 index 00000000..aa33036c --- /dev/null +++ b/examples/certificate_client.rs @@ -0,0 +1,55 @@ +extern crate apns2; +extern crate argparse; + +use argparse::{ArgumentParser, Store, StoreTrue}; +use apns2::client::CertificateClient; +use apns2::device_token::DeviceToken; +use apns2::payload::{Payload, APSAlert}; +use apns2::notification::{Notification, NotificationOptions}; +use std::fs::File; +use std::time::Duration; + +// An example client connectiong to APNs with a certificate and key +fn main() { + let mut certificate_pem_file = String::new(); + let mut key_pem_file = String::new(); + let mut device_token = String::new(); + let mut message = String::from("Ch-check it out!"); + let mut sandbox = false; + + { + let mut ap = ArgumentParser::new(); + ap.set_description("APNs certificate-based push"); + ap.refer(&mut certificate_pem_file).add_option(&["-c", "--certificate"], Store, "Certificate PEM file location"); + ap.refer(&mut key_pem_file).add_option(&["-k", "--key"], Store, "Private key PEM file location"); + ap.refer(&mut device_token).add_option(&["-d", "--device_token"], Store, "APNs device token"); + ap.refer(&mut message).add_option(&["-m", "--message"], Store, "Notification message"); + ap.refer(&mut sandbox).add_option(&["-s", "--sandbox"], StoreTrue, "Use the development APNs servers"); + ap.parse_args_or_exit(); + } + + // Read the private key and certificate from the disk + let mut cert_file = File::open(certificate_pem_file).unwrap(); + let mut key_file = File::open(key_pem_file).unwrap(); + + // Create a new client to APNs, giving the system CA certs + let client = CertificateClient::new(sandbox, &mut cert_file, &mut key_file).unwrap(); + + // Create a device token struct from given token + let device_token = DeviceToken::new(device_token); + + // APNs payload + let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); + + let options = NotificationOptions { + ..Default::default() + }; + + // Fire the request, return value is a mpsc rx channel + let request = client.push(Notification::new(payload, device_token, options)); + + // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout + let response = request.recv_timeout(Duration::from_millis(2000)); + + println!("{:?}", response); +} diff --git a/examples/token_client.rs b/examples/token_client.rs new file mode 100644 index 00000000..50576091 --- /dev/null +++ b/examples/token_client.rs @@ -0,0 +1,63 @@ +extern crate apns2; +extern crate argparse; + +use argparse::{ArgumentParser, Store, StoreTrue}; +use apns2::client::TokenClient; +use apns2::apns_token::ApnsToken; +use apns2::device_token::DeviceToken; +use apns2::payload::{Payload, APSAlert}; +use apns2::notification::{Notification, NotificationOptions}; +use std::fs::File; +use std::time::Duration; + +// An example client connectiong to APNs with a JWT token +fn main() { + let mut der_file_location = String::new(); + let mut team_id = String::new(); + let mut key_id = String::new(); + let mut device_token = String::new(); + let mut message = String::from("Ch-check it out!"); + let mut ca_certs = String::from("/etc/ssl/cert.pem"); + let mut sandbox = false; + + { + let mut ap = ArgumentParser::new(); + ap.set_description("APNs token-based push"); + ap.refer(&mut der_file_location).add_option(&["-e", "--der"], Store, "Private key file in DER format"); + ap.refer(&mut team_id).add_option(&["-t", "--team_id"], Store, "APNs team ID"); + ap.refer(&mut key_id).add_option(&["-k", "--key_id"], Store, "APNs key ID"); + ap.refer(&mut device_token).add_option(&["-d", "--device_token"], Store, "APNs device token"); + ap.refer(&mut message).add_option(&["-m", "--message"], Store, "Notification message"); + ap.refer(&mut sandbox).add_option(&["-s", "--sandbox"], StoreTrue, "Use the development APNs servers"); + ap.refer(&mut ca_certs).add_option(&["-c", "--ca_certs"], Store, "The system CA certificates PEM file"); + ap.parse_args_or_exit(); + } + + // Read the private key from disk + let mut der_file = File::open(der_file_location).unwrap(); + + // Create a new token struct with the private key, team id and key id + // The token is valid for an hour and needs to be renewed after that + let apns_token = ApnsToken::new(&mut der_file, team_id.as_ref(), key_id.as_ref()).unwrap(); + + // Create a new client to APNs, giving the system CA certs + let client = TokenClient::new(sandbox, &ca_certs).unwrap(); + + // Create a device token struct from given token + let device_token = DeviceToken::new(device_token.as_ref()); + + // APNs payload + let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); + + let options = NotificationOptions { + ..Default::default() + }; + + // Fire the request, return value is a mpsc rx channel + let request = client.push(Notification::new(payload, device_token, options), &apns_token); + + // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout + let response = request.recv_timeout(Duration::from_millis(2000)); + + println!("{:?}", response); +} diff --git a/src/apns_token.rs b/src/apns_token.rs new file mode 100644 index 00000000..49a8691c --- /dev/null +++ b/src/apns_token.rs @@ -0,0 +1,96 @@ +use btls::server_keys::LocalKeyPair; +use btls::jose_jws::{sign_jws, JsonNode}; +use std::convert::From; +use btls::error::KeyReadError; +use time::get_time; +use std::collections::BTreeMap; +use std::io::Read; + +const SIG_ECDSA_SHA256: u16 = 0x0403; + +pub struct ApnsToken { + signature: Option, + issued_at: Option, + key_id: String, + team_id: String, + secret: LocalKeyPair, +} + +#[derive(Debug)] +pub enum ApnsTokenError { + SignError, + KeyParseError(String), + KeyOpenError(String), + KeyReadError(String), + KeyGenerationError, + KeyError, +} + + +impl From for ApnsTokenError { + fn from(e: KeyReadError) -> ApnsTokenError { + match e { + KeyReadError::ParseError(e, _) => ApnsTokenError::KeyParseError(e), + KeyReadError::OpenError(e, _) => ApnsTokenError::KeyOpenError(e), + KeyReadError::ReadError(e, _) => ApnsTokenError::KeyReadError(e), + KeyReadError::KeyGenerationFailed => ApnsTokenError::KeyGenerationError, + _ => ApnsTokenError::KeyError, + } + } +} + +impl ApnsToken { + pub fn new, R: Read>(pk_der: &mut R, key_id: S, team_id: S) -> Result { + let mut token = ApnsToken { + signature: None, + issued_at: None, + key_id: key_id.into(), + team_id: team_id.into(), + secret: LocalKeyPair::new(pk_der, "apns_private_key")?, + }; + + match token.renew() { + Err(e) => Err(e), + _ => Ok(token), + } + } + + pub fn signature(&self) -> String { + match self.signature { + Some(ref sig) => sig.to_string(), + None => "".to_string() + } + } + + pub fn renew(&mut self) -> Result<(), ApnsTokenError> { + let issued_at = get_time().sec; + + let mut headers: BTreeMap = BTreeMap::new(); + headers.insert("alg".to_string(), JsonNode::String("ES256".to_string())); + headers.insert("kid".to_string(), JsonNode::String(self.key_id.to_string())); + + let mut payload: BTreeMap = BTreeMap::new(); + payload.insert("iss".to_string(), JsonNode::String(self.team_id.to_string())); + payload.insert("iat".to_string(), JsonNode::Number(issued_at)); + + let jwt_headers = JsonNode::Dictionary(headers); + let jwt_payload = JsonNode::Dictionary(payload).serialize(); + + match sign_jws(&jwt_headers, jwt_payload.as_bytes(), &self.secret, SIG_ECDSA_SHA256).read() { + Ok(Ok(token)) => { + self.signature = Some(token.to_compact()); + self.issued_at = Some(issued_at); + Ok(()) + } + _ => Err(ApnsTokenError::SignError) + } + } + + pub fn is_expired(&self) -> bool { + if let Some(issued_at) = self.issued_at { + (get_time().sec - issued_at) > 3600 + } else { + true + } + } +} diff --git a/src/client/certificate.rs b/src/client/certificate.rs new file mode 100644 index 00000000..82937e0a --- /dev/null +++ b/src/client/certificate.rs @@ -0,0 +1,60 @@ +use solicit::http::client::tls::TlsConnector; +use solicit::client::{Client}; +use openssl::ssl::{SslContext, SslMethod, SSL_VERIFY_NONE}; +use openssl::x509::X509; +use openssl::crypto::pkey::PKey; +use time::precise_time_ns; +use std::str; +use std::result::Result; +use std::io::Read; + +use client::response::ProviderResponse; +use client::headers::default_headers; +use client::error::ProviderError; +use notification::Notification; + +static DEVELOPMENT: &'static str = "api.development.push.apple.com"; +static PRODUCTION: &'static str = "api.push.apple.com"; + +pub struct CertificateClient { + pub client: Client, +} + +impl CertificateClient { + pub fn new<'a, R: Read>(sandbox: bool, certificate: &mut R, private_key: &mut R) + -> Result> { + let host = if sandbox { DEVELOPMENT } else { PRODUCTION }; + let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap(); + + let x509 = X509::from_pem(certificate)?; + let pkey = PKey::private_key_from_pem(private_key)?; + + ctx.set_cipher_list("DEFAULT")?; + ctx.set_certificate(&x509)?; + ctx.set_private_key(&pkey)?; + ctx.set_verify(SSL_VERIFY_NONE, None); + ctx.set_alpn_protocols(&[b"h2"]); + + let connector = TlsConnector::with_context(host, &ctx); + + let client = match Client::with_connector(connector) { + Ok(client) => client, + Err(_) => return Err(ProviderError::ClientConnectError("Couldn't connect to APNs service")) + }; + + Ok(CertificateClient { + client: client, + }) + } + + pub fn push<'a>(&self, notification: Notification) -> ProviderResponse { + let path = format!("/3/device/{}", notification.device_token).into_bytes(); + let body = notification.payload.to_string().into_bytes(); + let headers = default_headers(¬ification); + let request = self.client.post(&path, headers.as_slice(), body); + + ProviderResponse::new(request, precise_time_ns()) + } + +} + diff --git a/src/client/error.rs b/src/client/error.rs new file mode 100644 index 00000000..02cfb064 --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,31 @@ +use openssl::ssl::error::SslError; +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub enum ProviderError<'a> { + ClientConnectError(&'a str), + SslError(&'a str) +} + +impl<'a> From for ProviderError<'a> { + fn from(_: SslError) -> ProviderError<'a> { + ProviderError::SslError("Error generationg SSL context") + } +} + +impl<'a> Error for ProviderError<'a> { + fn description(&self) -> &str { + "Error in APNs connection" + } + + fn cause(&self) -> Option<&Error> { + None + } +} + +impl<'a> fmt::Display for ProviderError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description()) + } +} diff --git a/src/client/headers.rs b/src/client/headers.rs new file mode 100644 index 00000000..c77cf760 --- /dev/null +++ b/src/client/headers.rs @@ -0,0 +1,30 @@ +use solicit::http::Header; +use std::fmt::Display; +use notification::Notification; + +pub fn default_headers<'a, 'b>(notification: &'a Notification) -> Vec> { + let mut headers = Vec::new(); + headers.push(create_header("content_length", notification.payload.len())); + + if let Some(apns_id) = notification.options.apns_id { + headers.push(create_header("apns-id", apns_id)); + } + + if let Some(apns_expiration) = notification.options.apns_expiration { + headers.push(create_header("apns-expiration", apns_expiration)); + } + + if let Some(apns_priority) = notification.options.apns_priority { + headers.push(create_header("apns-priority", apns_priority)); + } + + if let Some(apns_topic) = notification.options.apns_topic { + headers.push(create_header("apns-topic", apns_topic)); + } + + headers +} + +pub fn create_header<'a, T: Display>(key: &'a str, value: T) -> Header<'a, 'a> { + Header::new(key.as_bytes(), format!("{}", value).into_bytes()) +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 00000000..7ad7da3c --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,8 @@ +mod certificate; +mod response; +mod headers; +mod error; +mod token; + +pub use self::token::TokenClient; +pub use self::certificate::CertificateClient; diff --git a/src/client/response.rs b/src/client/response.rs new file mode 100644 index 00000000..eed8ceb8 --- /dev/null +++ b/src/client/response.rs @@ -0,0 +1,273 @@ +use rustc_serialize::json::{Json, Object}; +use std::str; +use time::{Tm, Timespec, at}; +use solicit::http::{Header, Response as HttpResponse}; +use std::time::{Duration, Instant}; +use std::error::Error; +use std::fmt; +use std::thread; +use std::sync::mpsc::Receiver; + +use self::APNSError::*; + +// The APNS reasons. +pub enum APNSError { + PayloadEmpty, + PayloadTooLarge, + BadTopic, + TopicDisallowed, + BadMessageId, + BadExpirationDate, + BadPriority, + MissingDeviceToken, + BadDeviceToken, + DeviceTokenNotForTopic, + Unregistered, + DuplicateHeaders, + BadCertificateEnvironment, + BadCertificate, + Forbidden, + InvalidProviderToken, + MissingProviderToken, + ExpiredProviderToken, + BadPath, + MethodNotAllowed, + TooManyRequests, + IdleTimeout, + Shutdown, + InternalServerError, + ServiceUnavailable, + MissingTopic, +} + +impl fmt::Debug for APNSError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + + +impl fmt::Display for APNSError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl Error for APNSError { + fn description(&self) -> &str { + match *self { + PayloadEmpty => "The message payload was empty", + PayloadTooLarge => { + "The message payload was too large. \ + The maximum payload size is 4096 bytes" + } + BadTopic => "The apns-topic was invalid", + TopicDisallowed => "Pushing to this topic is not allowed", + BadMessageId => "The apns-id value is bad", + BadExpirationDate => "The apns-expiration value is bad", + BadPriority => "The apns-priority value is bad", + MissingDeviceToken => { + "The device token is not specified in the request :path. Verify that the :path \ + header contains the device token" + } + BadDeviceToken => { + "The specified device token was bad. Verify that the request contains a valid \ + token and that the token matches the environment" + } + DeviceTokenNotForTopic => "The device token does not match the specified topic", + Unregistered => "The device token is inactive for the specified topic", + DuplicateHeaders => "One or more headers were repeated", + BadCertificateEnvironment => "The client certificate was for the wrong environment", + BadCertificate => "The certificate was bad", + Forbidden => "The specified action is not allowed", + InvalidProviderToken => "The provider token is not valid or the token signature could not be verified", + MissingProviderToken => "No provider certificate was used to connect to APNs and Authorization header was missing or no provider token was specified", + ExpiredProviderToken => "The provider token is stale and a new token should be generated", + BadPath => "The request contained a bad :path value", + MethodNotAllowed => "The specified :method was not POST", + TooManyRequests => "Too many requests were made consecutively to the same device token", + IdleTimeout => "Idle time out", + Shutdown => "The server is shutting down", + InternalServerError => "An internal server error occurred", + ServiceUnavailable => "The service is unavailable", + MissingTopic => { + "The apns-topic header of the request was not specified and was required. The \ + apns-topic header is mandatory when the client is connected using a certificate \ + that supports multiple topics" + } + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + _ => None, + } + } +} + +// The HTTP status code. +#[derive(Debug, PartialEq)] +pub enum APNSStatus { + Success = 200, // Success + BadRequest = 400, // Bad request + Forbidden = 403, // There was an error with the certificate. + MethodNotAllowed = 405, // The request used a bad method value. Only POST requests are support + Unregistered = 410, // The device token is no longer active for the topic. + PayloadTooLarge = 413, // The notification payload was too large. + TooManyRequests = 429, // The server received too many requests for the same device token. + InternalServerError = 500, // Internal server error + ServiceUnavailable = 503, // The server is shutting down and unavailable. + MissingChannel = 997, // The response channel died before getting a response + Timeout = 998, // The request timed out + Unknown = 999, // Unknown error +} + +#[derive(Debug)] +pub struct Response { + // Status codes for a response + pub status: APNSStatus, + + // The apns-id value from the request. + // If no value was included in the request, + // the server creates a new UUID and returns it in this header. + pub apns_id: Option, + + // The error indicating the reason for the failure. + pub reason: Option, + + // If the value in the :status header is 410,the value of this key is the last time + // at which APNs confirmed that the device token was no longer valid for the topic. + // Stop pushing notifications until the device registers a token with + // a later timestamp with your provider. + pub timestamp: Option, +} + + +pub type ResponseChannel = Receiver>; + +pub struct ProviderResponse { + rx: Option, + pub requested_at: u64, +} + +impl ProviderResponse { + pub fn new(rx: Option, requested_at: u64) -> ProviderResponse { + ProviderResponse { rx: rx, requested_at: requested_at } + } + + pub fn recv_timeout(&self, timeout: Duration) -> Result { + if let Some(ref rx) = self.rx { + let now = Instant::now(); + + while now.elapsed() < timeout { + match rx.try_recv() { + Ok(http_response) => { + let status = Self::fetch_status(http_response.status_code().ok()); + let apns_id = Self::fetch_apns_id(http_response.headers); + let json = str::from_utf8(&http_response.body).ok().and_then(|v| Json::from_str(v).ok()); + let object = json.as_ref().and_then(|v| v.as_object()); + let reason = Self::fetch_reason(object); + let timestamp = Self::fetch_timestamp(object); + + let response = Response { + status: status, + reason: reason, + timestamp: timestamp, + apns_id: apns_id, + }; + + if response.status == APNSStatus::Success { + return Ok(response); + } else { + return Err(response); + } + }, + _ => thread::park_timeout(Duration::from_millis(10)), + } + } + + Err(Response { + status: APNSStatus::Timeout, + reason: None, + timestamp: None, + apns_id: None, + }) + } else { + Err(Response { + status: APNSStatus::MissingChannel, + reason: None, + timestamp: None, + apns_id: None, + }) + } + } + + fn fetch_status(code: Option) -> APNSStatus { + match code { + Some(200) => APNSStatus::Success, + Some(400) => APNSStatus::BadRequest, + Some(403) => APNSStatus::Forbidden, + Some(405) => APNSStatus::MethodNotAllowed, + Some(410) => APNSStatus::Unregistered, + Some(413) => APNSStatus::PayloadTooLarge, + Some(429) => APNSStatus::TooManyRequests, + Some(500) => APNSStatus::InternalServerError, + Some(503) => APNSStatus::ServiceUnavailable, + _ => APNSStatus::Unknown, + } + } + + fn fetch_apns_id(headers: Vec
) -> Option { + headers.iter().find(|&header| { + match str::from_utf8(header.name()).unwrap() { + "apns-id" => true, + _ => false, + } + }).map(|header| { + String::from_utf8(header.value().to_vec()).unwrap() + }) + } + + fn fetch_reason(js_object: Option<&Object>) -> Option { + let raw_reason = js_object.and_then(|v| v.get("reason")).and_then(|v| v.as_string()); + + match raw_reason { + Some("PayloadEmpty") => Some(APNSError::PayloadEmpty), + Some("PayloadTooLarge") => Some(APNSError::PayloadTooLarge), + Some("BadTopic") => Some(APNSError::BadTopic), + Some("TopicDisallowed") => Some(APNSError::TopicDisallowed), + Some("BadMessageId") => Some(APNSError::BadMessageId), + Some("BadExpirationDate") => Some(APNSError::BadExpirationDate), + Some("BadPriority") => Some(APNSError::BadPriority), + Some("MissingDeviceToken") => Some(APNSError::MissingDeviceToken), + Some("BadDeviceToken") => Some(APNSError::BadDeviceToken), + Some("DeviceTokenNotForTopic") => Some(APNSError::DeviceTokenNotForTopic), + Some("Unresgistered") => Some(APNSError::Unregistered), + Some("DuplicateHeaders") => Some(APNSError::DuplicateHeaders), + Some("BadCertificateEnvironment") => Some(APNSError::BadCertificateEnvironment), + Some("BadCertificate") => Some(APNSError::BadCertificate), + Some("Forbidden") => Some(APNSError::Forbidden), + Some("InvalidProviderToken") => Some(APNSError::InvalidProviderToken), + Some("MissingProviderToken") => Some(APNSError::MissingProviderToken), + Some("ExpiredProviderToken") => Some(APNSError::ExpiredProviderToken), + Some("BadPath") => Some(APNSError::BadPath), + Some("MethodNotAllowed") => Some(APNSError::MethodNotAllowed), + Some("TooManyRequests") => Some(APNSError::TooManyRequests), + Some("IdleTimeout") => Some(APNSError::IdleTimeout), + Some("Shutdown") => Some(APNSError::Shutdown), + Some("InternalServerError") => Some(APNSError::InternalServerError), + Some("ServiceUnavailable") => Some(APNSError::ServiceUnavailable), + Some("MissingTopic") => Some(APNSError::MissingTopic), + _ => None, + } + } + + fn fetch_timestamp(js_object: Option<&Object>) -> Option { + let raw_ts = js_object.and_then(|v| v.get("timestamp")).and_then(|v| v.as_i64()); + + match raw_ts { + Some(ts) => Some(at(Timespec::new(ts, 0))), + None => None, + } + } +} diff --git a/src/client/token.rs b/src/client/token.rs new file mode 100644 index 00000000..8853aba7 --- /dev/null +++ b/src/client/token.rs @@ -0,0 +1,44 @@ +use solicit::http::client::tls::TlsConnector; +use solicit::client::{Client}; +use time::precise_time_ns; +use std::result::Result; + +use client::response::ProviderResponse; +use client::headers::{default_headers, create_header}; +use client::error::ProviderError; +use apns_token::ApnsToken; +use notification::Notification; + +static DEVELOPMENT: &'static str = "api.development.push.apple.com"; +static PRODUCTION: &'static str = "api.push.apple.com"; + +pub struct TokenClient { + pub client: Client, +} + +impl TokenClient { + pub fn new<'a>(sandbox: bool, certificates: &str) -> Result> { + let host = if sandbox { DEVELOPMENT } else { PRODUCTION }; + let connector = TlsConnector::new(host, &certificates); + let client = match Client::with_connector(connector) { + Ok(client) => client, + Err(_) => return Err(ProviderError::ClientConnectError("Couldn't connect to APNs service")) + }; + + Ok(TokenClient { + client: client, + }) + } + + pub fn push(&self, notification: Notification, apns_token: &ApnsToken) -> ProviderResponse { + let path = format!("/3/device/{}", notification.device_token).into_bytes(); + let mut headers = default_headers(¬ification); + let body = notification.payload.to_string().into_bytes(); + + headers.push(create_header("authorization", format!("bearer {}", apns_token.signature()))); + + let request = self.client.post(&path, headers.as_slice(), body); + + ProviderResponse::new(request, precise_time_ns()) + } +} diff --git a/src/lib.rs b/src/lib.rs index ab874e0e..3a60c6e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,15 +3,11 @@ extern crate solicit; extern crate rustc_serialize; extern crate time; extern crate openssl; +extern crate btls; -pub mod provider; +pub mod client; pub mod notification; pub mod payload; pub mod device_token; +pub mod apns_token; pub mod response; - -pub use provider::{Provider, AsyncResponse}; -pub use notification::{Notification, NotificationOptions}; -pub use payload::{APS, APSAlert, APSLocalizedAlert, Payload, CustomData}; -pub use device_token::DeviceToken; -pub use response::{Response, APNSError, APNSStatus}; diff --git a/src/provider.rs b/src/provider.rs deleted file mode 100644 index c5cac804..00000000 --- a/src/provider.rs +++ /dev/null @@ -1,201 +0,0 @@ -use notification::*; -use response::*; -use openssl::ssl::{SslContext, SslMethod, SSL_VERIFY_NONE}; -use openssl::x509::X509; -use openssl::crypto::pkey::PKey; -use time::{Tm, Timespec, at, precise_time_ns}; -use rustc_serialize::json::{Json, Object}; -use std::str; -use std::fmt::Display; -use std::result::Result; -use std::thread; -use std::fs::File; -use std::io::Read; -use std::sync::mpsc::Receiver; - -static DEVELOPMENT: &'static str = "api.development.push.apple.com"; -static PRODUCTION: &'static str = "api.push.apple.com"; - -pub struct Provider { - pub client: Client, -} - -impl Provider { - pub fn new(sandbox: bool, certificate: &str, private_key: &str) -> Provider { - Provider::from_reader(sandbox, - &mut File::open(certificate).unwrap(), - &mut File::open(private_key).unwrap()) - } - - pub fn from_reader(sandbox: bool, certificate: &mut R, private_key: &mut R) -> Provider { - let host = if sandbox { DEVELOPMENT } else { PRODUCTION }; - let x509 = X509::from_pem(certificate).unwrap(); - let pkey = PKey::private_key_from_pem(private_key).unwrap(); - - let mut ctx = SslContext::new(SslMethod::Tlsv1_2).unwrap(); - ctx.set_cipher_list("DEFAULT").unwrap(); - ctx.set_certificate(&x509).unwrap(); - ctx.set_private_key(&pkey).unwrap(); - ctx.set_verify(SSL_VERIFY_NONE, None); - ctx.set_alpn_protocols(&[b"h2"]); - - let connector = TlsConnector::with_context(host, &ctx); - let client = Client::with_connector(connector).unwrap(); - - Provider { client: client, } - } - - pub fn push(&self, notification: Notification) -> AsyncResponse { - AsyncResponse::new(self.request(notification), precise_time_ns()) - } - - fn request(&self, notification: Notification) -> Option>> { - let path = format!("/3/device/{}", notification.device_token).into_bytes(); - let body = notification.payload.to_string().into_bytes(); - let mut headers = Vec::new(); - headers.push(Provider::create_header("content_length", notification.payload.len())); - if let Some(apns_id) = notification.options.apns_id { - headers.push(Provider::create_header("apns-id", apns_id)); - } - if let Some(apns_expiration) = notification.options.apns_expiration { - headers.push(Provider::create_header("apns-expiration", apns_expiration)); - } - if let Some(apns_priority) = notification.options.apns_priority { - headers.push(Provider::create_header("apns-priority", apns_priority)); - } - if let Some(apns_topic) = notification.options.apns_topic { - headers.push(Provider::create_header("apns-topic", apns_topic)); - } - - self.client.post(&path, headers.as_slice(), body) - } - - fn create_header<'a, T: Display>(key: &'a str, value: T) -> Header<'a, 'a> { - Header::new(key.as_bytes(), format!("{}", value).into_bytes()) - } - -} - -pub type ResponseChannel = Receiver>; - -pub struct AsyncResponse { - rx: Option, - pub requested_at: u64, -} - -impl AsyncResponse { - pub fn new(rx: Option, requested_at: u64) -> AsyncResponse { - AsyncResponse { rx: rx, requested_at: requested_at } - } - - pub fn recv_timeout(&self, timeout: Duration) -> Result { - if let Some(ref rx) = self.rx { - let now = Instant::now(); - - while now.elapsed() < timeout { - match rx.try_recv() { - Ok(http_response) => { - let status = AsyncResponse::fetch_status(http_response.status_code().ok()); - let apns_id = AsyncResponse::fetch_apns_id(http_response.headers); - let json = str::from_utf8(&http_response.body).ok().and_then(|v| Json::from_str(v).ok()); - let object = json.as_ref().and_then(|v| v.as_object()); - let reason = AsyncResponse::fetch_reason(object); - let timestamp = AsyncResponse::fetch_timestamp(object); - - let response = Response { - status: status, - reason: reason, - timestamp: timestamp, - apns_id: apns_id, - }; - - if response.status == APNSStatus::Success { - return Ok(response); - } else { - return Err(response); - } - }, - _ => thread::park_timeout(Duration::from_millis(10)), - } - } - - Err(Response { - status: APNSStatus::Timeout, - reason: None, - timestamp: None, - apns_id: None, - }) - } else { - Err(Response { - status: APNSStatus::MissingChannel, - reason: None, - timestamp: None, - apns_id: None, - }) - } - } - - fn fetch_status(code: Option) -> APNSStatus { - match code { - Some(200) => APNSStatus::Success, - Some(400) => APNSStatus::BadRequest, - Some(403) => APNSStatus::Forbidden, - Some(405) => APNSStatus::MethodNotAllowed, - Some(410) => APNSStatus::Unregistered, - Some(413) => APNSStatus::PayloadTooLarge, - Some(429) => APNSStatus::TooManyRequests, - Some(500) => APNSStatus::InternalServerError, - Some(503) => APNSStatus::ServiceUnavailable, - _ => APNSStatus::Unknown, - } - } - - fn fetch_apns_id(headers: Vec
) -> Option { - headers.iter().find(|&header| { - match str::from_utf8(header.name()).unwrap() { - "apns-id" => true, - _ => false, - } - }).map(|header| { - String::from_utf8(header.value().to_vec()).unwrap() - }) - } - - fn fetch_reason(js_object: Option<&Object>) -> Option { - let raw_reason = js_object.and_then(|v| v.get("reason")).and_then(|v| v.as_string()); - match raw_reason { - Some("PayloadEmpty") => Some(APNSError::PayloadEmpty), - Some("PayloadTooLarge") => Some(APNSError::PayloadTooLarge), - Some("BadTopic") => Some(APNSError::BadTopic), - Some("TopicDisallowed") => Some(APNSError::TopicDisallowed), - Some("BadMessageId") => Some(APNSError::BadMessageId), - Some("BadExpirationDate") => Some(APNSError::BadExpirationDate), - Some("BadPriority") => Some(APNSError::BadPriority), - Some("MissingDeviceToken") => Some(APNSError::MissingDeviceToken), - Some("BadDeviceToken") => Some(APNSError::BadDeviceToken), - Some("DeviceTokenNotForTopic") => Some(APNSError::DeviceTokenNotForTopic), - Some("Unresgistered") => Some(APNSError::Unregistered), - Some("DuplicateHeaders") => Some(APNSError::DuplicateHeaders), - Some("BadCertificateEnvironment") => Some(APNSError::BadCertificateEnvironment), - Some("BadCertificate") => Some(APNSError::BadCertificate), - Some("Forbidden") => Some(APNSError::Forbidden), - Some("BadPath") => Some(APNSError::BadPath), - Some("MethodNotAllowed") => Some(APNSError::MethodNotAllowed), - Some("TooManyRequests") => Some(APNSError::TooManyRequests), - Some("IdleTimeout") => Some(APNSError::IdleTimeout), - Some("Shutdown") => Some(APNSError::Shutdown), - Some("InternalServerError") => Some(APNSError::InternalServerError), - Some("ServiceUnavailable") => Some(APNSError::ServiceUnavailable), - Some("MissingTopic") => Some(APNSError::MissingTopic), - _ => None, - } - } - - fn fetch_timestamp(js_object: Option<&Object>) -> Option { - let raw_ts = js_object.and_then(|v| v.get("timestamp")).and_then(|v| v.as_i64()); - match raw_ts { - Some(ts) => Some(at(Timespec::new(ts, 0))), - None => None, - } - } -} diff --git a/src/response.rs b/src/response.rs index 382f648b..e69de29b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,133 +0,0 @@ -use std::error::Error; -use std::fmt; -use time::Tm; - -use self::APNSError::*; - -/// The APNS reasons. -pub enum APNSError { - PayloadEmpty, - PayloadTooLarge, - BadTopic, - TopicDisallowed, - BadMessageId, - BadExpirationDate, - BadPriority, - MissingDeviceToken, - BadDeviceToken, - DeviceTokenNotForTopic, - Unregistered, - DuplicateHeaders, - BadCertificateEnvironment, - BadCertificate, - Forbidden, - BadPath, - MethodNotAllowed, - TooManyRequests, - IdleTimeout, - Shutdown, - InternalServerError, - ServiceUnavailable, - MissingTopic, -} - -impl fmt::Debug for APNSError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) - } -} - - -impl fmt::Display for APNSError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) - } -} - -impl Error for APNSError { - fn description(&self) -> &str { - match *self { - PayloadEmpty => "The message payload was empty", - PayloadTooLarge => { - "The message payload was too large. \ - The maximum payload size is 4096 bytes" - } - BadTopic => "The apns-topic was invalid", - TopicDisallowed => "Pushing to this topic is not allowed", - BadMessageId => "The apns-id value is bad", - BadExpirationDate => "The apns-expiration value is bad", - BadPriority => "The apns-priority value is bad", - MissingDeviceToken => { - "The device token is not specified in the request :path. Verify that the :path \ - header contains the device token" - } - BadDeviceToken => { - "The specified device token was bad. Verify that the request contains a valid \ - token and that the token matches the environment" - } - DeviceTokenNotForTopic => "The device token does not match the specified topic", - Unregistered => "The device token is inactive for the specified topic", - DuplicateHeaders => "One or more headers were repeated", - BadCertificateEnvironment => "The client certificate was for the wrong environment", - BadCertificate => "The certificate was bad", - Forbidden => "The specified action is not allowed", - BadPath => "The request contained a bad :path value", - MethodNotAllowed => "The specified :method was not POST", - TooManyRequests => "Too many requests were made consecutively to the same device token", - IdleTimeout => "Idle time out", - Shutdown => "The server is shutting down", - InternalServerError => "An internal server error occurred", - ServiceUnavailable => "The service is unavailable", - MissingTopic => { - "The apns-topic header of the request was not specified and was required. The \ - apns-topic header is mandatory when the client is connected using a certificate \ - that supports multiple topics" - } - } - } - - fn cause(&self) -> Option<&Error> { - match *self { - _ => None, - } - } -} - -/// The HTTP status code. -#[derive(Debug, PartialEq)] -pub enum APNSStatus { - Success = 200, // Success - BadRequest = 400, // Bad request - Forbidden = 403, // There was an error with the certificate. - MethodNotAllowed = 405, // The request used a bad method value. Only POST requests are support - Unregistered = 410, // The device token is no longer active for the topic. - PayloadTooLarge = 413, // The notification payload was too large. - TooManyRequests = 429, // The server received too many requests for the same device token. - InternalServerError = 500, // Internal server error - ServiceUnavailable = 503, // The server is shutting down and unavailable. - MissingChannel = 997, // The response channel died before getting a response - Timeout = 998, // The request timed out - Unknown = 999, // Unknown error -} - -/// The response of request. -#[derive(Debug)] -pub struct Response { - /// Status codes for a response - pub status: APNSStatus, - - /// The apns-id value from the request. - /// If no value was included in the request, - /// the server creates a new UUID and returns it in this header. - pub apns_id: Option, - - /// The error indicating the reason for the failure. - pub reason: Option, - - /// If the value in the :status header is 410,the value of this key is the last time - /// at which APNs confirmed that the device token was no longer valid for the topic. - /// Stop pushing notifications until the device registers a token with - /// a later timestamp with your provider. - pub timestamp: Option, -} - From cdb6a185ab8da389d3ed4f339648252360d5f8c3 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 31 Jan 2017 11:41:32 +0100 Subject: [PATCH 13/21] Bump version to 0.1.0 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c0c22fb..1af07f54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "apns2" -version = "0.0.1" -authors = ["Sergey Tkachenko "] +version = "0.1.0" +authors = ["Sergey Tkachenko ", "Julius de Bruijn "] license = "MIT" readme = "README.md" description = "HTTP/2 Apple Push Notification Service for Rust" From a21dbdd3e68ff8ba94f18284321ca7e922ba119f Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 31 Jan 2017 12:24:46 +0100 Subject: [PATCH 14/21] ProviderResponse, APNSStatus and APNSError as public --- src/client/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/mod.rs b/src/client/mod.rs index 7ad7da3c..324c58b9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -6,3 +6,4 @@ mod token; pub use self::token::TokenClient; pub use self::certificate::CertificateClient; +pub use self::response::{ProviderResponse, APNSStatus, APNSError}; From e2b47a351ebc6d7c188ea54469817dca0d440a16 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 2 Feb 2017 17:17:47 +0100 Subject: [PATCH 15/21] Cosmetic stuff --- src/apns_token.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apns_token.rs b/src/apns_token.rs index 49a8691c..424ba2fc 100644 --- a/src/apns_token.rs +++ b/src/apns_token.rs @@ -40,7 +40,9 @@ impl From for ApnsTokenError { } impl ApnsToken { - pub fn new, R: Read>(pk_der: &mut R, key_id: S, team_id: S) -> Result { + pub fn new(pk_der: &mut R, key_id: S, team_id: S) -> Result + where S: Into, R: Read { + let mut token = ApnsToken { signature: None, issued_at: None, From 9b82ce43fba244958b11633a087350e8845e75cf Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 2 Feb 2017 17:38:17 +0100 Subject: [PATCH 16/21] Change the token push to accept str instead of a token --- examples/token_client.rs | 2 +- src/client/token.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/token_client.rs b/examples/token_client.rs index 50576091..3976ea35 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -54,7 +54,7 @@ fn main() { }; // Fire the request, return value is a mpsc rx channel - let request = client.push(Notification::new(payload, device_token, options), &apns_token); + let request = client.push(Notification::new(payload, device_token, options), &apns_token.signature()); // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout let response = request.recv_timeout(Duration::from_millis(2000)); diff --git a/src/client/token.rs b/src/client/token.rs index 8853aba7..cbf9d626 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -6,7 +6,6 @@ use std::result::Result; use client::response::ProviderResponse; use client::headers::{default_headers, create_header}; use client::error::ProviderError; -use apns_token::ApnsToken; use notification::Notification; static DEVELOPMENT: &'static str = "api.development.push.apple.com"; @@ -30,12 +29,12 @@ impl TokenClient { }) } - pub fn push(&self, notification: Notification, apns_token: &ApnsToken) -> ProviderResponse { + pub fn push(&self, notification: Notification, apns_token: &str) -> ProviderResponse { let path = format!("/3/device/{}", notification.device_token).into_bytes(); let mut headers = default_headers(¬ification); let body = notification.payload.to_string().into_bytes(); - headers.push(create_header("authorization", format!("bearer {}", apns_token.signature()))); + headers.push(create_header("authorization", format!("bearer {}", apns_token))); let request = self.client.post(&path, headers.as_slice(), body); From 9146f421f319dd35c36f2a0209482cb1cc620541 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Thu, 2 Feb 2017 18:46:07 +0100 Subject: [PATCH 17/21] Move the ownership for the pk in token creation --- examples/token_client.rs | 4 ++-- src/apns_token.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/token_client.rs b/examples/token_client.rs index 3976ea35..9f3b0d82 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -34,11 +34,11 @@ fn main() { } // Read the private key from disk - let mut der_file = File::open(der_file_location).unwrap(); + let der_file = File::open(der_file_location).unwrap(); // Create a new token struct with the private key, team id and key id // The token is valid for an hour and needs to be renewed after that - let apns_token = ApnsToken::new(&mut der_file, team_id.as_ref(), key_id.as_ref()).unwrap(); + let apns_token = ApnsToken::new(der_file, team_id.as_ref(), key_id.as_ref()).unwrap(); // Create a new client to APNs, giving the system CA certs let client = TokenClient::new(sandbox, &ca_certs).unwrap(); diff --git a/src/apns_token.rs b/src/apns_token.rs index 424ba2fc..fc3c024c 100644 --- a/src/apns_token.rs +++ b/src/apns_token.rs @@ -40,7 +40,7 @@ impl From for ApnsTokenError { } impl ApnsToken { - pub fn new(pk_der: &mut R, key_id: S, team_id: S) -> Result + pub fn new(mut pk_der: R, key_id: S, team_id: S) -> Result where S: Into, R: Read { let mut token = ApnsToken { @@ -48,7 +48,7 @@ impl ApnsToken { issued_at: None, key_id: key_id.into(), team_id: team_id.into(), - secret: LocalKeyPair::new(pk_der, "apns_private_key")?, + secret: LocalKeyPair::new(&mut pk_der, "apns_private_key")?, }; match token.renew() { From 266a107659c2e097620d5aa6205f0ceabbb07b77 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 6 Feb 2017 16:52:04 +0100 Subject: [PATCH 18/21] Write some docs about the new interface --- README.md | 75 ++++++++++++++++------------------ examples/certificate_client.rs | 2 +- examples/token_client.rs | 2 +- src/apns_token.rs | 68 ++++++++++++++++++++++++++++-- src/client/certificate.rs | 38 +++++++++++++++-- src/client/token.rs | 35 ++++++++++++++++ 6 files changed, 172 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index d62b2bfd..c464960e 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,43 @@ +[![Build Status](https://travis-ci.org/polyitan/apns2.svg?branch=master)](https://travis-ci.org/polyitan/apns2) + # apns2 HTTP/2 Apple Push Notification Service for Rust -## Install (Important: not published in Cargo yet!!!) -Add this to your Cargo.toml: -```toml -[dependencies] -apns2 = "0.0.1" -``` -and this to your crate root: -```rust -extern crate apns2; - -use apns2::{Provider, DeviceToken}; -use apns2::{Notification, NotificationOptions}; -use apns2::{Payload, APS, APSAlert, APSLocalizedAlert}; -use apns2::{Response, APNSError}; -``` -## Generate cert and key files -At first you need export APNs Certificate from KeyChain to .p12 format. And convert to .pem: +Supports certificate-based and token-based authentication. Depends on a forked +solicit to support rust-openssl 0.7 and btls which is not yet released or +stable. We use this right now, but use at your own risk. Plans are to get rid +of solicit and use the tokio http2 client when stable and available. + +### Certificate & Private Key Authentication + +If having the certificate straight from Apple as PKCS12 database, it must be +converted to PEM files containing the certificate and the private key. + ```shell -openssl pkcs12 -in push.p12 -clcerts -out push_cert.pem -openssl pkcs12 -in push.p12 -nocerts -nodes | openssl rsa > push_key.pem +openssl pkcs12 -in push_key.p12 -nodes -out push_key.key -nocerts +openssl pkcs12 -in push_cert.p12 -out push_cert.pem +openssl x509 -outform der -in push_cert.pem -out push_cert.crt ``` -## Usage -#### Sending a push notification -```rust -let provider = Provider::new(true, "/path/to/push_cert.pem", "/path/to/push_key.key"); -let alert = APSAlert::Plain("Message!".to_string()); -let payload = Payload::new(alert, Some(1), "default"); -let token = DeviceToken::new("xxxx...xxxx"); -let options = NotificationOptions::default(); -let notification = Notification::new(payload, token, options); -provider.push(notification, |result| { - match result { - Ok(res) => { - println!("Ok: {:?}", res); - }, - Err(res) => { - println!("Error: {:?}", res); - } - } -}); +The connection is now open for push notifications and should be kept open for +multiple notifications to prevent Apple treating the traffic as DOS. The +connection +is only valid for the application where the certificate was created to. + +### JWT Token Authentication + +To use the PKCS8 formatted private key for token generation, one must +convert it into DER format. + +```shell +openssl pkcs8 -nocrypt -in key.p8 -out newtest.der -outform DER ``` -## License -[MIT License](https://github.com/tkabit/apns2/blob/master/LICENSE) +The connection can be used to send push notifications into any application +by changing the token. The token is valid for one hour until it has to be +renewed. + +All responses are channels which can be blocked to receive the response. For +better +throughput it is a good idea to handle the responses in another thread. + diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index aa33036c..5054f482 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -32,7 +32,7 @@ fn main() { let mut cert_file = File::open(certificate_pem_file).unwrap(); let mut key_file = File::open(key_pem_file).unwrap(); - // Create a new client to APNs, giving the system CA certs + // Create a new client to APNs let client = CertificateClient::new(sandbox, &mut cert_file, &mut key_file).unwrap(); // Create a device token struct from given token diff --git a/examples/token_client.rs b/examples/token_client.rs index 9f3b0d82..9c67ff7d 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -54,7 +54,7 @@ fn main() { }; // Fire the request, return value is a mpsc rx channel - let request = client.push(Notification::new(payload, device_token, options), &apns_token.signature()); + let request = client.push(Notification::new(payload, device_token, options), apns_token.signature()); // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout let response = request.recv_timeout(Duration::from_millis(2000)); diff --git a/src/apns_token.rs b/src/apns_token.rs index fc3c024c..be53aaf6 100644 --- a/src/apns_token.rs +++ b/src/apns_token.rs @@ -40,6 +40,23 @@ impl From for ApnsTokenError { } impl ApnsToken { + /// Create a new ApnsToken. + /// + /// A generator for JWT tokens when using the token-based authentication in APNs. + /// The private key should be in DER binary format and can be provided in any + /// format implementing the Read trait. + /// + /// # Example + /// ```no_run + /// # extern crate apns2; + /// # fn main() { + /// use apns2::apns_token::ApnsToken; + /// use std::fs::File; + /// + /// let der_file = File::open("/path/to/apns.der").unwrap(); + /// ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// # } + ///``` pub fn new(mut pk_der: R, key_id: S, team_id: S) -> Result where S: Into, R: Read { @@ -57,13 +74,42 @@ impl ApnsToken { } } - pub fn signature(&self) -> String { + /// Generates an authentication signature. + /// + /// # Example + /// ```no_run + /// # extern crate apns2; + /// # fn main() { + /// use apns2::apns_token::ApnsToken; + /// use std::fs::File; + /// + /// let der_file = File::open("/path/to/apns.der").unwrap(); + /// let apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// let signature = apns_token.signature(); + /// # } + ///``` + pub fn signature(&self) -> &str { match self.signature { - Some(ref sig) => sig.to_string(), - None => "".to_string() + Some(ref sig) => sig, + None => "" } } + /// Sets a new timestamp for the token. APNs tokens are valid for 60 minutes until + /// they need to be renewed. + /// + /// # Example + /// ```no_run + /// # extern crate apns2; + /// # fn main() { + /// use apns2::apns_token::ApnsToken; + /// use std::fs::File; + /// + /// let der_file = File::open("/path/to/apns.der").unwrap(); + /// let mut apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// apns_token.renew().unwrap(); + /// # } + ///``` pub fn renew(&mut self) -> Result<(), ApnsTokenError> { let issued_at = get_time().sec; @@ -88,6 +134,22 @@ impl ApnsToken { } } + /// Info about the token expiration. If older than one hour, returns true. + /// + /// # Example + /// ```no_run + /// # extern crate apns2; + /// # fn main() { + /// use apns2::apns_token::ApnsToken; + /// use std::fs::File; + /// + /// let der_file = File::open("/path/to/apns.der").unwrap(); + /// let mut apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// if apns_token.is_expired() { + /// apns_token.renew(); + /// } + /// # } + ///``` pub fn is_expired(&self) -> bool { if let Some(issued_at) = self.issued_at { (get_time().sec - issued_at) > 3600 diff --git a/src/client/certificate.rs b/src/client/certificate.rs index 82937e0a..aab9acbc 100644 --- a/src/client/certificate.rs +++ b/src/client/certificate.rs @@ -7,12 +7,45 @@ use time::precise_time_ns; use std::str; use std::result::Result; use std::io::Read; - use client::response::ProviderResponse; use client::headers::default_headers; use client::error::ProviderError; use notification::Notification; +/// Creates a new connection to APNs using the certificate and private key +/// downloaded from Apple developer console. The connection is only valid for +/// the given application. +/// +/// Sends a push notification. Responds with a channel, which can be handled in the same thread or +/// sent out to be handled elsewhere. +/// +/// # Example +/// ``` +/// # extern crate apns2; +/// # fn main() { +/// use apns2::client::CertificateClient; +/// use apns2::device_token::DeviceToken; +/// use apns2::payload::{Payload, APSAlert}; +/// use apns2::notification::{Notification, NotificationOptions}; +/// use std::fs::File; +/// use std::time::Duration; +/// +/// let cert_file = File::open("/path/to/certificate.pem").unwrap(); +/// let key_file = File::open("/path/to/key.pem").unwrap(); +/// let client = CertificateClient::new(false, cert_file, key_file).unwrap(); +/// let device_token = DeviceToken::new("apple_device_token"); +/// let payload = Payload::new(APSAlert::Plain("Howdy"), 1u32, "default", None, None); +/// +/// let options = NotificationOptions { +/// ..Default::default() +/// }; +/// +/// let request = client.push(Notification::new(payload, device_token, options)); +/// let response = request.recv_timeout(Duration::from_millis(2000)); +/// println!("{:?}", response); +/// # } +///``` + static DEVELOPMENT: &'static str = "api.development.push.apple.com"; static PRODUCTION: &'static str = "api.push.apple.com"; @@ -20,6 +53,7 @@ pub struct CertificateClient { pub client: Client, } + impl CertificateClient { pub fn new<'a, R: Read>(sandbox: bool, certificate: &mut R, private_key: &mut R) -> Result> { @@ -55,6 +89,4 @@ impl CertificateClient { ProviderResponse::new(request, precise_time_ns()) } - } - diff --git a/src/client/token.rs b/src/client/token.rs index cbf9d626..a7e6148a 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -8,6 +8,41 @@ use client::headers::{default_headers, create_header}; use client::error::ProviderError; use notification::Notification; +/// Creates a new connection to APNs using the system certificates. When sending +/// notifications through this type of connection, one must attach a valid JWT +/// token with every request. +/// +/// Sends a push notification. Responds with a channel, which can be handled in the same thread or +/// sent out to be handled elsewhere. +/// +/// # Examples +/// ```no_run +/// # extern crate apns2; +/// # fn main() { +/// use apns2::client::TokenClient; +/// use apns2::apns_token::ApnsToken; +/// use apns2::device_token::DeviceToken; +/// use apns2::payload::{Payload, APSAlert}; +/// use apns2::notification::{Notification, NotificationOptions}; +/// use std::fs::File; +/// use std::time::Duration; +/// +/// let der_file = File::open("/path/to/key.der").unwrap(); +/// let apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); +/// let client = TokenClient::new(false, "/etc/ssl/cert.pem").unwrap(); +/// let device_token = DeviceToken::new("apple_device_token"); +/// let payload = Payload::new(APSAlert::Plain("Hi there!"), 1u32, "default", None, None); +/// +/// let options = NotificationOptions { +/// ..Default::default() +/// }; +/// +/// let request = client.push(Notification::new(payload, device_token, options), apns_token.signature()); +/// let response = request.recv_timeout(Duration::from_millis(2000)); +/// println!("{:?}", response); +/// # } +///``` + static DEVELOPMENT: &'static str = "api.development.push.apple.com"; static PRODUCTION: &'static str = "api.push.apple.com"; From 77217280f70653119ea0dc5eef9c61773d21433b Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Mon, 6 Feb 2017 19:58:49 +0100 Subject: [PATCH 19/21] Remove unnecessary response file --- src/response.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/response.rs diff --git a/src/response.rs b/src/response.rs deleted file mode 100644 index e69de29b..00000000 From 9e379e6dbf699c98cc2c0b9c751618a64fa81def Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 7 Feb 2017 15:35:02 +0100 Subject: [PATCH 20/21] Simplify API, more docs --- README.md | 8 +-- examples/certificate_client.rs | 6 +- examples/token_client.rs | 10 +-- src/apns_token.rs | 57 +++++++-------- src/client/certificate.rs | 57 +++++++-------- src/client/error.rs | 26 ++++--- src/client/mod.rs | 8 +++ src/client/response.rs | 124 +++++++++++++++++++++++++++------ src/client/token.rs | 56 +++++++-------- src/device_token.rs | 21 ------ src/lib.rs | 14 +++- src/notification.rs | 23 ++++-- src/payload.rs | 13 ++-- 13 files changed, 256 insertions(+), 167 deletions(-) delete mode 100644 src/device_token.rs diff --git a/README.md b/README.md index c464960e..c920c34f 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ openssl x509 -outform der -in push_cert.pem -out push_cert.crt ``` The connection is now open for push notifications and should be kept open for -multiple notifications to prevent Apple treating the traffic as DOS. The -connection +multiple notifications to prevent Apple treating the traffic as DOS. The connection is only valid for the application where the certificate was created to. ### JWT Token Authentication @@ -37,7 +36,8 @@ The connection can be used to send push notifications into any application by changing the token. The token is valid for one hour until it has to be renewed. -All responses are channels which can be blocked to receive the response. For -better +All responses are channels which can be blocked to receive the response. For better throughput it is a good idea to handle the responses in another thread. +## License +[MIT License](https://github.com/tkabit/apns2/blob/master/LICENSE) diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index 5054f482..46fdb03a 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -3,7 +3,6 @@ extern crate argparse; use argparse::{ArgumentParser, Store, StoreTrue}; use apns2::client::CertificateClient; -use apns2::device_token::DeviceToken; use apns2::payload::{Payload, APSAlert}; use apns2::notification::{Notification, NotificationOptions}; use std::fs::File; @@ -35,9 +34,6 @@ fn main() { // Create a new client to APNs let client = CertificateClient::new(sandbox, &mut cert_file, &mut key_file).unwrap(); - // Create a device token struct from given token - let device_token = DeviceToken::new(device_token); - // APNs payload let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); @@ -46,7 +42,7 @@ fn main() { }; // Fire the request, return value is a mpsc rx channel - let request = client.push(Notification::new(payload, device_token, options)); + let request = client.push(Notification::new(payload, &device_token, options)); // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout let response = request.recv_timeout(Duration::from_millis(2000)); diff --git a/examples/token_client.rs b/examples/token_client.rs index 9c67ff7d..5b2f1fad 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -3,8 +3,7 @@ extern crate argparse; use argparse::{ArgumentParser, Store, StoreTrue}; use apns2::client::TokenClient; -use apns2::apns_token::ApnsToken; -use apns2::device_token::DeviceToken; +use apns2::apns_token::APNSToken; use apns2::payload::{Payload, APSAlert}; use apns2::notification::{Notification, NotificationOptions}; use std::fs::File; @@ -38,14 +37,11 @@ fn main() { // Create a new token struct with the private key, team id and key id // The token is valid for an hour and needs to be renewed after that - let apns_token = ApnsToken::new(der_file, team_id.as_ref(), key_id.as_ref()).unwrap(); + let apns_token = APNSToken::new(der_file, team_id.as_ref(), key_id.as_ref()).unwrap(); // Create a new client to APNs, giving the system CA certs let client = TokenClient::new(sandbox, &ca_certs).unwrap(); - // Create a device token struct from given token - let device_token = DeviceToken::new(device_token.as_ref()); - // APNs payload let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); @@ -54,7 +50,7 @@ fn main() { }; // Fire the request, return value is a mpsc rx channel - let request = client.push(Notification::new(payload, device_token, options), apns_token.signature()); + let request = client.push(Notification::new(payload, &device_token, options), apns_token.signature()); // Read the response and block maximum of 2000 milliseconds, throwing an exception for a timeout let response = request.recv_timeout(Duration::from_millis(2000)); diff --git a/src/apns_token.rs b/src/apns_token.rs index be53aaf6..97f9647e 100644 --- a/src/apns_token.rs +++ b/src/apns_token.rs @@ -1,3 +1,5 @@ +//! A module for APNS JWT token management. + use btls::server_keys::LocalKeyPair; use btls::jose_jws::{sign_jws, JsonNode}; use std::convert::From; @@ -8,7 +10,7 @@ use std::io::Read; const SIG_ECDSA_SHA256: u16 = 0x0403; -pub struct ApnsToken { +pub struct APNSToken { signature: Option, issued_at: Option, key_id: String, @@ -17,7 +19,7 @@ pub struct ApnsToken { } #[derive(Debug)] -pub enum ApnsTokenError { +pub enum APNSTokenError { SignError, KeyParseError(String), KeyOpenError(String), @@ -26,21 +28,20 @@ pub enum ApnsTokenError { KeyError, } - -impl From for ApnsTokenError { - fn from(e: KeyReadError) -> ApnsTokenError { +impl From for APNSTokenError { + fn from(e: KeyReadError) -> APNSTokenError { match e { - KeyReadError::ParseError(e, _) => ApnsTokenError::KeyParseError(e), - KeyReadError::OpenError(e, _) => ApnsTokenError::KeyOpenError(e), - KeyReadError::ReadError(e, _) => ApnsTokenError::KeyReadError(e), - KeyReadError::KeyGenerationFailed => ApnsTokenError::KeyGenerationError, - _ => ApnsTokenError::KeyError, + KeyReadError::ParseError(e, _) => APNSTokenError::KeyParseError(e), + KeyReadError::OpenError(e, _) => APNSTokenError::KeyOpenError(e), + KeyReadError::ReadError(e, _) => APNSTokenError::KeyReadError(e), + KeyReadError::KeyGenerationFailed => APNSTokenError::KeyGenerationError, + _ => APNSTokenError::KeyError, } } } -impl ApnsToken { - /// Create a new ApnsToken. +impl APNSToken { + /// Create a new APNSToken. /// /// A generator for JWT tokens when using the token-based authentication in APNs. /// The private key should be in DER binary format and can be provided in any @@ -50,17 +51,17 @@ impl ApnsToken { /// ```no_run /// # extern crate apns2; /// # fn main() { - /// use apns2::apns_token::ApnsToken; + /// use apns2::apns_token::APNSToken; /// use std::fs::File; /// /// let der_file = File::open("/path/to/apns.der").unwrap(); - /// ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// APNSToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); /// # } - ///``` - pub fn new(mut pk_der: R, key_id: S, team_id: S) -> Result + /// ``` + pub fn new(mut pk_der: R, key_id: S, team_id: S) -> Result where S: Into, R: Read { - let mut token = ApnsToken { + let mut token = APNSToken { signature: None, issued_at: None, key_id: key_id.into(), @@ -80,14 +81,14 @@ impl ApnsToken { /// ```no_run /// # extern crate apns2; /// # fn main() { - /// use apns2::apns_token::ApnsToken; + /// use apns2::apns_token::APNSToken; /// use std::fs::File; /// /// let der_file = File::open("/path/to/apns.der").unwrap(); - /// let apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// let apns_token = APNSToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); /// let signature = apns_token.signature(); /// # } - ///``` + /// ``` pub fn signature(&self) -> &str { match self.signature { Some(ref sig) => sig, @@ -102,15 +103,15 @@ impl ApnsToken { /// ```no_run /// # extern crate apns2; /// # fn main() { - /// use apns2::apns_token::ApnsToken; + /// use apns2::apns_token::APNSToken; /// use std::fs::File; /// /// let der_file = File::open("/path/to/apns.der").unwrap(); - /// let mut apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// let mut apns_token = APNSToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); /// apns_token.renew().unwrap(); /// # } - ///``` - pub fn renew(&mut self) -> Result<(), ApnsTokenError> { + /// ``` + pub fn renew(&mut self) -> Result<(), APNSTokenError> { let issued_at = get_time().sec; let mut headers: BTreeMap = BTreeMap::new(); @@ -130,7 +131,7 @@ impl ApnsToken { self.issued_at = Some(issued_at); Ok(()) } - _ => Err(ApnsTokenError::SignError) + _ => Err(APNSTokenError::SignError) } } @@ -140,16 +141,16 @@ impl ApnsToken { /// ```no_run /// # extern crate apns2; /// # fn main() { - /// use apns2::apns_token::ApnsToken; + /// use apns2::apns_token::APNSToken; /// use std::fs::File; /// /// let der_file = File::open("/path/to/apns.der").unwrap(); - /// let mut apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); + /// let mut apns_token = APNSToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); /// if apns_token.is_expired() { /// apns_token.renew(); /// } /// # } - ///``` + /// ``` pub fn is_expired(&self) -> bool { if let Some(issued_at) = self.issued_at { (get_time().sec - issued_at) > 3600 diff --git a/src/client/certificate.rs b/src/client/certificate.rs index aab9acbc..93db70ad 100644 --- a/src/client/certificate.rs +++ b/src/client/certificate.rs @@ -11,52 +11,48 @@ use client::response::ProviderResponse; use client::headers::default_headers; use client::error::ProviderError; use notification::Notification; +use client::{DEVELOPMENT, PRODUCTION}; -/// Creates a new connection to APNs using the certificate and private key -/// downloaded from Apple developer console. The connection is only valid for -/// the given application. +pub struct CertificateClient { + pub client: Client, +} + +/// Creates a new connection to APNs using a certificate and private key to an +/// application. The connection is only valid for one application. /// -/// Sends a push notification. Responds with a channel, which can be handled in the same thread or -/// sent out to be handled elsewhere. +/// The response for `push` is asynchorous for better throughput. /// /// # Example -/// ``` +/// ```no_run /// # extern crate apns2; /// # fn main() { /// use apns2::client::CertificateClient; -/// use apns2::device_token::DeviceToken; /// use apns2::payload::{Payload, APSAlert}; /// use apns2::notification::{Notification, NotificationOptions}; /// use std::fs::File; /// use std::time::Duration; /// -/// let cert_file = File::open("/path/to/certificate.pem").unwrap(); -/// let key_file = File::open("/path/to/key.pem").unwrap(); -/// let client = CertificateClient::new(false, cert_file, key_file).unwrap(); -/// let device_token = DeviceToken::new("apple_device_token"); -/// let payload = Payload::new(APSAlert::Plain("Howdy"), 1u32, "default", None, None); +/// // Can be anything that implements the `Read` trait. +/// let mut cert_file = File::open("/path/to/certificate.pem").unwrap(); +/// let mut key_file = File::open("/path/to/key.pem").unwrap(); /// -/// let options = NotificationOptions { -/// ..Default::default() -/// }; +/// let client = CertificateClient::new(false, &mut cert_file, &mut key_file).unwrap(); +/// let alert = APSAlert::Plain(String::from("Hi there!")); +/// let payload = Payload::new(alert, 1u32, "default", None, None); +/// let options = NotificationOptions { ..Default::default() }; +/// let request = client.push(Notification::new(payload, "apple_device_token", options)); /// -/// let request = client.push(Notification::new(payload, device_token, options)); +/// // Block here to get the response. /// let response = request.recv_timeout(Duration::from_millis(2000)); +/// /// println!("{:?}", response); /// # } -///``` - -static DEVELOPMENT: &'static str = "api.development.push.apple.com"; -static PRODUCTION: &'static str = "api.push.apple.com"; - -pub struct CertificateClient { - pub client: Client, -} - - +/// ``` impl CertificateClient { + /// Create a new connection to APNs with custom certificate and key. Can be + /// used to send notification to only one app. pub fn new<'a, R: Read>(sandbox: bool, certificate: &mut R, private_key: &mut R) - -> Result> { + -> Result { let host = if sandbox { DEVELOPMENT } else { PRODUCTION }; let mut ctx = SslContext::new(SslMethod::Sslv23).unwrap(); @@ -70,17 +66,14 @@ impl CertificateClient { ctx.set_alpn_protocols(&[b"h2"]); let connector = TlsConnector::with_context(host, &ctx); - - let client = match Client::with_connector(connector) { - Ok(client) => client, - Err(_) => return Err(ProviderError::ClientConnectError("Couldn't connect to APNs service")) - }; + let client = Client::with_connector(connector)?; Ok(CertificateClient { client: client, }) } + /// Send a notification. pub fn push<'a>(&self, notification: Notification) -> ProviderResponse { let path = format!("/3/device/{}", notification.device_token).into_bytes(); let body = notification.payload.to_string().into_bytes(); diff --git a/src/client/error.rs b/src/client/error.rs index 02cfb064..e694a966 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,22 +1,30 @@ +use solicit::client::ClientConnectError; +use solicit::http::client::tls::TlsConnectError; use openssl::ssl::error::SslError; use std::error::Error; use std::fmt; #[derive(Debug)] -pub enum ProviderError<'a> { - ClientConnectError(&'a str), - SslError(&'a str) +pub enum ProviderError { + ClientConnectError(String), + SslError(String) } -impl<'a> From for ProviderError<'a> { - fn from(_: SslError) -> ProviderError<'a> { - ProviderError::SslError("Error generationg SSL context") +impl From for ProviderError { + fn from(e: SslError) -> ProviderError { + ProviderError::SslError(format!("Error generating an SSL context: {}", e.description())) } } -impl<'a> Error for ProviderError<'a> { +impl From> for ProviderError { + fn from(e: ClientConnectError) -> ProviderError { + ProviderError::ClientConnectError(format!("Error connecting to the APNs servers: {}", e.description())) + } +} + +impl Error for ProviderError { fn description(&self) -> &str { - "Error in APNs connection" + "APNs connection failed" } fn cause(&self) -> Option<&Error> { @@ -24,7 +32,7 @@ impl<'a> Error for ProviderError<'a> { } } -impl<'a> fmt::Display for ProviderError<'a> { +impl fmt::Display for ProviderError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.description()) } diff --git a/src/client/mod.rs b/src/client/mod.rs index 324c58b9..6247666f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,3 +1,8 @@ +//! The APNs connection handling modules. `TokenClient` for connections using +//! JWT authentication, `CertificateClient` when using a sertificate and a +//! private key to authenticate. `ProviderResponse` handles responses and maps +//! the results to `APNSStatus` and `APNSError`. + mod certificate; mod response; mod headers; @@ -7,3 +12,6 @@ mod token; pub use self::token::TokenClient; pub use self::certificate::CertificateClient; pub use self::response::{ProviderResponse, APNSStatus, APNSError}; + +static DEVELOPMENT: &'static str = "api.development.push.apple.com"; +static PRODUCTION: &'static str = "api.push.apple.com"; diff --git a/src/client/response.rs b/src/client/response.rs index eed8ceb8..067b77de 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -12,31 +12,89 @@ use self::APNSError::*; // The APNS reasons. pub enum APNSError { + /// The message payload was empty. PayloadEmpty, + + /// The message payload was too large. The maximum payload size is 4096 + /// bytes. PayloadTooLarge, + + /// The apns-topic was invalid. BadTopic, + + /// Pushing to this topic is not allowed. TopicDisallowed, + + /// The apns-id value is bad. BadMessageId, + + /// The apns-expiration value is bad. BadExpirationDate, + + /// The apns-priority value is bad. BadPriority, + + /// The device token is not specified in the request `path`. Verify that the + /// `path` header contains the device token. MissingDeviceToken, + + /// The specified device token was bad. Verify that the request contains a + /// valid token and that the token matches the environment. BadDeviceToken, + + /// The device token does not match the specified topic. DeviceTokenNotForTopic, + + /// The device token is inactive for the specified topic. Unregistered, + + /// One or more headers were repeated. DuplicateHeaders, + + /// The client certificate was for the wrong environment. BadCertificateEnvironment, + + /// The certificate was bad. BadCertificate, + + /// The specified action is not allowed. Forbidden, + + /// The provider token is not valid or the token signature could not be + /// verified. InvalidProviderToken, + + /// No provider certificate was used to connect to APNs and Authorization + /// header was missing or no provider token was specified. MissingProviderToken, + + /// The provider token is stale and a new token should be generated. ExpiredProviderToken, + + /// The request contained a bad `path` value. BadPath, + + /// The specified `method` was not `POST`. MethodNotAllowed, + + /// Too many requests were made consecutively to the same device token. TooManyRequests, + + /// Idle timeout. IdleTimeout, + + /// The server is shutting down. Shutdown, + + /// An internal server error occurred. InternalServerError, + + /// The service is unavailable. ServiceUnavailable, + + /// The apns-topic header of the request was not specified and was required. + /// The apns-topic header is mandatory when the client is connected using a + /// certificate that supports multiple topics. MissingTopic, } @@ -108,37 +166,60 @@ impl Error for APNSError { // The HTTP status code. #[derive(Debug, PartialEq)] pub enum APNSStatus { - Success = 200, // Success - BadRequest = 400, // Bad request - Forbidden = 403, // There was an error with the certificate. - MethodNotAllowed = 405, // The request used a bad method value. Only POST requests are support - Unregistered = 410, // The device token is no longer active for the topic. - PayloadTooLarge = 413, // The notification payload was too large. - TooManyRequests = 429, // The server received too many requests for the same device token. - InternalServerError = 500, // Internal server error - ServiceUnavailable = 503, // The server is shutting down and unavailable. - MissingChannel = 997, // The response channel died before getting a response - Timeout = 998, // The request timed out - Unknown = 999, // Unknown error + /// Success + Success = 200, + + /// Bad request + BadRequest = 400, + + /// There was an error with the certificate. + Forbidden = 403, + + /// The request used a bad method value. Only POST requests are support + MethodNotAllowed = 405, + + /// The device token is no longer active for the topic. + Unregistered = 410, + + /// The notification payload was too large. + PayloadTooLarge = 413, + + /// The server received too many requests for the same device token. + TooManyRequests = 429, + + /// Internal server error + InternalServerError = 500, + + /// The server is shutting down and unavailable. + ServiceUnavailable = 503, + + /// The response channel died before getting a response + MissingChannel = 997, + + /// The request timed out + Timeout = 998, + + /// Unknown error + Unknown = 999, } #[derive(Debug)] pub struct Response { - // Status codes for a response + /// Status codes for a response pub status: APNSStatus, - // The apns-id value from the request. - // If no value was included in the request, - // the server creates a new UUID and returns it in this header. + /// The apns-id value from the request. + /// If no value was included in the request, + /// the server creates a new UUID and returns it in this header. pub apns_id: Option, - // The error indicating the reason for the failure. + /// The error indicating the reason for the failure. pub reason: Option, - // If the value in the :status header is 410,the value of this key is the last time - // at which APNs confirmed that the device token was no longer valid for the topic. - // Stop pushing notifications until the device registers a token with - // a later timestamp with your provider. + /// If the value in the :status header is 410,the value of this key is the last time + /// at which APNs confirmed that the device token was no longer valid for the topic. + /// Stop pushing notifications until the device registers a token with + /// a later timestamp with your provider. pub timestamp: Option, } @@ -155,6 +236,7 @@ impl ProviderResponse { ProviderResponse { rx: rx, requested_at: requested_at } } + /// Blocks until having a response from APNS or the timeout is due. pub fn recv_timeout(&self, timeout: Duration) -> Result { if let Some(ref rx) = self.rx { let now = Instant::now(); diff --git a/src/client/token.rs b/src/client/token.rs index a7e6148a..1a545a43 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -1,4 +1,4 @@ -use solicit::http::client::tls::TlsConnector; +use solicit::http::client::tls::{TlsConnector}; use solicit::client::{Client}; use time::precise_time_ns; use std::result::Result; @@ -7,63 +7,63 @@ use client::response::ProviderResponse; use client::headers::{default_headers, create_header}; use client::error::ProviderError; use notification::Notification; +use client::{DEVELOPMENT, PRODUCTION}; + +pub struct TokenClient { + pub client: Client, +} /// Creates a new connection to APNs using the system certificates. When sending /// notifications through this type of connection, one must attach a valid JWT -/// token with every request. +/// token with every request using either `APNSToken` or an own implementation. +/// The same connection can be used to send notifications to multiple applications. /// -/// Sends a push notification. Responds with a channel, which can be handled in the same thread or -/// sent out to be handled elsewhere. +/// The response for `push` is asynchronous for better throughput. /// /// # Examples /// ```no_run /// # extern crate apns2; /// # fn main() { /// use apns2::client::TokenClient; -/// use apns2::apns_token::ApnsToken; -/// use apns2::device_token::DeviceToken; +/// use apns2::apns_token::APNSToken; /// use apns2::payload::{Payload, APSAlert}; /// use apns2::notification::{Notification, NotificationOptions}; /// use std::fs::File; /// use std::time::Duration; /// +/// // Can be anything that implements the `Read` trait. /// let der_file = File::open("/path/to/key.der").unwrap(); -/// let apns_token = ApnsToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); -/// let client = TokenClient::new(false, "/etc/ssl/cert.pem").unwrap(); -/// let device_token = DeviceToken::new("apple_device_token"); -/// let payload = Payload::new(APSAlert::Plain("Hi there!"), 1u32, "default", None, None); /// -/// let options = NotificationOptions { -/// ..Default::default() -/// }; +/// let apns_token = APNSToken::new(der_file, "TEAMID1234", "KEYID12345").unwrap(); /// -/// let request = client.push(Notification::new(payload, device_token, options), apns_token.signature()); +/// // Example certificate file from Ubuntu +/// let client = TokenClient::new(false, "/etc/ssl/certs/ca-certificates.crt").unwrap(); +/// +/// let alert = APSAlert::Plain(String::from("Hi there!")); +/// let payload = Payload::new(alert, 1u32, "default", None, None); +/// let options = NotificationOptions { ..Default::default() }; +/// let request = client.push(Notification::new(payload, "Hi there!", options), apns_token.signature()); +/// +/// // Block here to get the response. /// let response = request.recv_timeout(Duration::from_millis(2000)); +/// /// println!("{:?}", response); /// # } -///``` - -static DEVELOPMENT: &'static str = "api.development.push.apple.com"; -static PRODUCTION: &'static str = "api.push.apple.com"; - -pub struct TokenClient { - pub client: Client, -} - +/// ``` impl TokenClient { - pub fn new<'a>(sandbox: bool, certificates: &str) -> Result> { + /// Create a new connection to APNs. `certificates` should point to system ca certificate + /// file. In Ubuntu it's usually `/etc/ssl/certs/ca-certificates.crt`. + pub fn new<'a>(sandbox: bool, certificates: &str) -> Result { let host = if sandbox { DEVELOPMENT } else { PRODUCTION }; let connector = TlsConnector::new(host, &certificates); - let client = match Client::with_connector(connector) { - Ok(client) => client, - Err(_) => return Err(ProviderError::ClientConnectError("Couldn't connect to APNs service")) - }; + let client = Client::with_connector(connector)?; Ok(TokenClient { client: client, }) } + /// Send a push notification with a JWT signature. pub fn push(&self, notification: Notification, apns_token: &str) -> ProviderResponse { let path = format!("/3/device/{}", notification.device_token).into_bytes(); let mut headers = default_headers(¬ification); diff --git a/src/device_token.rs b/src/device_token.rs deleted file mode 100644 index 39a23a8a..00000000 --- a/src/device_token.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::fmt; -use std::borrow::Cow; - -/// Specify the hexadecimal bytes of the device token for the target device. -pub struct DeviceToken<'a> { - pub token: Cow<'a, str>, -} - -impl<'a> DeviceToken<'a> { - pub fn new(token: S) -> DeviceToken<'a> - where S: Into> - { - DeviceToken { token: token.into() } - } -} - -impl<'a> fmt::Display for DeviceToken<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.token) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3a60c6e6..a9147194 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,15 @@ +//! A library for sending push notifications to iOS devices using Apple's APNS +//! API. Supports certificate based authentication through +//! `apns2::client::CertificateClient` and JWT token based authentication +//! through `apns2::client::TokenClient`. +//! +//! If using JWT tokens for authentication, `apns2::apns_token::ApnsToken` can +//! be used for generating and holding tokens, allowing re-use and renewal. +//! +//! The `apns::client::ProviderResponse` does not block until using the +//! `recv_timeout`. The request is handled in another thread and the response is +//! sent through a channel to the thread calling the method. + #[macro_use] extern crate solicit; extern crate rustc_serialize; @@ -8,6 +20,4 @@ extern crate btls; pub mod client; pub mod notification; pub mod payload; -pub mod device_token; pub mod apns_token; -pub mod response; diff --git a/src/notification.rs b/src/notification.rs index 66f4c945..937a27c6 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,13 +1,15 @@ +//! A helper struct for generating an APNS request. + use payload::*; -use device_token::*; /// The Remote Notification. pub struct Notification<'a> { /// The Remote Notification Payload. - pub payload: Payload<'a>, + pub payload: Payload, - /// Specify the hexadecimal string of the device token for the target device. - pub device_token: DeviceToken<'a>, + /// Specify the hexadecimal string of the device token for the target + /// device. + pub device_token: &'a str, /// The optional settings for the notification pub options: NotificationOptions<'a>, @@ -34,7 +36,8 @@ pub struct NotificationOptions<'a> { /// The priority of the notification. pub apns_priority: Option, - /// The topic of the remote notification, which is typically the bundle ID for your app. + /// The topic of the remote notification, which is typically the bundle ID + /// for your app. pub apns_topic: Option<&'a str>, } @@ -48,3 +51,13 @@ impl<'a> Default for NotificationOptions<'a> { } } } + +impl<'a> Notification<'a> { + pub fn new(payload: Payload, token: &'a str, options: NotificationOptions<'a>) -> Notification<'a> { + Notification { + payload: payload, + device_token: token, + options: options, + } + } +} diff --git a/src/payload.rs b/src/payload.rs index ac23c1c1..fd51c0fc 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -3,24 +3,27 @@ use std::collections::BTreeMap; use rustc_serialize::json::{Json, ToJson}; pub struct Payload { + /// The standard APNS payload data. pub aps: APS, + + /// Custom data to be handled by the app. pub custom: Option, } pub struct APS { pub alert: Option, - // The number to display as the badge of the app icon. + /// The number to display as the badge of the app icon. pub badge: Option, - // The name of a sound file in the app bundle or in the Library/Sounds folder of - // the app’s data container. + /// The name of a sound file in the app bundle or in the Library/Sounds folder of + /// the app’s data container. pub sound: Option, - // Provide this key with a value of 1 to indicate that new content is available. + /// Provide this key with a value of 1 to indicate that new content is available. pub content_available: Option, - // Provide this key with a string value that represents the identifier property. + /// Provide this key with a string value that represents the identifier property. pub category: Option, } From 5b5058e9e6be953f67852547b9f743d1693b1be3 Mon Sep 17 00:00:00 2001 From: Julius de Bruijn Date: Tue, 7 Feb 2017 17:28:28 +0100 Subject: [PATCH 21/21] Merge fixes --- examples/certificate_client.rs | 2 +- examples/token_client.rs | 2 +- src/client/certificate.rs | 2 +- src/client/token.rs | 2 +- src/notification.rs | 12 +------- src/payload.rs | 54 ++++++++-------------------------- 6 files changed, 17 insertions(+), 57 deletions(-) diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs index 46fdb03a..b531a51e 100644 --- a/examples/certificate_client.rs +++ b/examples/certificate_client.rs @@ -35,7 +35,7 @@ fn main() { let client = CertificateClient::new(sandbox, &mut cert_file, &mut key_file).unwrap(); // APNs payload - let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); + let payload = Payload::new(APSAlert::Plain(message), "default", Some(1u32), None, None); let options = NotificationOptions { ..Default::default() diff --git a/examples/token_client.rs b/examples/token_client.rs index 5b2f1fad..30092362 100644 --- a/examples/token_client.rs +++ b/examples/token_client.rs @@ -43,7 +43,7 @@ fn main() { let client = TokenClient::new(sandbox, &ca_certs).unwrap(); // APNs payload - let payload = Payload::new(APSAlert::Plain(message), 1u32, "default", None, None); + let payload = Payload::new(APSAlert::Plain(message), "default", Some(1u32), None, None); let options = NotificationOptions { ..Default::default() diff --git a/src/client/certificate.rs b/src/client/certificate.rs index 93db70ad..9bb37f65 100644 --- a/src/client/certificate.rs +++ b/src/client/certificate.rs @@ -38,7 +38,7 @@ pub struct CertificateClient { /// /// let client = CertificateClient::new(false, &mut cert_file, &mut key_file).unwrap(); /// let alert = APSAlert::Plain(String::from("Hi there!")); -/// let payload = Payload::new(alert, 1u32, "default", None, None); +/// let payload = Payload::new(alert, "default", Some(1u32), None, None); /// let options = NotificationOptions { ..Default::default() }; /// let request = client.push(Notification::new(payload, "apple_device_token", options)); /// diff --git a/src/client/token.rs b/src/client/token.rs index 1a545a43..0484f412 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -40,7 +40,7 @@ pub struct TokenClient { /// let client = TokenClient::new(false, "/etc/ssl/certs/ca-certificates.crt").unwrap(); /// /// let alert = APSAlert::Plain(String::from("Hi there!")); -/// let payload = Payload::new(alert, 1u32, "default", None, None); +/// let payload = Payload::new(alert, "default", Some(1u32), None, None); /// let options = NotificationOptions { ..Default::default() }; /// let request = client.push(Notification::new(payload, "Hi there!", options), apns_token.signature()); /// diff --git a/src/notification.rs b/src/notification.rs index 937a27c6..440ebf1c 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -16,7 +16,7 @@ pub struct Notification<'a> { } impl<'a> Notification<'a> { - pub fn new(payload: Payload<'a>, token: DeviceToken<'a>, options: NotificationOptions<'a>) -> Notification<'a> { + pub fn new(payload: Payload, token: &'a str, options: NotificationOptions<'a>) -> Notification<'a> { Notification { payload: payload, device_token: token, @@ -51,13 +51,3 @@ impl<'a> Default for NotificationOptions<'a> { } } } - -impl<'a> Notification<'a> { - pub fn new(payload: Payload, token: &'a str, options: NotificationOptions<'a>) -> Notification<'a> { - Notification { - payload: payload, - device_token: token, - options: options, - } - } -} diff --git a/src/payload.rs b/src/payload.rs index fd51c0fc..396f1a11 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::BTreeMap; use rustc_serialize::json::{Json, ToJson}; @@ -10,46 +9,17 @@ pub struct Payload { pub custom: Option, } -pub struct APS { - pub alert: Option, - - /// The number to display as the badge of the app icon. - pub badge: Option, - - /// The name of a sound file in the app bundle or in the Library/Sounds folder of - /// the app’s data container. - pub sound: Option, - - /// Provide this key with a value of 1 to indicate that new content is available. - pub content_available: Option, - - /// Provide this key with a string value that represents the identifier property. - pub category: Option, -} - -pub enum APSAlert { - Plain(String), - Localized(APSLocalizedAlert), -} - -pub struct APSLocalizedAlert { - pub title: String, - pub body: String, - pub title_loc_key: Option, - pub title_loc_args: Option>, - pub action_loc_key: Option, - pub loc_key: Option, - pub loc_args: Option>, - pub launch_image: Option, -} - pub struct CustomData { + /// The JSON root key for app specific custom data. pub key: String, + + /// The custom data. pub body: Json, } impl Payload { - pub fn new(alert: APSAlert, badge: u32, sound: S, category: Option, custom_data: Option) -> Payload + pub fn new(alert: APSAlert, sound: S, badge: Option, category: Option, + custom_data: Option) -> Payload where S: Into { Payload { @@ -86,7 +56,7 @@ impl Payload { } } -impl<'a> ToJson for Payload<'a> { +impl ToJson for Payload { fn to_json(&self) -> Json { let mut d = BTreeMap::new(); d.insert("aps".to_string(), self.aps.to_json()); @@ -103,7 +73,7 @@ impl<'a> ToJson for Payload<'a> { /// - an alert message to display to the user /// - a number to badge the app icon with /// - a sound to play -pub struct APS<'a> { +pub struct APS { /// If this property is included, the system displays a standard alert or a banner, /// based on the user’s setting. pub alert: Option, @@ -113,16 +83,16 @@ pub struct APS<'a> { /// The name of a sound file in the app bundle or in the Library/Sounds folder of /// the app’s data container. - pub sound: Option>, + pub sound: Option, /// Provide this key with a value of 1 to indicate that new content is available. pub content_available: Option, /// Provide this key with a string value that represents the identifier property. - pub category: Option>, + pub category: Option, } -impl<'a> ToJson for APS<'a> { +impl ToJson for APS { fn to_json(&self) -> Json { let mut d = BTreeMap::new(); match self.alert { @@ -174,10 +144,10 @@ pub struct APSLocalizedAlert { pub action_loc_key: Option, /// A key to an alert-message string in a Localizable.strings file for the current localization. - pub loc_key: String, + pub loc_key: Option, /// Variable string values to appear in place of the format specifiers in loc-key. - pub loc_args: Vec, + pub loc_args: Option>, /// The filename of an image file in the app bundle. /// The image is used as the launch image when users tap the action button or move the action slider.