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 ccc89d03..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" @@ -15,10 +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/aagahi/solicit" +git = "https://github.com/pimeys/solicit" default-features = true features = ["tls"] + +[dependencies.btls] +features = ["gnutls-ecdsa"] +git = "https://gitlab.com/ilari_l/btls.git" diff --git a/README.md b/README.md index 39c7a593..c920c34f 100644 --- a/README.md +++ b/README.md @@ -1,49 +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 ``` +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. + ## License [MIT License](https://github.com/tkabit/apns2/blob/master/LICENSE) diff --git a/examples/certificate_client.rs b/examples/certificate_client.rs new file mode 100644 index 00000000..b531a51e --- /dev/null +++ b/examples/certificate_client.rs @@ -0,0 +1,51 @@ +extern crate apns2; +extern crate argparse; + +use argparse::{ArgumentParser, Store, StoreTrue}; +use apns2::client::CertificateClient; +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 + let client = CertificateClient::new(sandbox, &mut cert_file, &mut key_file).unwrap(); + + // APNs payload + let payload = Payload::new(APSAlert::Plain(message), "default", Some(1u32), 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..30092362 --- /dev/null +++ b/examples/token_client.rs @@ -0,0 +1,59 @@ +extern crate apns2; +extern crate argparse; + +use argparse::{ArgumentParser, Store, StoreTrue}; +use apns2::client::TokenClient; +use apns2::apns_token::APNSToken; +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 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(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(); + + // APNs payload + let payload = Payload::new(APSAlert::Plain(message), "default", Some(1u32), 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.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)); + + println!("{:?}", response); +} diff --git a/src/apns_token.rs b/src/apns_token.rs new file mode 100644 index 00000000..97f9647e --- /dev/null +++ b/src/apns_token.rs @@ -0,0 +1,161 @@ +//! A module for APNS JWT token management. + +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 { + /// 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 { + + let mut token = APNSToken { + signature: None, + issued_at: None, + key_id: key_id.into(), + team_id: team_id.into(), + secret: LocalKeyPair::new(&mut pk_der, "apns_private_key")?, + }; + + match token.renew() { + Err(e) => Err(e), + _ => Ok(token), + } + } + + /// 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, + 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; + + 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) + } + } + + /// 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 + } else { + true + } + } +} diff --git a/src/client/certificate.rs b/src/client/certificate.rs new file mode 100644 index 00000000..9bb37f65 --- /dev/null +++ b/src/client/certificate.rs @@ -0,0 +1,85 @@ +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; +use client::{DEVELOPMENT, PRODUCTION}; + +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. +/// +/// The response for `push` is asynchorous for better throughput. +/// +/// # Example +/// ```no_run +/// # extern crate apns2; +/// # fn main() { +/// use apns2::client::CertificateClient; +/// 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 mut cert_file = File::open("/path/to/certificate.pem").unwrap(); +/// let mut key_file = File::open("/path/to/key.pem").unwrap(); +/// +/// 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, "default", Some(1u32), None, None); +/// let options = NotificationOptions { ..Default::default() }; +/// let request = client.push(Notification::new(payload, "apple_device_token", options)); +/// +/// // Block here to get the response. +/// let response = request.recv_timeout(Duration::from_millis(2000)); +/// +/// println!("{:?}", response); +/// # } +/// ``` +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 { + 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 = 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(); + 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..e694a966 --- /dev/null +++ b/src/client/error.rs @@ -0,0 +1,39 @@ +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 { + ClientConnectError(String), + SslError(String) +} + +impl From for ProviderError { + fn from(e: SslError) -> ProviderError { + ProviderError::SslError(format!("Error generating an SSL context: {}", e.description())) + } +} + +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 { + "APNs connection failed" + } + + fn cause(&self) -> Option<&Error> { + None + } +} + +impl fmt::Display for ProviderError { + 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..6247666f --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,17 @@ +//! 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; +mod error; +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 new file mode 100644 index 00000000..067b77de --- /dev/null +++ b/src/client/response.rs @@ -0,0 +1,355 @@ +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 { + /// 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, +} + +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 + 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 + 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 } + } + + /// 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(); + + 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..0484f412 --- /dev/null +++ b/src/client/token.rs @@ -0,0 +1,78 @@ +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 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 using either `APNSToken` or an own implementation. +/// The same connection can be used to send notifications to multiple applications. +/// +/// 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::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(); +/// +/// // 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, "default", Some(1u32), 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); +/// # } +/// ``` +impl TokenClient { + /// 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 = 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); + let body = notification.payload.to_string().into_bytes(); + + headers.push(create_header("authorization", format!("bearer {}", apns_token))); + + let request = self.client.post(&path, headers.as_slice(), body); + + ProviderResponse::new(request, precise_time_ns()) + } +} 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 4261c13c..a9147194 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,23 @@ +//! 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; 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 response; - -pub use provider::Provider; -pub use notification::{Notification, NotificationOptions}; -pub use payload::{Payload, APS, APSAlert, APSLocalizedAlert}; -pub use device_token::DeviceToken; -pub use response::{Response, APNSStatus, APNSError}; +pub mod apns_token; diff --git a/src/notification.rs b/src/notification.rs index 66f4c945..440ebf1c 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -1,20 +1,22 @@ +//! 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>, } 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, @@ -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>, } diff --git a/src/payload.rs b/src/payload.rs index 9ade2aa4..396f1a11 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -1,31 +1,26 @@ -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 { + /// The standard APNS payload data. + pub aps: APS, + + /// Custom data to be handled by the app. + pub custom: Option, } -impl<'a> Payload<'a> { - pub fn new(alert: APSAlert, badge: Option, sound: S) -> Payload<'a> - where S: Into> - { - Payload { - aps: APS { - alert: Some(alert), - badge: badge, - sound: Some(sound.into()), - content_available: None, - category: None, - }, - } - } +pub struct CustomData { + /// The JSON root key for app specific custom data. + pub key: String, - pub fn new_action_notification(alert: APSAlert, badge: Option, sound: S, category: S) -> Payload<'a> - where S: Into> + /// The custom data. + pub body: Json, +} + +impl Payload { + pub fn new(alert: APSAlert, sound: S, badge: Option, category: Option, + custom_data: Option) -> Payload + where S: Into { Payload { aps: APS { @@ -33,12 +28,13 @@ impl<'a> Payload<'a> { badge: badge, sound: Some(sound.into()), content_available: None, - category: Some(category.into()), + category: category, }, + custom: custom_data, } } - pub fn new_silent_notification() -> Payload<'a> { + pub fn new_silent_notification(custom_data: Option) -> Payload { Payload { aps: APS { alert: None, @@ -47,6 +43,7 @@ impl<'a> Payload<'a> { content_available: Some(1), category: None, }, + custom: custom_data, } } @@ -59,10 +56,15 @@ impl<'a> Payload<'a> { } } -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()); + + if let Some(ref custom) = self.custom { + d.insert(custom.key.to_string(), custom.body.clone()); + } + Json::Object(d) } } @@ -71,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, @@ -81,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 { @@ -142,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. @@ -155,25 +157,44 @@ 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, - }); - 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 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); } + + 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()); + } + Json::Object(d) } } diff --git a/src/provider.rs b/src/provider.rs deleted file mode 100644 index bfd4cf5f..00000000 --- a/src/provider.rs +++ /dev/null @@ -1,185 +0,0 @@ -use notification::*; -use response::*; -// Time and serialization -use time::{Tm, Timespec, at}; -use rustc_serialize::json::*; -// Standard lib -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; - -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_options(SSL_OP_NO_COMPRESSION); - ctx.set_alpn_protocols(ALPN_PROTOCOLS); - ctx.set_npn_protocols(ALPN_PROTOCOLS); - - let connector = TlsConnector::with_context(host, &ctx); - let client = Client::with_connector(connector).unwrap(); - - Provider { - client: client, - } - } - - pub fn push(&self, notification: Notification, handler: F) - where F: Send + 'static + FnOnce(Result) - { - 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)); - } - - 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 { - 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()) - } - - 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 deleted file mode 100644 index f42b29f3..00000000 --- a/src/response.rs +++ /dev/null @@ -1,132 +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. - - 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, -}