Make the apn library take a cert and a key as a string instead of as a file so i can store multiples of them. Also fixed up a bunch of js lint error, not all of them, but most of them.

This commit is contained in:
keithnlarsen 2011-12-29 19:54:21 -07:00 коммит произвёл Andrew Naylor
Родитель a3cf8b090b
Коммит a847befdff
2 изменённых файлов: 281 добавлений и 213 удалений

Просмотреть файл

@ -24,29 +24,39 @@ As a submodule of your project
### Load in the module
var apns = require('apn');
### Exported Objects
- Connection
- Notification
- Device
- Feedback
- errors
### Connecting
Create a new connection to the gateway server using a dictionary of options. The defaults are listed below:
options = { cert: 'cert.pem' /* Certificate file */
, key: 'key.pem' /* Key file */
, gateway: 'gateway.push.apple.com' /* gateway address */
, port: 2195 /* gateway port */
, enhanced: true /* enable enhanced format */
, errorCallback: undefined /* Callback when error occurs */
, cacheLength: 5 /* Notifications to cache for error purposes */
};
var apnsConnection = new apns.connection(options);
var options = {
cert: 'cert.pem', /* Certificate file */
certData: null, /* Optional: if supplied uses this instead of Certificate File */
key: 'key.pem', /* Key file */
keyData: null, /* Optional: if supplied uses this instead of Key file */
gateway: 'gateway.push.apple.com',/* gateway address */
port: 2195, /* gateway port */
enhanced: true, /* enable enhanced format */
errorCallback: undefined, /* Callback when error occurs */
cacheLength: 5 /* Number of notifications to cache for error purposes */
};
var apnsConnection = new apns.Connection(options);
### Sending a notification
To send a notification first create a `Device` object. Pass it the device token as either a hexadecimal string, or alternatively as a `Buffer` object containing the binary token, setting the second argument to `false`.
var myDevice = new apns.device(token /*, ascii=true*/);
var myDevice = new apns.Device(token /*, ascii=true*/);
Next create a notification object and set parameters. See the [payload documentation][pl] for more details
var note = new apns.notification();
var note = new apns.Notification();
note.badge = 3;
note.sound = "ping.aiff";
@ -76,15 +86,18 @@ Apple recommends checking the feedback service periodically for a list of device
Using the `Feedback` object it is possible to periodically query the server for the list. You should provide a function which will accept two arguments, the `time` returned by the server (epoch time) and a `Device` object containing the device token. You can also set the query interval in seconds. Again the default options are shown below.
options = { cert: 'cert.pem' /* Certificate file */
, key: 'key.pem' /* Key file */
, address: 'feedback.push.apple.com' /* feedback address */
, port: 2196 /* feedback port */
, feedback: false /* callback function */
, interval: 3600 /* query interval in seconds */
};
var options = {
cert: 'cert.pem', /* Certificate file */
certData: null, /* Certificate file contents */
key: 'key.pem', /* Key file */
keyData: null, /* Key file contents */
address: 'feedback.push.apple.com', /* feedback address */
port: 2196, /* feedback port */
feedback: false, /* enable feedback service, set to callback */
interval: 3600 /* interval in seconds to connect to feedback service */
};
var feedback = new apns.feedback(options);
var feedback = new apns.Feedback(options);
## Converting your APNs Certificate
@ -175,4 +188,4 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
1.0.0:
* Well I created a module; Version 0.0.0 had no code, and now it does, and it works, so that's pretty neat, right?
* Well I created a module; Version 0.0.0 had no code, and now it does, and it works, so that's pretty neat, right?

Просмотреть файл

