reverts to use binary APNs

This commit is contained in:
Florent Vilmart
2016-03-14 08:15:38 -04:00
parent 7c387e1ee9
commit 120f23c791
4 changed files with 383 additions and 176 deletions

View File

@@ -1,30 +1,9 @@
"use strict";
const Parse = require('parse/node').Parse;
const http = require('http2');
const fs = require('fs');
const path = require('path');
const urlParse = require('url').parse;
const DEV_PUSH_SERVER = 'api.development.push.apple.com';
const PROD_PUSH_SERVER = 'api.push.apple.com';
const createRequestOptions = (opts, device, body) => {
let domain = opts.production === true ? PROD_PUSH_SERVER : DEV_PUSH_SERVER;
var options = urlParse(`https://${domain}/3/device/${device.deviceToken}`);
options.method = 'POST';
options.headers = {
'apns-expiration': opts.expiration || 0,
'apns-priority': opts.priority || 10,
'apns-topic': opts.bundleId || opts['apns-topic'],
'content-length': body.length
};
options.key = opts.key;
options.cert = opts.cert;
options.pfx = opts.pfx;
options.passphrase = opts.passphrase;
return Object.assign({}, options);
}
// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1,
// but probably we will replace it in the future.
const apn = require('apn');
/**
* Create a new connection to the APN service.
@@ -37,115 +16,170 @@ const createRequestOptions = (opts, device, body) => {
* @param {String} args.bundleId The bundleId for cert
* @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
*/
function APNS(options) {
if (!Array.isArray(options)) {
options = [options];
function APNS(args) {
// Since for ios, there maybe multiple cert/key pairs,
// typePushConfig can be an array.
let apnsArgsList = [];
if (Array.isArray(args)) {
apnsArgsList = apnsArgsList.concat(args);
} else if (typeof args === 'object') {
apnsArgsList.push(args);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'APNS Configuration is invalid');
}
let agents = {};
let optionsByBundle = options.reduce((memo, option) => {
try {
if (option.key && option.cert) {
option.key = fs.readFileSync(option.key);
option.cert = fs.readFileSync(option.cert);
} else if (option.pfx) {
option.pfx = fs.readFileSync(option.pfx);
} else {
throw 'Either cert AND key, OR pfx is required'
}
} catch(e) {
if (!process.env.NODE_ENV == 'test' || options.enforceCertificates) {
throw e;
}
this.conns = [];
for (let apnsArgs of apnsArgsList) {
let conn = new apn.Connection(apnsArgs);
if (!apnsArgs.bundleId) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'BundleId is mssing for %j', apnsArgs);
}
option.agent = new http.Agent({
key: option.key,
cert: option.cert,
pfx: option.pfx,
passphrase: option.passphrase
conn.bundleId = apnsArgs.bundleId;
// Set the priority of the conns, prod cert has higher priority
if (apnsArgs.production) {
conn.priority = 0;
} else {
conn.priority = 1;
}
// Set apns client callbacks
conn.on('connected', () => {
console.log('APNS Connection %d Connected', conn.index);
});
memo[option.bundleId] = option;
return memo;
}, {});
let getConfiguration = (bundleIdentifier) => {
let configuration;
if (bundleIdentifier) {
configuration = optionsByBundle[bundleIdentifier];
if (!configuration) {
return;
}
}
if (!configuration) {
configuration = options[0];
}
return configuration;
}
conn.on('transmissionError', (errCode, notification, apnDevice) => {
handleTransmissionError(this.conns, errCode, notification, apnDevice);
});
/**
* Send apns request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of device tokens
* @returns {Object} A promises that resolves with each notificaiton sending promise
*/
let send = function(data, devices) {
// Make sure devices are in an array
if (!Array.isArray(devices)) {
devices = [devices];
}
conn.on('timeout', () => {
console.log('APNS Connection %d Timeout', conn.index);
});
let coreData = data.data;
let expirationTime = data['expiration_time'];
let notification = generateNotification(coreData);
let notificationString = JSON.stringify(notification);
let buffer = new Buffer(notificationString);
conn.on('disconnected', () => {
console.log('APNS Connection %d Disconnected', conn.index);
});
let promises = devices.map((device) => {
return new Promise((resolve, reject) => {
let configuration = getConfiguration(device.appIdentifier);
if (!configuration) {
return Promise.reject({
status: -1,
device: device,
response: {"error": "No configuration set for that appIdentifier"},
transmitted: false
})
}
configuration = Object.assign({}, configuration, {expiration: expirationTime })
let requestOptions = createRequestOptions(configuration, device, buffer);
let req = configuration.agent.request(requestOptions, (response) => {
response.setEncoding('utf8');
var chunks = "";
response.on('data', (chunk) => {
chunks+=chunk;
});
response.on('end', () => {
let body;
try{
body = JSON.parse(chunks);
} catch (e) {
body = {};
}
resolve({ status: response.statusCode,
response: body,
headers: response.headers,
device: device,
transmitted: response.statusCode == 200 });
});
conn.on('socketError', () => {
console.log('APNS Connection %d Socket Error', conn.index);
});
conn.on('transmitted', function(notification, device) {
if (device.callback) {
device.callback({
notification: notification,
transmitted: true,
device: device
});
req.write(buffer);
req.end();
});
}
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
});
return Promise.all(promises);
this.conns.push(conn);
}
// Sort the conn based on priority ascending, high pri first
this.conns.sort((s1, s2) => {
return s1.priority - s2.priority;
});
// Set index of conns
for (let index = 0; index < this.conns.length; index++) {
this.conns[index].index = index;
}
}
/**
* Send apns request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved immediately
*/
APNS.prototype.send = function(data, devices) {
let coreData = data.data;
let expirationTime = data['expiration_time'];
let notification = generateNotification(coreData, expirationTime);
let promises = devices.map((device) => {
let qualifiedConnIndexs = chooseConns(this.conns, device);
// We can not find a valid conn, just ignore this device
if (qualifiedConnIndexs.length == 0) {
return Promise.resolve({
transmitted: false,
result: {error: 'No connection available'}
});
}
let conn = this.conns[qualifiedConnIndexs[0]];
let apnDevice = new apn.Device(device.deviceToken);
apnDevice.connIndex = qualifiedConnIndexs[0];
// Add additional appIdentifier info to apn device instance
if (device.appIdentifier) {
apnDevice.appIdentifier = device.appIdentifier;
}
return new Promise((resolve, reject) => {
apnDevice.callback = resolve;
conn.pushNotification(notification, apnDevice);
});
});
return Parse.Promise.when(promises);
}
function handleTransmissionError(conns, errCode, notification, apnDevice) {
// This means the error notification is not in the cache anymore or the recepient is missing,
// we just ignore this case
if (!notification || !apnDevice) {
return
}
return Object.freeze({
send: send,
getConfiguration: getConfiguration
})
// If currentConn can not send the push notification, we try to use the next available conn.
// Since conns is sorted by priority, the next conn means the next low pri conn.
// If there is no conn available, we give up on sending the notification to that device.
let qualifiedConnIndexs = chooseConns(conns, apnDevice);
let currentConnIndex = apnDevice.connIndex;
let newConnIndex = -1;
// Find the next element of currentConnIndex in qualifiedConnIndexs
for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) {
if (qualifiedConnIndexs[index] === currentConnIndex) {
newConnIndex = qualifiedConnIndexs[index + 1];
break;
}
}
// There is no more available conns, we give up in this case
if (newConnIndex < 0 || newConnIndex >= conns.length) {
if (apnDevice.callback) {
apnDevice.callback({
response: {error: `APNS can not find vaild connection for ${apnDevice.token}`, code: errCode},
status: errCode,
transmitted: false
});
}
return;
}
let newConn = conns[newConnIndex];
// Update device conn info
apnDevice.connIndex = newConnIndex;
// Use the new conn to send the notification
newConn.pushNotification(notification, apnDevice);
}
function chooseConns(conns, device) {
// If device does not have appIdentifier, all conns maybe proper connections.
// Otherwise we try to match the appIdentifier with bundleId
let qualifiedConns = [];
for (let index = 0; index < conns.length; index++) {
let conn = conns[index];
// If the device we need to send to does not have
// appIdentifier, any conn could be a qualified connection
if (!device.appIdentifier || device.appIdentifier === '') {
qualifiedConns.push(index);
continue;
}
if (device.appIdentifier === conn.bundleId) {
qualifiedConns.push(index);
}
}
return qualifiedConns;
}
/**
@@ -154,12 +188,12 @@ function APNS(options) {
* @returns {Object} A apns notification
*/
function generateNotification(coreData, expirationTime) {
let notification = new apn.notification();
let payload = {};
let notification = {};
for (let key in coreData) {
switch (key) {
case 'alert':
notification.alert = coreData.alert;
notification.setAlertText(coreData.alert);
break;
case 'badge':
notification.badge = coreData.badge;
@@ -168,10 +202,9 @@ function generateNotification(coreData, expirationTime) {
notification.sound = coreData.sound;
break;
case 'content-available':
notification.setNewsstandAvailable(true);
let isAvailable = coreData['content-available'] === 1;
if (isAvailable) {
notification['content-available'] = 1;
}
notification.setContentAvailable(isAvailable);
break;
case 'category':
notification.category = coreData.category;
@@ -181,11 +214,14 @@ function generateNotification(coreData, expirationTime) {
break;
}
}
payload.aps = notification;
return payload;
notification.payload = payload;
notification.expiry = expirationTime;
return notification;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
APNS.generateNotification = generateNotification;
APNS.chooseConns = chooseConns;
APNS.handleTransmissionError = handleTransmissionError;
}
module.exports = APNS;