Features
- chainable message builder
- persistent connection management
- apn feedback service integration
- feature complete mock agents for local-only testing/development
Starting Resources
- Delivering iOS Push Notifications with Node.js: In-depth getting started with push notifications and
apnagent
tutorial.
Related Projects
- apnagent-ios: Tiny iOS application for use with the example(s) and live tests.
- apnagent-playground: Extended examples for connecting, error mitigation and available message types.
Quick Start Guide
apn agent is a Node.js module that facilitates sending notifications via the Apple Push Notification (APN) service. It is production ready and includes a number of a features that make it easy for developers to get running quickly. It also contains several mock helpers which can assist in testing an application or provide feature parity during development.
Installation
apnagent
package is available through npm for Node.js v0.8.x
and v0.10.x
.
npm install apnagent
Generate Certificates
Before using apn agent you will need certificates to establish a secure connection with the APNs or Feedback service. The following steps will walk you through generating these certificates in all of the formats that apn agent supports.
1. Application: Log in the iOS Provision Portal from the Apple Developer website. Select "App IDs" from the left side menu, then "configure" next to the application you wish generate certificates for.
2. Enable: [Screenshot] If it is not already enabled, check the box for "Enable for Apple Push Notification server".
3. Configure: [Screenshot] Select "Configure" for the environment you want to generate certificates for. Follow the wizard's instructions for generating a CSR file. Make sure to save the CSR file in a safe place so that you can reuse it for future certificates.
4. Generate: [Screenshot] After you have uploaded your CSR it will generate a "Apple Push Notification service SSL Certificate". Download it to a safe place.
5. Import: [Screenshot] Once downloaded, locate the file with Finder and double-click to import the certificate into "Keychain Access". Use the filters on the left to locate your newly import certificate if it is not visible. It will be listed under the "login" keychain in the "Certificates" category. Once located, right-click and select "Export".
6. Export: When the export dialog appears be sure that the "File Format" is set at ".p12". Name the file according
to the environment, such as apnagent-dev.p12
and save it to your project directory. You will then be prompted
to enter a password to secure the exported item. This is optional, however if you do specify one you will
need to configure apn agent's classes to use that "passphrase". You will also be prompted for your login password.
7. Convert (optional): apn agent's classes support ".p12" files through the pfx
configuration options to authenticate
connections. The last step is optional but instructs on how to turn a ".p12" file into a "key" and "cert"
pair (".pem" format).
Locate your ".p12" file in a terminal session and execute the following commands.
openssl pkcs12 -clcerts -nokeys -out apnagent-dev-cert.pem -in apnagent-dev.p12
openssl pkcs12 -nocerts -out apnagent-dev-key.pem -in apnagent-dev.p12
The first will generate a ".pem" file for use as the cert
file. The second will generate
a ".pem" file for use as the key
file. The second requires a password be entered to secure
the key. This can be removed by executing the following command.
openssl rsa -in apnagent-dev-key.pem -out apnagent-dev-key.pem
Agent Guide
- Jump to: Agent API
The Agent
class is used to provide a consistent connection to the APN service. In some cases
this documentation will refer to this Agent as the "live agent" to differentiate itself from the
MockAgent
which has feature parity but does not make a real connection to the APN service.
Methodology
Apple insists that a connection "always" be established with the APN service even if there will
be long periods without outbound messages. The Agent
will maintain this connection and reconnect
when it is needed.
The MockAgent
implements all of the same methods and components as the Agent
but it does not
connect to the APN service; It does not need to connect to any service. By using a custom Node.js
Stream the MockAgent
can simulate an outbound socket without
needing any network resources. Keeping things simple is incredibly adventageous for development
and test environment, or applications that are deployed through continous integration.
One oddity of the APN service is that if an message error occurs on the service side the connection will respond with the error referencing which message it had a problem parsing and then disconnect. Any messages that were sent after the errored message will not be parsed by APNs and will need to be resent. apn agent uses a number of components to handle this scenario and ensure all messages are sent without additional intervention from the user.
Configuration
All settings and event are documented later on a per-component basis.
Settings
apn agent uses methods to provide granular setting configuraton. Value based Settings can be
modified using .set()
and boolean based settings cen be modified using .enable()
, or .disable()
.
Furthermore, methods that modify settings are chainable. For example:
agent
.set('cert file', join(__dirname, 'certs/cert.pem'))
.set('key file', join(__dirname, 'certs/key.pem'))
.enable('sandbox');
Core contributors to the
apnagent
module should familiarize themselves with the facet module.
Events
Like most Node.js modules, apn agent's Agent
and MockAgent
classes implement the Node.js-style
EventEmitter paradigm. Event listeners can be added with .on
or .addListener
, removed with .off
or .removeListener
and emitted using .emit
.
agent.on('message:error', function (err, msg) {
// We'll discuss error mitigation later.
});
Core contributors to the
apnagent
module should familiarize themselves with the drip module'sEnhancedEmitter
class.
Gateway Component
The gateway component represents the connection to the APN service. For the live agent it is Node.js tls socket. For the mock agent it is a writable stream. A new gateway is constructed on every connect or reconnect attempt.
Core contributors to the
apnagent
module should familiarize themselves with the lotus module.
Settings
key file, cert file, ca file, pfx file {String} - Set security credentials for the APN service
connection by filename. Must use full path. These options are ignored by the MockAgent
.
key, cert, ca, pfx {Buffer} - Set the raw security credentials for connect to the APN service.
These options are ignored by the MockAgent
.
passphrase {String} - For use with certificates if they are secured with a password. This option
ignored by the MockAgent
.
sandbox {Boolean} (default: false) Should agent connect to the APN sandbox gateway. This option
is ignored by the MockAgent
.
reconnect delay {Number|String} (default: 3s) - Milliseconds after a disconnect that a reconnect
should be attempted. Can also be set as string using s
, m
, h
for seconds, minutes, hours)
respectively.
Events
gateway:connect - Emitted every time a gateway connection is established. Since Apple closes the connection when it cannot process a message, this event may be emitted numerous times in an applications life-cycle.
gateway:reconnect - Emitted after a connection has been re-established after Apple had forcefully closed the connection. May be emitted numerous times in an application's life-cycle.
gateway:error (err) - Emitted if there is a client-side error with gateway. This includes authorization
errors or TLS socket errors. An authorization error will be an instance of GatewayAuthorizationError
.
Queue Component
The queue component stores outgoing messages until the socket is ready to written. Messages are serialized before being placed in the queue to minimize APNs-side errors.
User created messages are placed at the end of the queue such that it will operate with a first-in/first-out strategy. In the event of a connection error, messages that need to be resent will be placed in the beginning of the queue to best mimic the user's intended send order.
Core contributors to the
apnagent
module should familiarize themselves with the breeze-queue module.
Events
queue:drain - Emitted after the queue has process all messages through the gateway. This does not guarantee that all messages sent have been processed by the APN service or received by the device.
Cache Component
The cache component maintains a limited ordered history of all outgoing messages should they need to be resent. If the APN service deems a message invalid it will not process any further message, respond with the unique ID and error code for the message that failed, then disconnect. The agent will use the cache to requeue messages that were sent after the failed message.
To control memory usage the cache employs a time-to-live (ttl) mechanism to remove items which are beyond a
certain age and therefor have presumably been processed successfully by Apple. The default value for this
ttl is 10 minutes but it can be modified by configuring the cache ttl
setting on the agent.
Settings
cache ttl {Number|String} (default: 10m) - The minimum number of milliseconds that a message should be present in the queue before
considered a success and removed. Can also be set as string using s
, m
, h
for seconds, minutes, hours) respectively.
Sending Messages
- Jump to: Message Builder API
To create a new message invoke the .createMessage
method from an agent. This will return a
constructed message that can be modified via chainable methods and then sent.
agent.createMessage()
.device(token)
.alert('Hello Universe')
.send();
Message Expiration
- Jump to: Message#expires()
It is unnecissary to cover in this section all of the chainable methods that can be used to
custom your outbound messages, but one that deserves a bit of attention is handling message
expiration. By default all messages have an expiration value of 0
(zero). This indicates to
Apple that if you cannot deliver the message immediately after processing then it should be
discarded. For example, if the default is kept then messages to devices which are off or out
of service range would not be delivered.
Though useful in some application contexts there are many cases where it is not. A social networking application may wish to deliver at any time or a calendar application for an event that occurs within the next hour. For this you may modify the default expiration value or change it on a per-message basis.
// set default to one day
agent.set('expires', '1d');
// use custom for 1 hour
agent.createMessage()
.device(token)
.alert('New Event @ 4pm')
.expires('1h')
.send();
// set custom no expiration
agent.createMessage()
.device(token)
.alert('Event happening now!')
.expires(0)
.send();
Send Confirmation
- Jump to: Error Mitigation
As you might have noticed in the above .createMessage()
examples a callback was not specified
for the .send()
method though the Message#send() api
allows for one to be set. Since the APN service does not provide confirmation that every
message has been successfully parsed managing a callback flow can be tricky. Here are rules governing
when the message .send(cb)
callback will be invoked.
When
.send()
is invoked it immediately attempts the serialize the message into a JSON payload. It then validates the payload based on a series of rules to mimimize APN-side errors. Issues such as a message missing a token or being to long to send will be captured as a newMessageSerializationError
. If a.send()
callback exists it will be invoked with the error as the first argument. This error will also be emitted on theagent
as amessage:error
event.The message will then be placed in the queue and wait for itself to be sent. Once the message has been processed by the queue and been successfully written to and flushed from the gateway the callback will then be invoked without any arguments.
If the message cannot be parsed on the APN-side, Apple will inform the
agent
. Theagent
will construct anGatewayMessageError
based on Apple's response. Theagent
will also attempt to reconstruct the original message if it is still present in the cache. The error and possible message will then be emitted on theagent
as themessage:error
event. The callback WILL NOT be invoked again.
Since any error occurance will be emitted as a message:error
it may not always be necissary to specify a .send()
callback unless your flow needs to wait until the message has confirmed valid serialization. If possible, keep your
code DRY and use the message:error
event instead.
Mock Agent Enhancement
One unfortunate downside to APNs implementation is that it cannot be determined when a message has been
successfully parsed by Apple, only when a message has failed. To minimize the occurance of APN-side
errors each message passes through a set of validation rules prior to being transferred over the wire.
As a result of this implementation the reasonable assumption is that any message that is transferred
over the wire when using the MockAgent
class can be assumed to be a successful message.
The only difference between the Agent
and MockAgent
implementations is that when data is written
to the MockAgent
's gateway, that data will be decoded into a JSON object that represents the original
payload. This object will then be emitted on the agent
as the mock:message
event. Users can listen
for this event during tests to confirm that their applications have successfully implemented sending
mechanisms.
Settings
expires {String|Number} (default: 0) - Set this value on the agent
to modify the default message expiration value.
Can also be set as string using s
, m
, h
, d
for seconds, minutes, hours), days respectively.
agent.set('expires', '1d');
Events
message:error (err[, msg]) - Listen for this event on the agent
for when a message cannot be sent or
has errored after sending. Visit Error Mitigation for a complex example of how to work
with the error emitted.
mock:message (obj) - Listen for this event on a constructed MockAgent
. Will be emitted when the
mock gateway stream has decoded a message that was sent over the "wire".
Full Example
The following demonstrates the intended usage of both an Agent
and MockAgent
within the context
of an express.js based RESTful application. Since we can use an Agent
and
MockAgent
interchangably, we can configure this application to use different agents depending on
the active NODE_ENV
.
/**
* Example dependencies / constants
*/
var apnagent = require('apnagent')
, express = require('express')
, join = require('path').join
, port = process.env.PORT || 8080;
/**
* Create application
*/
var app = express()
, server = require('http').createServer(app);
/**
* Configure Express
*/
app.configure(function () {
app.use(express.bodyParser());
});
/**
* Use a MockAgent for dev/test envs
*/
app.configure('development', 'test', function () {
var agent = new apnagent.MockAgent();
// no configuration needed
// mount to app
app
.set('apn', agent)
.set('apn-env', 'mock');
});
/**
* Usa a live Agent with sandbox certificates
* for our staging environment.
*/
app.configure('staging', function () {
var agent = new apnagent.Agent();
// configure agent
agent
.set('cert file', join(__dirname, 'certs/apn/dev-cert.pem'))
.set('key file', join(__dirname, 'certs/apn/dev-key.pem'))
.enable('sandbox');
// mount to app
app
.set('apn', agent)
.set('apn-env', 'live-sandbox');
});
/**
* Use a live Agent with production certificates
* for our production environment.
*/
app.configure('production', function () {
var agent = new apnagent.Agent();
// configure agent
agent
.set('cert file', join(__dirname, 'certs/apn/prod-cert.pem'))
.set('key file', join(__dirname, 'certs/apn/prod-key.pem'));
// mount to app
app
.set('apn', agent)
.set('apn-env', 'live-production');
});
/**
* Set our environment independant configuration
* and event listeners.
*/
app.configure(function () {
var agent = app.get('apn')
, env = app.get('apn-env');
// common settings
agent
.set('expires', '1d')
.set('reconnect delay', '1s')
.set('cache ttl', '30m');
// see error mitigation section
agent.on('message:error', function (err, msg) {
// ...
});
// connect needed to start message processing
agent.connect(function (err) {
if (err) throw err;
console.log('[%s] apn agent running', env);
});
});
/**
* Sample endpoint
*/
app.post('/apn', function (req, res) {
var agent = app.get('apn')
, alert = req.body.alert
, token = req.body.token;
agent.createMessage()
.device(token)
.alert(alert)
.send(function (err) {
// handle apnagent custom errors
if (err && err.toJSON) {
res.json(400, { error: err.toJSON(false) });
}
// handle anything else (not likely)
else if (err) {
res.json(400, { error: err.message });
}
// it was a success
else {
res.json({ success: true });
}
});
});
/**
* Start server
*/
server.listen(port, function () {
console.log('http started on port %d', server.address().port);
});
Agent API
This API section covers both the Agent
and MockAgent
classes. The architecture was developed to provide
feature parity for not just available methods but the
events emitted and processing methodology. Any significant
differences have already been outlined in the Agent Guide.
.createMessage ([encoding])
Creates a message that can be further modified through chaining. Do not provide arguments unless you know what you are doing.
.send (message[, callback])
Serialize the message and catch any validation errors
that might occur. If validation passes add the message
to the send queue. Messages can also be sent by invoking
the message instance's .send()
method.
var message = agent.createMessage();
message
.device(token)
.alert('Hello Universe');
agent.send(message);
.connect ([ callback ])
Open an active gateway connection. Once the connection is established the outgoing message queue will begin to process items.
.close ([ callback ])
Close the active gateway connection or cancel further reconnect attempts. If the queue is currently processing a message it will wait for the current message to finish before closing.
Apple recommends that a connection always remains open even when there are no messages to process. Production deployments should use this sparingly.
Message Builder API
A message encapsulates all data points that will be encoded and sent through the wire to APNS. The message builder is a chainable API that provides full feature coverage of the Apple Push Notification specifications.
The preferred method of composing messages is directly
from a constructed Agent
or MockAgent
.
var msg = agent.createMessage();
.alert (key, value)
Sets variables to be included in the alert
dictionary
for the aps
portion of the payload. If you wish to set
a singlular message, set the body
key. You may also set
any of the other values outlined in the APNS documentation
and the codecs will optimize the payload format for delivery.
Allowed keys:
body
action-loc-key
loc-key
loc-args
launch-image
// just set body
msg.alert('Hello Universe');
// set multiple values
msg.alert({
body: 'Hello Universe'
, 'launch-image': 'notif.png'
});
// chainable
msg
.alert('Hello Universe')
.alert('launch-image', 'notif.png');
.set (key, value)
Set extra key values that will be incuded
as part of the payload. aps
is reserved by
Apple and enc
is reserved by apnagent.
Is a key/value you pair is provided it will be set. If an object is provided, all data points will be merged into the current payload.
// single value
msg.set('key', 'value');
// multiple values
msg.set({
key1: 'value1'
, key2: 'value2'
});
// or chainable
msg
.set('key1', 'value1')
.set('key2', 'value2');
.device (token)
Set the device that this message is to be delivered
to. Device can be provided as a string or buffer. If
provided as a string, it will be sanitized of spaces
and extra characters (such as <
and >
).
msg.device('a1b2c3');
msg.device('<a1b2c3>');
.expires (time)
Set the message expiration date when being used
with the enhanced codec. The default value is 0
which
will indicate to Apple to only attempt to deliver
the message once.
Should be provided as the number of ms until expiration
or as a string that can be converted, such as 1d
.
// set to specific time in future
msg.expires('30m'); // 30 minutes
msg.expires('1d'); // 1 day
// reset to default value
msg.expires(0);
msg.expires(true);
.sound (file)
Set the sound file to be played when this message is delivered if the app is closed.
msg.sound('bingbong.aiff');
.send (cb)
Send the message through the connected agent. The cb
function will be invoked with an error if there is
a problem serializing the message for transport.
If there are no serialization errors, the callback
will be invoked when the message has been flushed
through the socket. This does NOT mean the message
has be received by the device or that Apple has
accepted the message. If Apple has a problem with
the message it will be emitted on the agent's
message:error
event.
msg.send(function (err) {
if (err) {
// handle it
}
});
.use (fn)
.connect ([ callback ])
.close ([ callback ])
Close the active gateway connection or cancel further reconnect attempts. If the queue is currently processing a message it will wait for the current message to finish before closing.
Apple recommends that a connection always remains open even when there are no messages to process. Production deployments should use this sparingly.
Device API
A small constructor to easily encapsulate a device token so it can be passed around and converted to whatever type is needed for given scenario.
If a Device is constructed without parameters, a
token can be assigned later by setting the .token
property to either a string or buffer.
The most common usage for the Device
constructor
is to sanitize a device token for storage in a database.
var Device = require('apnagent').Device
, device = new Device('<a1b56d2c 08f621d8 7060da2b>');
.toString ()
Convert the stored device token to a string. The string will be sanitized and thus not include spaces or extra characters.
var str = device.toString();
.equal (device)
Compare the stored device token to another device, string, or buffer. Will also return false if both Devices do not have tokens associated with them.
// testing string
device.equal('a1b56d2c08f621d87060da2b').should.be.true;
// testing another device
var dev2 = new Device('feedface');
device.equal(dev2).should.be.false;
Resources
Dependencies
The following dependencies play a significant role in the apn agent architecture. This is not a complete list but a recommended research list for those interested in contributing to apn agent.
breeze-queue
- @github qualiancy/breeze-queue
- @npm breeze-queue
drip
- @github qualiancy/drip
- @npm drip
facet
- @github qualiancy/facet
- @npm facet
lotus
- @github qualiancy/lotus
- @npm lotus
tea-error
- @github qualiancy/tea-error
- @npm tea-error
Testing
- @see Chai Assertion Library
- @see Mocha Test Runner
Tests are writting in Mocha using the Chai should
BDD assertion library. Clone this repo and install
development dependencies using npm install
.
Normal Testing
The normal testing life-cyle will test all features that do not require a live connection to the APN service. All tests that require a live connection will have a status of "pending". These are the tests that are run by travis-ci.
make test
Live Testing
In order to perform the live tests you will need to provide your key, cert, and device token. These can be provided within the test folder. Here is the expected folder structure and file names:
tests
├── bootstrap
│ └── ...
├── certs
│ ├── apnagent-cert.pem
│ ├── apnagent-key-noenc.pem
│ └── device.txt
├── common
│ └── ...
├── *.js
The key/cert pair should be for a sandbox based application. Furthermore, you should have the application installed on you device. If you do not have one then use apnagent-ios.
make test-live
License
(The MIT License)