Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from pimeys/master
JWT auth, lots of refactoring etc.
- Loading branch information
Showing
18 changed files
with
1,015 additions
and
441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String>, | ||
issued_at: Option<i64>, | ||
key_id: String, | ||
team_id: String, | ||
secret: LocalKeyPair, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub enum APNSTokenError { | ||
SignError, | ||
KeyParseError(String), | ||
KeyOpenError(String), | ||
KeyReadError(String), | ||
KeyGenerationError, | ||
KeyError, | ||
} | ||
|
||
impl From<KeyReadError> 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<S,R>(mut pk_der: R, key_id: S, team_id: S) -> Result<APNSToken, APNSTokenError> | ||
where S: Into<String>, 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<String, JsonNode> = 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<String, JsonNode> = 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 | ||
} | ||
} | ||
} |
Oops, something went wrong.