@ -3,17 +3,17 @@ var fs = require('fs');
var Buffer = require('buffer').Buffer;
var Errors = {
'noErrorsEncountered': 0
, 'processingError': 1
, 'missingDeviceToken': 2
, 'missingTopic': 3
, 'missingPayload': 4
, 'invalidTokenSize': 5
, 'invalidTopicSize': 6
, 'invalidPayloadSize': 7
, 'invalidToken': 8
, 'none': 255
}
'noErrorsEncountered': 0,
'processingError': 1,
'missingDeviceToken': 2,
'missingTopic': 3,
'missingPayload': 4,
'invalidTokenSize': 5,
'invalidTopicSize': 6,
'invalidPayloadSize': 7,
'invalidToken': 8,
'none': 255
};
var Connection = function (optionArgs) {
var currentId = 0;
@ -25,15 +25,18 @@ var Connection = function (optionArgs) {
var socketOptions = {};
var openingSocket = false;
var writeBuffer = [];
var options = { cert: 'cert.pem' /* Certificate file */
, key: 'key.pem' /* Key file */
, gateway: 'gateway.push.apple.com' /* gateway address */
, port: 2195 /* gateway port */
, enhanced: true /* enable enhanced format */
, errorCallback: undefined /* Callback when error occurs */
, cacheLength: 5 /* Number of notifications to cache for error purposes */
};
var options = {
cert: 'cert.pem' /* Certificate file */,
certData: '',
key: 'key.pem' /* Key file */,
keyData: '',
gateway: 'gateway.push.apple.com' /* gateway address */,
port: 2195 /* gateway port */,
enhanced: true /* enable enhanced format */,
errorCallback: undefined /* Callback when error occurs */,
cacheLength: 5 /* Number of notifications to cache for error purposes */
};
if (optionArgs) {
var keys = Object.keys(options);
@ -42,77 +45,89 @@ var Connection = function (optionArgs) {
if (optionArgs[k] !== undefined) options[k] = optionArgs[k];
}
}
var readyToConnect = function () {
return (hasKey && hasCert);
}
};
var onDrain = function() {
var onDrain = function () {
while (writeBuffer.length && self.socket.bufferSize == 0) {
writeNotificationToSocket(writeBuffer.shift());
}
};
var startSocket = function () {
if(!self.openingSocket) {
process.nextTick(function() {
self.socket = tls.connect(options['port'], options['gateway'], socketOptions,
function() {
if(!self.socket.authorized) {
throw self.socket.authorizationError
var startSocket = function () {
if (!self.openingSocket) {
process.nextTick(function () {
self.socket = tls.connect(options['port'], options['gateway'], socketOptions,
callback = function () {
if (!self.socket.authorized) {
throw self.socket.authorizationError;
}
onDrain();
self.openingSocket=false;
self.openingSocket = false;
});
self.socket.on('data', function(data) {
self.socket.on('data', function (data) {
handleTransmissionError(data);
});
self.socket.on('error', function(data) {
self.socket.on('error', function () {
self.socket.removeAllListeners();
self.socket = undefined;
});
self.socket.once('close', function () {
if(writeBuffer.length && readyToConnect()) {
if (writeBuffer.length && readyToConnect()) {
startSocket();
}
if (!self.socket) {
return;
}
self.socket.removeAllListeners();
self.socket = undefined;
});
self.socket.on("drain", onDrain);
});
self.openingSocket = true;
}
}
var connect = invoke_after(function() { startSocket(); });
fs.readFile(options['cert'], connect(function(err, data) {
if(err) {
throw err;
}
socketOptions['cert'] = data.toString();
};
var connect = invoke_after(function () {
startSocket();
});
if (options['certData']) {
socketOptions['cert'] = options['certData'];
hasCert = true;
}));
} else {
fs.readFile(options['cert'], connect(function (err, data) {
if (err) {
throw err;
}
socketOptions['cert'] = data.toString();
hasCert = true;
}));
}
fs.readFile(options['key'], connect(function(err, data) {
if(err) {
throw err;
}
socketOptions['key'] = data.toString();
if (options['keyData']) {
socketOptions['key'] = options['keyData'];
hasKey = true;
}));
} else {
fs.readFile(options['key'], connect(function (err, data) {
if (err) {
throw err;
}
socketOptions['key'] = data.toString();
hasKey = true;
}));
}
var writeNotificationToSocket = function(data) {
if (self.socket === undefined || self.socket.readyState != 'open') {
var writeNotificationToSocket = function (data) {
if (self.socket === undefined || self.socket.readyState != 'open') {
if ((self.socket === undefined || self.socket.readyState == 'closed') && readyToConnect()) {
startSocket();
}
@ -126,65 +141,66 @@ var Connection = function (optionArgs) {
}
};
var bufferDataForWrite = function(data) {
var bufferDataForWrite = function (data) {
writeBuffer.push(data);
}
};
this.sendNotification = function (note) {
var encoding = 'utf8';
if(note.encoding) {
if (note.encoding) {
encoding = note.encoding;
}
var token = note.device.token;
var message = JSON.stringify(note);
var messageLength = Buffer.byteLength(message, encoding);
var pos = 0;
if(token === undefined) {
if (token === undefined) {
return Errors['missingDeviceToken'];
}
if(messageLength > 256) {
if (messageLength > 256) {
return Errors['invalidPayloadSize'];
}
note._uid = currentId++;
if(options.enhanced) {
var data = new Buffer(1 + 4 + 4 + 2 + token.length + 2 + messageLength);
var data;
if (options.enhanced) {
data = new Buffer(1 + 4 + 4 + 2 + token.length + 2 + messageLength);
// Command
data[pos] = 1;
pos++;
// Identifier
pos += int2buf(note._uid, data, pos, 4);
// Expiry
pos += int2buf(note.expiry, data, pos, 4);
cachedNotes.push(note);
tidyCachedNotes();
}
else {
var data = new Buffer(1 + 2 + token.length + 2 + messageLength);
data = new Buffer(1 + 2 + token.length + 2 + messageLength);
data[pos] = 0;
pos++;
}
pos += int2buf(token.length, data, pos, 2);
pos += token.copy(data, pos, 0);
pos += int2buf(messageLength, data, pos, 2);
pos += data.write(message, pos, encoding);
writeNotificationToSocket(data);
}
var tidyCachedNotes = function() {
};
var tidyCachedNotes = function () {
// Maybe a timestamp should be stored for each note and kept for a duration?
if(cachedNotes.length > options.cacheLength) {
if (cachedNotes.length > options.cacheLength) {
cachedNotes.shift();
}
}
var handleTransmissionError = function(data) {
};
var handleTransmissionError = function (data) {
// Need to check message that errors
// return failed notification to owner
// purge writeBuffer and start again with cachedNotes
@ -200,120 +216,127 @@ var Connection = function (optionArgs) {
// This is an error condition
var errorCode = data[1];
var identifier = bytes2int(data.slice(2,6), 4);
var identifier = bytes2int(data.slice(2, 6), 4);
var note = undefined;
while(cachedNotes.length) {
while (cachedNotes.length) {
note = cachedNotes.shift();
if(note['_uid'] == identifier) {
if (note['_uid'] == identifier) {
break;
}
}
// Notify callback of failed notification
if(typeof options.errorCallback == 'function') {
if (typeof options.errorCallback == 'function') {
options.errorCallback(errorCode, note);
}
writeBuffer = [];
var count = cachedNotes.length;
for(var i=0; i<count; i++) {
for (var i = 0; i < count; i++) {
note = cachedNotes.shift();
self.sendNotification(note);
}
}
}
}
};
var Notification = function () {
this.payload = {};
this.expiry = 0;
this.identifier = 0;
this.device;
this.alert = undefined;
this.badge = undefined;
this.sound = undefined;
}
};
Notification.prototype.toJSON = function() {
if(this.payload.aps === undefined) {
Notification.prototype.toJSON = function () {
if (this.payload === undefined) {
this.payload = {};
}
if (this.payload.aps === undefined) {
this.payload.aps = {};
}
if(typeof this.badge == 'number') {
if (typeof this.badge == 'number') {
this.payload.aps.badge = this.badge;
}
if(typeof this.sound == 'string') {
if (typeof this.sound == 'string') {
this.payload.aps.sound = this.sound;
}
if(typeof this.alert == 'string' || typeof this.alert == 'object') {
if (typeof this.alert == 'string' || typeof this.alert == 'object') {
this.payload.aps.alert = this.alert;
}
return this.payload;
}
};
var Device = function (/* deviceToken, ascii=true */) {
var self = this;
self.token = undefined;
if(arguments.length > 0) {
if (arguments.length > 0) {
self.setToken.apply(self, arguments);
}
}
};
Device.prototype.parseToken = function (token) {
token = token.replace(/\s/g, "");
length = Math.ceil(token.length / 2);
hexToken = new Buffer(length);
for(var i=0; i < token.length; i+=2) {
word = token[i];
if((i + 1) >= token.length || typeof(token[i+1]) === undefined) {
var length = Math.ceil(token.length / 2);
var hexToken = new Buffer(length);
for (var i = 0; i < token.length; i += 2) {
var word = token[i];
if ((i + 1) >= token.length || typeof(token[i + 1]) === undefined) {
word += '0';
}
else {
word += token[i+1];
word += token[i + 1];
}
hexToken[i/2] = parseInt(word, 16);
hexToken[i / 2] = parseInt(word, 16);
}
return hexToken;
}
};
Device.prototype.setToken = function (newToken, ascii) {
if(ascii === undefined || ascii == true) {
if (ascii === undefined || ascii == true) {
newToken = this.parseToken(newToken);
}
this.token = newToken;
return this;
}
};
Device.prototype.hexToken = function () {
var out = [],
len = this.token.length;
var out = [];
var len = this.token.length;
var n;
for (var i = 0; i < len; i++) {
n = this.token[i];
if (n < 16) out[i] = "0" + n.toString(16);
else out[i] = n.toString(16);
}
return out.join("");
}
};
var Feedback = function (optionArgs) {
var self = this;
var hasKey = false;
var hasCert = false;
var socketOptions = {}
var responsePacketLength = 38;
var socketOptions = {};
var responsePacketLength = 38;
var readBuffer = new Buffer(responsePacketLength);
var readLength = 0;
var options = { cert: 'cert.pem' /* Certificate file */
, key: 'key.pem' /* Key file */
, address: 'feedback.push.apple.com' /* feedback address */
, port: 2196 /* feedback port */
, feedback: false /* enable feedback service, set to callback */
, interval: 3600 /* interval in seconds to connect to feedback service */
};
var options = {
cert: 'cert.pem', /* Certificate file */
certData: '', /* Certificate data */
key: 'key.pem', /* Key file */
keyData: '', /* Key data */
address: 'feedback.push.apple.com', /* feedback address */
port: 2196, /* feedback port */
feedback: false, /* enable feedback service, set to callback */
interval: 3600, /* interval in seconds to connect to feedback service */
};
if (optionArgs) {
var keys = Object.keys(options);
for (var i = 0; i < keys.length; i++) {
@ -321,62 +344,89 @@ var Feedback = function (optionArgs) {
if (optionArgs[k] !== undefined) options[k] = optionArgs[k];
}
}
if(typeof options['feedback'] != 'function') {
if (typeof options['feedback'] != 'function') {
return Error(-1, 'A callback function must be specified');
}
this.readyToConnect = function () {
return (hasKey && hasCert);
}
};
self.startSocket = function () {
self.socket = tls.connect(options['port'], options['address'], socketOptions);
self.socket.pair.on('secure', function () { if(!self.socket.authorized) { throw self.socket.authorizationError } });
self.socket.on('data', function(data) { processData(data); });
self.socket.once('error', function(data) {self.socket.removeAllListeners(); self.socket = undefined; });
self.socket.once('end', function () { });
self.socket.once('close', function () { self.socket.removeAllListeners(); self.socket = undefined; });
}
var connect = invoke_after(function() { self.startSocket(); });
fs.readFile(options['cert'], connect(function(err, data) {
if(err) {
throw err;
}
socketOptions['cert'] = data.toString();
hasCert = true;
}));
self.socket.pair.on('secure', function () {
if (!self.socket.authorized) {
throw self.socket.authorizationError;
}
});
self.socket.on('data', function (data) {
processData(data);
});
self.socket.once('error', function () {
self.socket.removeAllListeners();
self.socket = undefined;
});
self.socket.once('end', function () {
});
self.socket.once('close', function () {
self.socket.removeAllListeners();
self.socket = undefined;
});
};
fs.readFile(options['key'], connect(function(err, data) {
if(err) {
throw err;
}
socketOptions['key'] = data.toString();
hasKey = true;
}));
if(options['interval'] > 0) {
this.interval = setInterval(function() { self.request(); }, options['interval'] * 1000);
var connect = invoke_after(function () {
self.startSocket();
});
if (options['certData']) {
socketOptions['cert'] = options['certData'];
hasCert = true;
} else {
fs.readFile(options['cert'], connect(function (err, data) {
if (err) {
throw err;
}
socketOptions['cert'] = data.toString();
hasCert = true;
}));
}
var processData = function(data) {
if (options['keyData']) {
socketOptions['key'] = options['keyData'];
hasKey = true;
} else {
fs.readFile(options['key'], connect(function (err, data) {
if (err) {
throw err;
}
socketOptions['key'] = data.toString();
hasKey = true;
}));
}
if (options['interval'] > 0) {
this.interval = setInterval(function () {
self.request();
}, options['interval'] * 1000);
}
var processData = function (data) {
var pos = 0;
// If there is some buffered data, read the remainder and process this first.
if(readLength > 0) {
if(data.length < (responsePacketLength - readLength)) {
if (readLength > 0) {
if (data.length < (responsePacketLength - readLength)) {
data.copy(readBuffer, readLength, 0);
readLength += data.length;
return;
}
data.copy(readBuffer, readLength, 0, responsePacketLength-readLength);
data.copy(readBuffer, readLength, 0, responsePacketLength - readLength);
decodeResponse(readBuffer, 0);
pos = responsePacketLength-readLength;
pos = responsePacketLength - readLength;
readLength = 0;
}
while(pos<data.length-1) {
if((data.length-pos) < responsePacketLength) {
while (pos < data.length - 1) {
if ((data.length - pos) < responsePacketLength) {
//Buffer remaining data until next time
data.copy(readBuffer, 0, pos);
readLength = data.length - pos;
@ -385,66 +435,71 @@ var Feedback = function (optionArgs) {
decodeResponse(data, pos);
pos += responsePacketLength;
}
}
};
var decodeResponse = function(data, start) {
time = bytes2int(data, 4, start);
var decodeResponse = function (data, start) {
var time = bytes2int(data, 4, start);
start += 4;
len = bytes2int(data, 2, start);
var len = bytes2int(data, 2, start);
start += 2;
tok = new Buffer(len);
data.copy(tok, 0, start, start+len);
if(typeof options['feedback'] == 'function') {
options['feedback'](time, new exports.device(tok, false));
var tok = new Buffer(len);
data.copy(tok, 0, start, start + len);
if (typeof options['feedback'] == 'function') {
options['feedback'](time, new exports.Device(tok, false));
}
}
}
};
Feedback.prototype.request = function () {
if((this.socket === undefined || this.socket.readyState == 'closed') && this.readyToConnect()) {
if ((this.socket === undefined || this.socket.readyState == 'closed') && this.readyToConnect()) {
this.startSocket();
}
}
};
Feedback.prototype.cancel = function () {
if(this.interval !== undefined) {
if (this.interval !== undefined) {
clearInterval(this.interval);
}
}
};
function int2buf(number, buffer, start, length) {
function int2buf (number, buffer, start, length) {
length -= 1;
for(var i=0; i<=length; i++) {
buffer[start+length-i] = number & 0xff;
for (var i = 0; i <= length; i++) {
buffer[start + length - i] = number & 0xff;
number = number >> 8;
}
return length+1;
return length + 1;
}
function bytes2int(bytes, length, start) {
if(start === undefined) start = 0;
function bytes2int (bytes, length, start) {
if (start === undefined) start = 0;
var num = 0;
length -= 1;
for(var i=0; i<=length; i++) {
num += (bytes[start+i] << ((length - i) * 8));
for (var i = 0; i <= length; i++) {
num += (bytes[start + i] << ((length - i) * 8));
}
return num;
}
function invoke_after(callback) {
function invoke_after (callback) {
var n = 0;
return function (delegate) {
n++;
return function() {
return function () {
delegate.apply(delegate, arguments);
if(--n == 0) callback();
if (--n == 0) callback();
};
};
}
exports.Connection = Connection;
exports.connection = Connection;
exports.Notification = Notification;
exports.notification = Notification;
exports.Device = Device;
exports.device = Device;
exports.Feedback = Feedback;
exports.feedback = Feedback;
exports.error = Errors;
exports.error = Errors;