Change APNS multiple certs handling

This commit is contained in:
wangmengyan95
2016-02-11 02:13:23 -08:00
parent 06b1ee2362
commit 273a20767b
6 changed files with 505 additions and 253 deletions

View File

@@ -8,37 +8,77 @@ const apn = require('apn');
/**
* Create a new connection to the APN service.
* @constructor
* @param {Object} args Arguments to config APNS connection
* @param {String} args.cert The filename of the connection certificate to load from disk, default is cert.pem
* @param {String} args.key The filename of the connection key to load from disk, default is key.pem
* @param {Object|Array} args An argument or a list of arguments to config APNS connection
* @param {String} args.cert The filename of the connection certificate to load from disk
* @param {String} args.key The filename of the connection key to load from disk
* @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key
* @param {String} args.passphrase The passphrase for the connection key, if required
* @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(args) {
this.sender = new apn.connection(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');
}
this.sender.on('connected', function() {
console.log('APNS Connected');
});
this.sender.on('transmissionError', function(errCode, notification, device) {
console.error('APNS Notification caused error: ' + errCode + ' for device ', device, notification);
// TODO: For error caseud by invalid deviceToken, we should mark those installations.
});
this.sender.on("timeout", function () {
console.log("APNS Connection Timeout");
});
this.sender.on("disconnected", function() {
console.log("APNS Disconnected");
});
this.sender.on("socketError", console.error);
this.sender.on("transmitted", function(notification, device) {
console.log("APNS Notification transmitted to:" + device.token.toString("hex"));
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);
}
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);
});
conn.on('transmissionError', (errCode, notification, apnDevice) => {
handleTransmissionError(this.conns, errCode, notification, apnDevice);
});
conn.on('timeout', () => {
console.log('APNS Connection %d Timeout', conn.index);
});
conn.on('disconnected', () => {
console.log('APNS Connection %d Disconnected', conn.index);
});
conn.on('socketError', () => {
console.log('APNS Connection %d Socket Error', conn.index);
});
conn.on('transmitted', function(notification, device) {
console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex'));
});
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;
}
}
/**
@@ -51,23 +91,84 @@ APNS.prototype.send = function(data, devices) {
let coreData = data.data;
let expirationTime = data['expiration_time'];
let notification = generateNotification(coreData, expirationTime);
let deviceTokens = [];
for (let device of devices) {
deviceTokens.push(device.deviceToken);
let qualifiedConnIndexs = chooseConns(this.conns, device);
// We can not find a valid conn, just ignore this device
if (qualifiedConnIndexs.length == 0) {
continue;
}
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;
}
conn.pushNotification(notification, apnDevice);
}
this.sender.pushNotification(notification, deviceTokens);
// TODO: pushNotification will push the notification to apn's queue.
// We do not handle error in V1, we just relies apn to auto retry and send the
// notifications.
return Parse.Promise.as();
}
function handleTransmissionError(conns, errCode, notification, apnDevice) {
console.error('APNS Notification caused error: ' + errCode + ' for device ', apnDevice, notification);
// 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
}
// 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) {
console.log('APNS can not find vaild connection for %j', apnDevice.token);
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;
}
/**
* Generate the apns notification from the data we get from api request.
* @param {Object} coreData The data field under api request body
* @returns {Object} A apns notification
*/
let generateNotification = function(coreData, expirationTime) {
function generateNotification(coreData, expirationTime) {
let notification = new apn.notification();
let payload = {};
for (let key in coreData) {
@@ -101,5 +202,7 @@ let generateNotification = function(coreData, expirationTime) {
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
APNS.generateNotification = generateNotification;
APNS.chooseConns = chooseConns;
APNS.handleTransmissionError = handleTransmissionError;
}
module.exports = APNS;

View File

@@ -9,12 +9,7 @@ const APNS = require('../../APNS');
function ParsePushAdapter(pushConfig) {
this.validPushTypes = ['ios', 'android'];
this.senders = {};
// Initialize senders
for (let validPushType of this.validPushTypes) {
this.senders[validPushType] = [];
}
this.senderMap = {};
pushConfig = pushConfig || {};
let pushTypes = Object.keys(pushConfig);
@@ -23,47 +18,17 @@ function ParsePushAdapter(pushConfig) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push to ' + pushTypes + ' is not supported');
}
let typePushConfig = pushConfig[pushType];
let senderArgs = [];
// Since for ios, there maybe multiple cert/key pairs,
// typePushConfig can be an array.
if (Array.isArray(typePushConfig)) {
senderArgs = senderArgs.concat(typePushConfig);
} else if (typeof typePushConfig === 'object') {
senderArgs.push(typePushConfig);
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push Configuration is invalid');
}
for (let senderArg of senderArgs) {
let sender;
switch (pushType) {
case 'ios':
sender = new APNS(senderArg);
break;
case 'android':
sender = new GCM(senderArg);
break;
}
this.senders[pushType].push(sender);
switch (pushType) {
case 'ios':
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
break;
case 'android':
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
break;
}
}
}
/**
* Get an array of push senders based on the push type.
* @param {String} The push type
* @returns {Array|Undefined} An array of push senders
*/
ParsePushAdapter.prototype.getPushSenders = function(pushType) {
if (!this.senders[pushType]) {
console.log('No push sender for push type %s', pushType);
return [];
}
return this.senders[pushType];
}
/**
* Get an array of valid push types.
* @returns {Array} An array of valid push types
@@ -76,24 +41,18 @@ ParsePushAdapter.prototype.send = function(data, installations) {
let deviceMap = classifyInstallation(installations, this.validPushTypes);
let sendPromises = [];
for (let pushType in deviceMap) {
let senders = this.getPushSenders(pushType);
// Since ios have dev/prod cert, a push type may have multiple senders
for (let sender of senders) {
let devices = deviceMap[pushType];
if (!sender || devices.length == 0) {
continue;
}
// For android, we can only have 1000 recepients per send
let chunkDevices = sliceDevices(pushType, devices, GCM.GCMRegistrationTokensMax);
for (let chunkDevice of chunkDevices) {
sendPromises.push(sender.send(data, chunkDevice));
}
let sender = this.senderMap[pushType];
if (!sender) {
console.log('Can not find sender for push type %s, %j', pushType, data);
continue;
}
let devices = deviceMap[pushType];
sendPromises.push(sender.send(data, devices));
}
return Parse.Promise.when(sendPromises);
}
/**
/**g
* Classify the device token of installations based on its device type.
* @param {Object} installations An array of installations
* @param {Array} validPushTypes An array of valid push types(string)
@@ -113,7 +72,8 @@ function classifyInstallation(installations, validPushTypes) {
let pushType = installation.deviceType;
if (deviceMap[pushType]) {
deviceMap[pushType].push({
deviceToken: installation.deviceToken
deviceToken: installation.deviceToken,
appIdentifier: installation.appIdentifier
});
} else {
console.log('Unknown push type from installation %j', installation);
@@ -122,26 +82,7 @@ function classifyInstallation(installations, validPushTypes) {
return deviceMap;
}
/**
* Slice a list of devices to several list of devices with fixed chunk size.
* @param {String} pushType The push type of the given device tokens
* @param {Array} devices An array of devices
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
*/
function sliceDevices(pushType, devices, chunkSize) {
if (pushType !== 'android') {
return [devices];
}
let chunkDevices = [];
while (devices.length > 0) {
chunkDevices.push(devices.splice(0, chunkSize));
}
return chunkDevices;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
ParsePushAdapter.classifyInstallation = classifyInstallation;
ParsePushAdapter.sliceDevices = sliceDevices;
}
module.exports = ParsePushAdapter;

View File

@@ -8,6 +8,10 @@ const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
const GCMRegistrationTokensMax = 1000;
function GCM(args) {
if (typeof args !== 'object' || !args.apiKey) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'GCM Configuration is invalid');
}
this.sender = new gcm.Sender(args.apiKey);
}
@@ -18,10 +22,6 @@ function GCM(args) {
* @returns {Object} A promise which is resolved after we get results from gcm
*/
GCM.prototype.send = function(data, devices) {
if (devices.length >= GCMRegistrationTokensMax) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Too many registration tokens for a GCM request.');
}
let pushId = randomstring.generate({
length: 10,
charset: 'alphanumeric'
@@ -37,21 +37,30 @@ GCM.prototype.send = function(data, devices) {
let gcmPayload = generateGCMPayload(data.data, pushId, timeStamp, expirationTime);
// Make and send gcm request
let message = new gcm.Message(gcmPayload);
let promise = new Parse.Promise();
let registrationTokens = []
for (let device of devices) {
registrationTokens.push(device.deviceToken);
}
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
// TODO: Use the response from gcm to generate and save push report
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
console.log('GCM request and response %j', {
request: message,
response: response
let sendPromises = [];
// For android, we can only have 1000 recepients per send, so we need to slice devices to
// chunk if necessary
let chunkDevices = sliceDevices(devices, GCMRegistrationTokensMax);
for (let chunkDevice of chunkDevices) {
let sendPromise = new Parse.Promise();
let registrationTokens = []
for (let device of chunkDevice) {
registrationTokens.push(device.deviceToken);
}
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
// TODO: Use the response from gcm to generate and save push report
// TODO: If gcm returns some deviceTokens are invalid, set tombstone for the installation
console.log('GCM request and response %j', {
request: message,
response: response
});
sendPromise.resolve();
});
promise.resolve();
});
return promise;
sendPromises.push(sendPromise);
}
return Parse.Promise.when(sendPromises);
}
/**
@@ -62,7 +71,7 @@ GCM.prototype.send = function(data, devices) {
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
* @returns {Object} A promise which is resolved after we get results from gcm
*/
let generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) {
let payloadData = {
'time': new Date(timeStamp).toISOString(),
'push_id': pushId,
@@ -86,9 +95,22 @@ let generateGCMPayload = function(coreData, pushId, timeStamp, expirationTime) {
return payload;
}
GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
/**
* Slice a list of devices to several list of devices with fixed chunk size.
* @param {Array} devices An array of devices
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
*/
function sliceDevices(devices, chunkSize) {
let chunkDevices = [];
while (devices.length > 0) {
chunkDevices.push(devices.splice(0, chunkSize));
}
return chunkDevices;
}
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
GCM.generateGCMPayload = generateGCMPayload;
GCM.sliceDevices = sliceDevices;
}
module.exports = GCM;