New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

apn

Package Overview
Dependencies
Maintainers
1
Versions
61
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

apn - npm Package Compare versions

Comparing version 1.7.6 to 2.0.0-alpha1

lib/config.js

15

ChangeLog.md
## Changelog
1.7.6:
* Emit an error when retry limit is exceeded (#333)
* Documentation fixes (#229, #343, #379)
* Reinstate broken aps behaviour (#377)
1.7.5:
* Notification property improvements
* Round-robin all open sockets when sending notifications
* `minConnections` option
* Prevent crashing when cancelling feedback multiple times (#307)
* Fix connection.shutdown behaviour (#299)
* Small doc improvements
1.7.4:

@@ -19,0 +4,0 @@

59

index.js

@@ -1,14 +0,53 @@

exports.Connection = require("./lib/connection");
exports.connection = exports.Connection;
const debug = require("debug")("apn");
exports.Device = require("./lib/device");
exports.device = exports.Device;
const parse = require("./lib/credentials/parse")({
parsePkcs12: require("./lib/credentials/parsePkcs12"),
parsePemKey: require("./lib/credentials/parsePemKey"),
parsePemCert: require("./lib/credentials/parsePemCertificate"),
});
exports.Errors = require("./lib/errors");
exports.error = exports.Errors;
const prepareCredentials = require("./lib/credentials/prepare")({
load: require("./lib/credentials/load"),
parse,
validate: require("./lib/credentials/validate"),
logger: debug,
});
exports.Feedback = require("./lib/feedback");
exports.feedback = exports.Feedback;
const config = require("./lib/config")({
debug,
prepareCredentials,
});
exports.Notification = require("./lib/notification");
exports.notification = exports.Notification;
const tls = require("tls");
const framer = require("http2/lib/protocol/framer");
const compressor = require("http2/lib/protocol/compressor");
const protocol = {
Serializer: framer.Serializer,
Deserializer: framer.Deserializer,
Compressor: compressor.Compressor,
Decompressor: compressor.Decompressor,
Connection: require("http2/lib/protocol/connection").Connection,
};
const Endpoint = require("./lib/protocol/endpoint")({
tls,
protocol,
});
const EndpointManager = require("./lib/protocol/endpointManager")({
Endpoint,
});
const Connection = require("./lib/connection")({
config,
EndpointManager,
});
const Notification = require("./lib/notification");
module.exports = {
Connection,
Notification,
};
"use strict";
const EventEmitter = require("events");
const Promise = require("bluebird");
const extend = require("./util/extend");
var Errors = require("./errors");
module.exports = function(dependencies) {
const config = dependencies.config;
const EndpointManager = dependencies.EndpointManager;
var q = require("q");
var sysu = require("util");
var util = require("./util");
var events = require("events");
var Device = require("./device");
var loadCredentials = require("./credentials/load");
var parseCredentials = require("./credentials/parse");
var validateCredentials = require("./credentials/validate");
var createSocket = require("./socket");
var debug = require("debug")("apn");
var trace = require("debug")("apn:trace");
/**
* Create a new connection to the APN service.
* @constructor
* @param {Object} [options]
* @config {Buffer|String} [cert="cert.pem"] The filename of the connection certificate to load from disk, or a Buffer/String containing the certificate data.
* @config {Buffer|String} [key="key.pem"] The filename of the connection key to load from disk, or a Buffer/String containing the key data.
* @config {Buffer[]|String[]} [ca] An array of trusted certificates. Each element should contain either a filename to load, or a Buffer/String to be used directly. If this is omitted several well known "root" CAs will be used. - You may need to use this as some environments don't include the CA used by Apple (entrust_2048).
* @config {Buffer|String} [pfx] File path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer/String containing the PFX data. If supplied will be used instead of certificate and key above.
* @config {String} [passphrase] The passphrase for the connection key, if required
* @config {Boolean} [production=(NODE_ENV=='production')] Specifies which environment to connect to: Production (if true) or Sandbox. (Defaults to false, unless NODE_ENV == "production")
* @config {Number} [port=2195] Gateway port
* @config {Boolean} [rejectUnauthorized=true] Reject Unauthorized property to be passed through to tls.connect()
* @config {Function} [errorCallback] A callback which accepts 2 parameters (err, notification). Use `transmissionError` event instead.
* @config {Number} [cacheLength=1000] Number of notifications to cache for error purposes (See doc/apn.markdown)
* @config {Boolean} [autoAdjustCache=false] Whether the cache should grow in response to messages being lost after errors. (Will still emit a 'cacheTooSmall' event)
* @config {Number} [maxConnections=1] The maximum number of connections to create for sending messages.
* @config {Number} [minConnections=1] The minimum number of connections to create for sending messages.
* @config {Number} [connectionTimeout=3600000] The duration the socket should stay alive with no activity in milliseconds. 0 = Disabled.
* @config {Boolean} [buffersNotifications=true] Whether to buffer notifications and resend them after failure.
* @config {Boolean} [fastMode=false] Whether to aggresively empty the notification buffer while connected.
*/
function Connection (options) {
if(false === (this instanceof Connection)) {
return new Connection(options);
function Connection (options) {
if(false === (this instanceof Connection)) {
return new Connection(options);
}
this.options = {
cert: "cert.pem",
key: "key.pem",
ca: null,
pfx: null,
passphrase: null,
production: (process.env.NODE_ENV === "production"),
voip: false,
address: null,
port: 2195,
rejectUnauthorized: true,
cacheLength: 1000,
autoAdjustCache: true,
maxConnections: 1,
minConnections: 1,
connectTimeout: 10000,
connectionTimeout: 3600000,
connectionRetryLimit: 10,
buffersNotifications: true,
fastMode: false,
disableNagle: false,
disableEPIPEFix: false
};
for (var key in options) {
if (options[key] === null || options[key] === undefined) {
debug("Option [" + key + "] set to null. This may cause unexpected behaviour.");
}
}
this.config = config(options);
this.endpointManager = new EndpointManager(this.config);
this.endpointManager.on("wakeup", () => {
while (this.queue.length > 0) {
const stream = this.endpointManager.getStream();
if (!stream) {
return;
}
const resolve = this.queue.shift();
resolve(stream);
}
});
util.extend(this.options, options);
this.queue = [];
this.configureAddress();
EventEmitter.call(this);
}
if (this.options.pfx || this.options.pfxData) {
this.options.cert = options.cert;
this.options.key = options.key;
}
Connection.prototype = Object.create(EventEmitter.prototype);
// Set cache length to 1 to ensure transmitted notifications can be sent.
this.options.cacheLength = Math.max(this.options.cacheLength, 1);
this.options.maxConnections = Math.max(this.options.maxConnections, 1);
this.options.minConnections = Math.max(this.options.minConnections, 1);
this.deferredConnection = null;
this.sockets = [];
this.notificationBuffer = [];
Connection.prototype.pushNotification = function pushNotification(notification, recipients) {
this.socketId = 0;
const notificationHeaders = notification.headers();
const notificationBody = notification.compile();
this.failureCount = 0;
this.currentConnectionRoundRobin = 0;
const send = device => {
return new Promise( resolve => {
const stream = this.endpointManager.getStream();
if (!stream) {
this.queue.push(resolve);
} else {
resolve(stream);
}
}).then( stream => {
return new Promise ( resolve => {
stream.setEncoding("utf8");
// when true, we end all sockets after the pending notifications reach 0
this.shutdownPending = false;
stream.headers(extend({
":scheme": "https",
":method": "POST",
":authority": this.config.address,
":path": "/3/device/" + device,
}, notificationHeaders));
// track when notifications are queued so transmitCompleted is only emitted one when
// notifications are transmitted rather than after socket timeouts
this.notificationsQueued = false;
let status, responseData = "";
stream.on("headers", headers => {
status = headers[":status"];
});
this.terminated = false;
stream.on("data", data => {
responseData = responseData + data;
});
events.EventEmitter.call(this);
}
stream.on("end", () => {
if (status === "200") {
resolve({ device });
} else {
const response = JSON.parse(responseData);
resolve({ device, status, response });
}
});
stream.write(notificationBody);
stream.end();
});
});
};
sysu.inherits(Connection, events.EventEmitter);
if (!Array.isArray(recipients)) {
recipients = [recipients];
}
return Promise.all( recipients.map(send) ).then( responses => {
let sent = [];
let failed = [];
/**
*
* @private
*/
Connection.prototype.maintainMinConnection = function() {
if (this.sockets.length < this.options.minConnections && !this.shutdownPending) {
this.createConnection();
}
};
responses.forEach( response => {
if (response.status) {
failed.push(response);
} else {
sent.push(response);
}
});
return {sent, failed};
});
};
Connection.prototype.configureAddress = function() {
if (this.options.gateway) {
this.options.address = this.options.gateway;
}
if (!this.options.address) {
if (this.options.production) {
this.options.address = "gateway.push.apple.com";
}
else {
this.options.address = "gateway.sandbox.push.apple.com";
}
}
else {
if (this.options.address === "gateway.push.apple.com") {
this.options.production = true;
}
else {
this.options.production = false;
}
}
return Connection;
};
/**
* You should never need to call this method, initialization and connection is handled by {@link Connection#sendNotification}
* @private
*/
Connection.prototype.loadCredentials = function () {
if (!this.credentialsPromise) {
debug("Loading Credentials");
var production = this.options.production;
this.credentialsPromise = loadCredentials(this.options)
.then(function(credentials) {
var parsed;
try {
parsed = parseCredentials(credentials);
}
catch (e) {
debug(e);
return credentials;
}
parsed.production = production;
validateCredentials(parsed);
return credentials;
});
}
return this.credentialsPromise;
};
/**
* You should never need to call this method, initialisation and connection is handled by {@link Connection#pushNotification}
* @private
*/
Connection.prototype.createSocket = function () {
if (this.deferredConnection) {
return this.deferredConnection.promise;
}
debug("Initialising connection");
this.deferredConnection = q.defer();
this.loadCredentials().then(function(credentials) {
var socketOptions = {};
socketOptions.port = this.options.port;
socketOptions.host = this.options.address;
socketOptions.disableEPIPEFix = this.options.disableEPIPEFix;
socketOptions.disableNagle = this.options.disableNagle;
socketOptions.connectionTimeout = this.options.connectionTimeout;
socketOptions.pfx = credentials.pfx;
socketOptions.cert = credentials.cert;
socketOptions.key = credentials.key;
socketOptions.ca = credentials.ca;
socketOptions.passphrase = this.options.passphrase;
socketOptions.rejectUnauthorized = this.options.rejectUnauthorized;
this.socket = createSocket(this, socketOptions,
function () {
debug("Connection established");
this.emit("connected", this.sockets.length + 1);
if(this.deferredConnection) {
this.deferredConnection.resolve();
}
}.bind(this));
this.socket.on("error", this.errorOccurred.bind(this, this.socket));
this.socket.on("timeout", this.socketTimeout.bind(this, this.socket));
this.socket.on("data", this.handleTransmissionError.bind(this, this.socket));
this.socket.on("drain", this.socketDrained.bind(this, this.socket, true));
this.socket.once("close", this.socketClosed.bind(this, this.socket));
}.bind(this)).done(null, function (error) {
debug("Module initialisation error:", error);
// This is a pretty fatal scenario, we don't have key/certificate to connect to APNS, there's not much we can do, so raise errors and clear the queue.
this.rejectBuffer(Errors.moduleInitialisationFailed);
this.emit("error", error);
this.deferredConnection.reject(error);
this.terminated = true;
}.bind(this));
if (this.options.connectTimeout > 0) {
var connectionTimer = setTimeout(function () {
this.deferredConnection.reject(new Error("Connect timed out"));
if(this.socket) {
this.socket.end();
}
}.bind(this), this.options.connectTimeout);
return this.deferredConnection.promise.finally(function() {
clearTimeout(connectionTimer);
});
}
return this.deferredConnection.promise;
};
/**
* @private
*/
Connection.prototype.createConnection = function() {
if (this.initialisingConnection() || this.sockets.length >= this.options.maxConnections) {
return;
}
// Delay here because Apple will successfully authenticate production certificates
// in sandbox, but will then immediately close the connection. Necessary to wait for a beat
// to see if the connection stays before sending anything because the connection will be closed
// without error and messages could be lost.
this.createSocket().delay(100).then(function () {
if (this.socket.apnRetired) {
throw new Error("Socket unusable after connection. Hint: You may be using a certificate for the wrong environment");
}
this.failureCount = 0;
this.socket.apnSocketId = this.socketId++;
this.socket.apnCurrentId = 0;
this.socket.apnCachedNotifications = [];
this.sockets.push(this.socket);
trace("connection established", this.socketId);
}.bind(this)).fail(function (error) {
// Exponential backoff when connections fail.
var delay = Math.pow(2, this.failureCount++) * 1000;
trace("connection failed", delay);
this.raiseError(error);
this.emit("socketError", error);
if (this.options.connectionRetryLimit > 0
&& this.failureCount > this.options.connectionRetryLimit
&& this.sockets.length === 0) {
this.rejectBuffer(Errors.connectionRetryLimitExceeded);
this.emit("error", error);
this.shutdown();
this.terminated = true;
return;
}
return q.delay(delay);
}.bind(this)).finally(function () {
trace("create completed", this.sockets.length);
this.deferredConnection = null;
this.socket = undefined;
this.maintainMinConnection();
this.serviceBuffer();
}.bind(this)).done(null, function(error) {
this.emit("error", error);
}.bind(this));
};
/**
* @private
*/
Connection.prototype.initialisingConnection = function() {
if(this.deferredConnection !== null) {
return true;
}
return false;
};
/**
* @private
*/
Connection.prototype.serviceBuffer = function() {
var socket = null;
var repeat = false;
var socketsAvailable = 0;
if(this.options.fastMode) {
repeat = true;
}
do {
socketsAvailable = 0;
for (var i = 0; i < this.sockets.length; i++) {
var roundRobin = this.currentConnectionRoundRobin;
socket = this.sockets[roundRobin];
if(!this.socketAvailable(socket)) {
continue;
}
if (this.notificationBuffer.length === 0) {
socketsAvailable += 1;
break;
}
this.currentConnectionRoundRobin = (roundRobin + 1) % this.sockets.length;
// If a socket is available then transmit. If true is returned then manually call socketDrained
if (this.transmitNotification(socket, this.notificationBuffer.shift())) {
// Only set socket available here because if transmit returns false then the socket
// is blocked so shouldn't be used in the next loop.
socketsAvailable += 1;
this.socketDrained(socket, !repeat);
}
}
} while(repeat && socketsAvailable > 0 && this.notificationBuffer.length > 0);
if (this.notificationBuffer.length > 0 && socketsAvailable === 0) {
this.createConnection();
}
if (this.notificationBuffer.length === 0 && socketsAvailable === this.sockets.length){
if (this.notificationsQueued) {
this.emit("completed");
this.notificationsQueued = false;
}
if (this.shutdownPending) {
debug("closing connections");
for (var j = 0; j < this.sockets.length; j++) {
socket = this.sockets[j];
// We delay before closing connections to ensure we don't miss any error packets from the service.
setTimeout(socket.end.bind(socket), 2500);
this.retireSocket(socket);
}
}
}
debug("%d left to send", this.notificationBuffer.length);
};
/**
* @private
*/
Connection.prototype.errorOccurred = function(socket, err) {
debug("Socket error occurred", socket.apnSocketId, err);
if(socket.transmissionErrorOccurred && err.code === "EPIPE") {
debug("EPIPE occurred after a transmission error which we can ignore");
return;
}
if(this.socket === socket && this.deferredConnection && this.deferredConnection.promise.isPending()) {
this.deferredConnection.reject(err);
}
else {
this.emit("socketError", err);
this.raiseError(err, null);
}
if(socket.apnBusy && socket.apnCachedNotifications.length > 0) {
// A notification was in flight. It should be buffered for resending.
this.bufferNotification(socket.apnCachedNotifications[socket.apnCachedNotifications.length - 1]);
}
this.destroyConnection(socket);
};
/**
* @private
*/
Connection.prototype.socketAvailable = function(socket) {
if (!socket || !socket.writable || socket.apnRetired || socket.apnBusy || socket.transmissionErrorOccurred) {
return false;
}
return true;
};
/**
* @private
*/
Connection.prototype.socketDrained = function(socket, serviceBuffer) {
debug("Socket drained", socket.apnSocketId);
socket.apnBusy = false;
if(socket.apnCachedNotifications.length > 0) {
var notification = socket.apnCachedNotifications[socket.apnCachedNotifications.length - 1];
this.emit("transmitted", notification.notification, notification.recipient);
}
if(serviceBuffer === true && !this.runningOnNextTick) {
// There is a possibility that this could add multiple invocations to the
// call stack unnecessarily. It will be resolved within one event loop but
// should be mitigated if possible, this.nextTick aims to solve this,
// ensuring "serviceBuffer" is only called once per loop.
util.setImmediate(function() {
this.runningOnNextTick = false;
this.serviceBuffer();
}.bind(this));
this.runningOnNextTick = true;
}
};
/**
* @private
*/
Connection.prototype.socketTimeout = function(socket) {
debug("Socket timeout", socket.apnSocketId);
this.emit("timeout");
this.destroyConnection(socket);
this.serviceBuffer();
};
/**
* @private
*/
Connection.prototype.destroyConnection = function(socket) {
debug("Destroying connection", socket.apnSocketId);
if (socket) {
this.retireSocket(socket);
socket.destroy();
}
};
/**
* @private
*/
Connection.prototype.socketClosed = function(socket) {
debug("Socket closed", socket.apnSocketId);
if (socket === this.socket && this.deferredConnection.promise.isPending()) {
debug("Connection error occurred before TLS Handshake");
this.deferredConnection.reject(new Error("Unable to connect"));
}
else {
this.retireSocket(socket);
this.emit("disconnected", this.sockets.length);
}
this.serviceBuffer();
};
/**
* @private
*/
Connection.prototype.retireSocket = function(socket) {
debug("Removing socket from pool", socket.apnSocketId);
socket.apnRetired = true;
var index = this.sockets.indexOf(socket);
if (index > -1) {
this.sockets.splice(index, 1);
}
this.maintainMinConnection();
};
/**
* Use this method to modify the cache length after initialisation.
*/
Connection.prototype.setCacheLength = function(newLength) {
this.options.cacheLength = newLength;
};
/**
* @private
*/
Connection.prototype.bufferNotification = function (notification) {
if (notification.retryLimit === 0) {
this.raiseError(Errors.retryLimitExceeded, notification);
this.emit("transmissionError", Errors.retryLimitExceeded, notification.notification, notification.recipient);
return;
}
notification.retryLimit -= 1;
this.notificationBuffer.push(notification);
this.notificationsQueued = true;
};
/**
* @private
*/
Connection.prototype.rejectBuffer = function (errCode) {
while(this.notificationBuffer.length > 0) {
var notification = this.notificationBuffer.shift();
this.raiseError(errCode, notification.notification, notification.recipient);
this.emit("transmissionError", errCode, notification.notification, notification.recipient);
}
};
/**
* @private
*/
Connection.prototype.prepareNotification = function (notification, device) {
var recipient = device;
// If a device token hasn't been given then we should raise an error.
if (recipient === undefined) {
util.setImmediate(function () {
this.raiseError(Errors.missingDeviceToken, notification);
this.emit("transmissionError", Errors.missingDeviceToken, notification);
}.bind(this));
return;
}
// If we have been passed a token instead of a `Device` then we should convert.
if (!(recipient instanceof Device)) {
try {
recipient = new Device(recipient);
}
catch (e) {
// If an exception has been thrown it's down to an invalid token.
util.setImmediate(function () {
this.raiseError(Errors.invalidToken, notification, device);
this.emit("transmissionError", Errors.invalidToken, notification, device);
}.bind(this));
return;
}
}
var retryLimit = (notification.retryLimit < 0) ? -1 : notification.retryLimit + 1;
this.bufferNotification( { "notification": notification, "recipient": recipient, "retryLimit": retryLimit } );
};
/**
* @private
*/
Connection.prototype.cacheNotification = function (socket, notification) {
socket.apnCachedNotifications.push(notification);
if (socket.apnCachedNotifications.length > this.options.cacheLength) {
debug("Clearing notification %d from the cache", socket.apnCachedNotifications[0]._uid);
socket.apnCachedNotifications.splice(0, socket.apnCachedNotifications.length - this.options.cacheLength);
}
};
/**
* @private
*/
Connection.prototype.handleTransmissionError = function (socket, data) {
if (data[0] === 8) {
socket.transmissionErrorOccurred = true;
var errorCode = data[1];
var identifier = data.readUInt32BE(2);
var notification = null;
var foundNotification = false;
var temporaryCache = [];
debug("Notification %d caused an error: %d", identifier, errorCode);
while (socket.apnCachedNotifications.length) {
notification = socket.apnCachedNotifications.shift();
if (notification._uid === identifier) {
foundNotification = true;
break;
}
temporaryCache.push(notification);
}
if (foundNotification) {
while (temporaryCache.length) {
temporaryCache.shift();
}
this.emit("transmissionError", errorCode, notification.notification, notification.recipient);
this.raiseError(errorCode, notification.notification, notification.recipient);
}
else {
socket.apnCachedNotifications = temporaryCache;
if(socket.apnCachedNotifications.length > 0) {
var differentialSize = socket.apnCachedNotifications[0]._uid - identifier;
this.emit("cacheTooSmall", differentialSize);
if(this.options.autoAdjustCache) {
this.options.cacheLength += differentialSize * 2;
}
}
this.emit("transmissionError", errorCode, null);
this.raiseError(errorCode, null);
}
var count = socket.apnCachedNotifications.length;
if(this.options.buffersNotifications) {
debug("Buffering %d notifications for resending", count);
for (var i = 0; i < count; ++i) {
notification = socket.apnCachedNotifications.shift();
this.bufferNotification(notification);
}
}
}
else {
debug("Unknown data received: ", data);
}
};
/**
* @private
*/
Connection.prototype.raiseError = function(errorCode, notification, recipient) {
debug("Raising error:", errorCode, notification, recipient);
if(errorCode instanceof Error) {
debug("Error occurred with trace:", errorCode.stack);
}
if (notification && typeof notification.errorCallback === "function" ) {
notification.errorCallback(errorCode, recipient);
} else if (typeof this.options.errorCallback === "function") {
this.options.errorCallback(errorCode, notification, recipient);
}
};
/**
* @private
* @return {Boolean} Write completed, returns true if socketDrained should be called by the caller of this method.
*/
Connection.prototype.transmitNotification = function(socket, notification) {
var token = notification.recipient.token;
var encoding = notification.notification.encoding || "utf8";
var message = notification.notification.compile();
var messageLength = Buffer.byteLength(message, encoding);
var position = 0;
var data;
notification._uid = socket.apnCurrentId++;
if (socket.apnCurrentId > 0xffffffff) {
socket.apnCurrentId = 0;
}
// New Protocol uses framed notifications consisting of multiple items
// 1: Device Token
// 2: Payload
// 3: Notification Identifier
// 4: Expiration Date
// 5: Priority
// Each item has a 3 byte header: Type (1), Length (2) followed by data
// The frame layout is hard coded for now as original dynamic system had a
// significant performance penalty
var frameLength = 3 + token.length + 3 + messageLength + 3 + 4;
if(notification.notification.expiry > 0) {
frameLength += 3 + 4;
}
if(notification.notification.priority !== 10) {
frameLength += 3 + 1;
}
// Frame has a 5 byte header: Type (1), Length (4) followed by items.
data = new Buffer(5 + frameLength);
data[position] = 2; position += 1;
// Frame Length
data.writeUInt32BE(frameLength, position); position += 4;
// Token Item
data[position] = 1; position += 1;
data.writeUInt16BE(token.length, position); position += 2;
position += token.copy(data, position, 0);
// Payload Item
data[position] = 2; position += 1;
data.writeUInt16BE(messageLength, position); position += 2;
position += data.write(message, position, encoding);
// Identifier Item
data[position] = 3; position += 1;
data.writeUInt16BE(4, position); position += 2;
data.writeUInt32BE(notification._uid, position); position += 4;
if(notification.notification.expiry > 0) {
// Expiry Item
data[position] = 4; position += 1;
data.writeUInt16BE(4, position); position += 2;
data.writeUInt32BE(notification.notification.expiry, position); position += 4;
}
if(notification.notification.priority !== 10) {
// Priority Item
data[position] = 5; position += 1;
data.writeUInt16BE(1, position); position += 2;
data[position] = notification.notification.priority; position += 1;
}
this.cacheNotification(socket, notification);
socket.apnBusy = true;
return socket.write(data);
};
Connection.prototype.validNotification = function (notification, recipient) {
var messageLength = notification.length();
var maxLength = (this.options.voip ? 4096 : 2048);
if (messageLength > maxLength) {
util.setImmediate(function () {
this.raiseError(Errors.invalidPayloadSize, notification, recipient);
this.emit("transmissionError", Errors.invalidPayloadSize, notification, recipient);
}.bind(this));
return false;
}
return true;
};
/**
* Queue a notification for delivery to recipients
* @param {Notification} notification The Notification object to be sent
* @param {Device|String|Buffer|Device[]|String[]|Buffer[]} recipient The token(s) for devices the notification should be delivered to.
* @since v1.3.0
*/
Connection.prototype.pushNotification = function (notification, recipient) {
if (this.terminated) {
this.emit("transmissionError", Errors.connectionTerminated, notification, recipient);
return false;
}
if (!this.validNotification(notification, recipient)) {
return;
}
if (sysu.isArray(recipient)) {
for (var i = recipient.length - 1; i >= 0; i--) {
this.prepareNotification(notification, recipient[i]);
}
}
else {
this.prepareNotification(notification, recipient);
}
this.shutdownPending = false;
this.serviceBuffer();
};
/**
* Send a notification to the APN service
* @param {Notification} notification The notification object to be sent
* @deprecated Since v1.3.0, use pushNotification instead
*/
Connection.prototype.sendNotification = function (notification) {
return this.pushNotification(notification, notification.device);
};
/**
* End connections with APNS once we've finished sending all notifications
*/
Connection.prototype.shutdown = function () {
debug("Shutdown pending");
this.shutdownPending = true;
};
module.exports = Connection;
"use strict";
var q = require("q");
var sysu = require("util");
var resolve = require("./resolve");

@@ -11,33 +8,26 @@

// Prepare PKCS#12 data if available
var pfxPromise = resolve(credentials.pfx || credentials.pfxData);
var pfx = resolve(credentials.pfx || credentials.pfxData);
// Prepare Certificate data if available.
var certPromise = resolve(credentials.cert || credentials.certData);
var cert = resolve(credentials.cert || credentials.certData);
// Prepare Key data if available
var keyPromise = resolve(credentials.key || credentials.keyData);
var key = resolve(credentials.key || credentials.keyData);
// Prepare Certificate Authority data if available.
var caPromises = [];
var ca = [];
if (credentials.ca !== null) {
if(!sysu.isArray(credentials.ca)) {
if(!Array.isArray(credentials.ca)) {
credentials.ca = [ credentials.ca ];
}
credentials.ca.forEach(function(ca) {
caPromises.push(resolve(ca));
});
ca = credentials.ca.map( resolve );
}
if (caPromises.length === 0) {
caPromises = undefined;
if (ca.length === 0) {
ca = undefined;
}
else {
caPromises = q.all(caPromises);
}
return q.all([pfxPromise, certPromise, keyPromise, caPromises])
.spread(function(pfx, cert, key, ca) {
return { pfx: pfx, cert: cert, key: key, ca: ca, passphrase: credentials.passphrase };
});
return { pfx: pfx, cert: cert, key: key, ca: ca, passphrase: credentials.passphrase };
}
module.exports = loadCredentials;
module.exports = loadCredentials;

@@ -1,20 +0,21 @@

var parsePkcs12 = require("./parsePkcs12");
var parsePemKey = require("./parsePemKey");
var parsePemCert = require("./parsePemCertificate");
module.exports = function (dependencies) {
const parsePkcs12 = dependencies.parsePkcs12;
const parsePemKey = dependencies.parsePemKey;
const parsePemCert = dependencies.parsePemCert;
function parse(credentials) {
var parsed = {};
function parse(credentials) {
var parsed = {};
parsed.key = parsePemKey(credentials.key, credentials.passphrase);
parsed.certificates = parsePemCert(credentials.cert);
parsed.key = parsePemKey(credentials.key, credentials.passphrase);
parsed.certificates = parsePemCert(credentials.cert);
var pkcs12Parsed = parsePkcs12(credentials.pfx, credentials.passphrase);
if (pkcs12Parsed) {
parsed.key = pkcs12Parsed.key;
parsed.certificates = pkcs12Parsed.certificates;
}
var pkcs12Parsed = parsePkcs12(credentials.pfx, credentials.passphrase);
if (pkcs12Parsed) {
parsed.key = pkcs12Parsed.key;
parsed.certificates = pkcs12Parsed.certificates;
return parsed;
}
return parsed;
}
module.exports = parse;
return parse;
};
"use strict";
var fs = require("fs");
var q = require("q");

@@ -17,6 +16,6 @@ function resolveCredential(value) {

else {
return q.nfbind(fs.readFile)(value);
return fs.readFileSync(value);
}
}
module.exports = resolveCredential;
module.exports = resolveCredential;

@@ -8,17 +8,17 @@ "use strict";

function Device(deviceToken) {
if (!(this instanceof Device)) {
return new Device(deviceToken);
}
if (!(this instanceof Device)) {
return new Device(deviceToken);
}
if(typeof deviceToken === "string") {
this.token = new Buffer(deviceToken.replace(/[^0-9a-f]/gi, ""), "hex");
}
else if(Buffer.isBuffer(deviceToken)) {
this.token = new Buffer(deviceToken.length);
deviceToken.copy(this.token);
}
if (!this.token || this.token.length === 0) {
throw new Error("Invalid Token Specified, must be a Buffer or valid hex String");
}
if(typeof deviceToken === "string") {
this.token = new Buffer(deviceToken.replace(/[^0-9a-f]/gi, ""), "hex");
}
else if(Buffer.isBuffer(deviceToken)) {
this.token = new Buffer(deviceToken.length);
deviceToken.copy(this.token);
}
if (!this.token || this.token.length === 0) {
throw new Error("Invalid Token Specified, must be a Buffer or valid hex String");
}
}

@@ -31,5 +31,5 @@

Device.prototype.toString = function() {
return this.token.toString("hex");
return this.token.toString("hex");
};
module.exports = Device;
module.exports = Device;
{
"name": "apn",
"description": "An interface to the Apple Push Notification service for Node.js",
"version": "1.7.6",
"version": "2.0.0-alpha1",
"author": "Andrew Naylor <argon@mkbot.net>",

@@ -30,12 +30,11 @@ "contributors": [

"dependencies": {
"debug": "^2.2.0",
"debug": "^2.1.3",
"http2": "https://github.com/argon/node-http2#master",
"node-forge": "^0.6.20",
"q": "^1.1.0"
"bluebird": "*"
},
"devDependencies": {
"chai": "3.x",
"chai": "2.x",
"chai-as-promised": "*",
"lolex": "^1.2.1",
"mocha": "*",
"rewire": "^2.3.0",
"sinon": "^1.12.2",

@@ -50,6 +49,13 @@ "sinon-chai": "^2.6.0"

},
"eslintConfig": {
"ecmaVersion": 6,
"env": {
"es6": true,
"node": true
}
},
"engines": {
"node": ">= 0.8.0"
"node": ">= 4.0.0"
},
"license": "MIT"
}

@@ -99,4 +99,2 @@ #node-apn

You should only create one `Connection` per-process for each certificate/key pair you have. You do not need to create a new `Connection` for each notification. If you are only sending notifications to one app then there is no need for more than one `Connection`, if throughput is a problem then look at the `maxConnections` property.
### Setting up the feedback service

@@ -171,3 +169,3 @@

[pl]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 "Local and Push Notification Programming Guide: Apple Push Notification Service"
[fs]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Appendixes/BinaryProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH106-SW4 "The Feedback Service"
[fs]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3 "The Feedback Service"
[tn2265]: http://developer.apple.com/library/ios/#technotes/tn2265/_index.html "Troubleshooting Push Notifications"

@@ -174,0 +172,0 @@ [googlegroup]:https://groups.google.com/group/node-apn "node-apn Google Group"

@@ -1,491 +0,339 @@

var rewire = require("rewire");
var Connection = rewire("../lib/connection");
"use strict";
var events = require("events");
var sinon = require("sinon");
var lolex = require("lolex");
var Q = require("q");
const sinon = require("sinon");
const stream = require("stream");
const EventEmitter = require("events");
describe("Connection", function() {
describe("constructor", function () {
var originalEnv;
let fakes, Connection;
before(function() {
originalEnv = process.env.NODE_ENV;
});
beforeEach(() => {
fakes = {
config: sinon.stub(),
EndpointManager: sinon.stub(),
endpointManager: new EventEmitter(),
};
after(function() {
process.env.NODE_ENV = originalEnv;
});
fakes.EndpointManager.returns(fakes.endpointManager);
beforeEach(function() {
process.env.NODE_ENV = "";
});
Connection = require("../lib/connection")(fakes);
});
// Issue #50
it("should use gateway.sandbox.push.apple.com as the default connection address", function () {
expect(Connection().options.address).to.equal("gateway.sandbox.push.apple.com");
});
describe("constructor", function () {
it("should use gateway.push.apple.com when NODE_ENV=production", function () {
process.env.NODE_ENV = "production";
expect(Connection().options.address).to.equal("gateway.push.apple.com");
});
context("called without `new`", () => {
it("returns a new instance", () => {
expect(Connection()).to.be.an.instanceof(Connection);
});
});
it("should give precedence to production flag over NODE_ENV=production", function () {
process.env.NODE_ENV = "production";
expect(Connection({ production: false }).options.address).to.equal("gateway.sandbox.push.apple.com");
});
it("prepares the configuration with passed options", () => {
let options = { production: true };
Connection(options);
it("should use gateway.push.apple.com when production:true", function () {
expect(Connection({production:true}).options.address).to.equal("gateway.push.apple.com");
});
expect(fakes.config).to.be.calledWith(options);
});
it("should use a custom address when passed", function () {
expect(Connection({address: "testaddress"}).options.address).to.equal("testaddress");
});
describe("EndpointManager instance", function() {
it("is created", () => {
Connection();
describe("gateway option", function() {
it("uses the legacy gateway option when supplied", function() {
expect(Connection({gateway: "testaddress"}).options.address).to.equal("testaddress");
});
});
describe("address is passed", function() {
it("sets production to true when using production address", function() {
expect(Connection({address: "gateway.push.apple.com"}).options.production).to.be.true;
});
expect(fakes.EndpointManager).to.be.calledOnce;
expect(fakes.EndpointManager).to.be.calledWithNew;
});
it("sets production to false when using sandbox address", function() {
process.env.NODE_ENV = "production";
expect(Connection({address: "gateway.sandbox.push.apple.com"}).options.production).to.be.false;
});
});
});
it("is passed the prepared configuration", () => {
const returnSentinel = { "configKey": "configValue"};
fakes.config.returns(returnSentinel);
describe("#loadCredentials", function () {
var loadStub, parseStub, validateStub, removeStubs;
beforeEach(function() {
loadStub = sinon.stub();
loadStub.displayName = "loadCredentials";
Connection({});
expect(fakes.EndpointManager).to.be.calledWith(returnSentinel);
});
});
});
parseStub = sinon.stub();
parseStub.displayName = "parseCredentials";
validateStub = sinon.stub();
validateStub.displayName = "validateCredentials";
describe("pushNotification", () => {
removeStubs = Connection.__set__({
"loadCredentials": loadStub,
"parseCredentials": parseStub,
"validateCredentials": validateStub,
});
});
beforeEach(() => {
fakes.config.returnsArg(0);
fakes.endpointManager.getStream = sinon.stub();
afterEach(function() {
removeStubs();
});
fakes.EndpointManager.returns(fakes.endpointManager);
});
it("only loads credentials once", function() {
loadStub.returns(Q({}));
describe("single notification behaviour", () => {
var connection = Connection();
connection.loadCredentials();
connection.loadCredentials();
expect(loadStub).to.be.calledOnce;
});
context("a single stream is available", () => {
let connection;
describe("with valid credentials", function() {
var credentials;
var testOptions = {
pfx: "myCredentials.pfx", cert: "myCert.pem", key: "myKey.pem", ca: "myCa.pem",
passphrase: "apntest", production: true
};
context("transmission succeeds", () => {
beforeEach( () => {
connection = new Connection( { address: "testapi" } );
beforeEach(function() {
loadStub.withArgs(sinon.match(function(v) {
return v.pfx === "myCredentials.pfx" && v.cert === "myCert.pem" && v.key === "myKey.pem" &&
v.ca === "myCa.pem" && v.passphrase === "apntest";
})).returns(Q({ pfx: "myPfxData", cert: "myCertData", key: "myKeyData", ca: ["myCaData"], passphrase: "apntest" }));
fakes.stream = new FakeStream("abcd1234", "200");
fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
});
parseStub.returnsArg(0);
it("attempts to acquire one stream", () => {
return connection.pushNotification(notificationDouble(), "abcd1234")
.then(() => {
expect(fakes.endpointManager.getStream).to.be.calledOnce;
});
});
credentials = Connection(testOptions).loadCredentials();
});
describe("headers", () => {
it("should be fulfilled", function () {
return expect(credentials).to.be.fulfilled;
});
it("sends the required HTTP/2 headers", () => {
return connection.pushNotification(notificationDouble(), "abcd1234")
.then(() => {
expect(fakes.stream.headers).to.be.calledWithMatch( {
":scheme": "https",
":method": "POST",
":authority": "testapi",
":path": "/3/device/abcd1234",
});
});
});
describe("the validation stage", function() {
it("is called once", function() {
return credentials.finally(function() {
expect(validateStub).to.be.calledOnce;
});
});
it("does not include apns headers when not required", () => {
return connection.pushNotification(notificationDouble(), "abcd1234")
.then(() => {
["apns-id", "apns-priority", "apns-expiration", "apns-topic"].forEach( header => {
expect(fakes.stream.headers).to.not.be.calledWithMatch(sinon.match.has(header));
});
});
});
it("is passed the production flag", function() {
return credentials.finally(function() {
expect(validateStub.getCall(0).args[0]).to.have.property("production", true);
});
});
it("sends the notification-specific apns headers when specified", () => {
let notification = notificationDouble();
describe("passed credentials", function() {
it("contains the PFX data", function() {
return credentials.finally(function() {
expect(validateStub.getCall(0).args[0]).to.have.property("pfx", "myPfxData");
});
});
notification.headers.returns({
"apns-id": "123e4567-e89b-12d3-a456-42665544000",
"apns-priority": 5,
"apns-expiration": 123,
"apns-topic": "io.apn.node",
});
it("contains the key data", function() {
return credentials.finally(function() {
expect(validateStub.getCall(0).args[0]).to.have.property("key", "myKeyData");
});
});
return connection.pushNotification(notification, "abcd1234")
.then(() => {
expect(fakes.stream.headers).to.be.calledWithMatch( {
"apns-id": "123e4567-e89b-12d3-a456-42665544000",
"apns-priority": 5,
"apns-expiration": 123,
"apns-topic": "io.apn.node",
});
});
});
});
it("contains the certificate data", function() {
return credentials.finally(function() {
expect(validateStub.getCall(0).args[0]).to.have.property("cert", "myCertData");
});
});
it("writes the notification data to the pipe", () => {
return connection.pushNotification(notificationDouble(), "abcd1234")
.then(() => {
expect(fakes.stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer(notificationDouble().compile())));
});
});
it("includes passphrase", function() {
return credentials.finally(function() {
expect(validateStub.getCall(0).args[0]).to.have.property("passphrase", "apntest");
});
});
});
});
it("ends the stream", () => {
return connection.pushNotification(notificationDouble(), "abcd1234")
.then(() => {
expect(() => fakes.stream.write("ended?")).to.throw("write after end");
});
});
describe("resolution value", function() {
it("contains the PFX data", function() {
return expect(credentials).to.eventually.have.property("pfx", "myPfxData");
});
it("resolves with the device token in the sent array", () => {
return expect(connection.pushNotification(notificationDouble(), "abcd1234"))
.to.become({ sent: [{"device": "abcd1234"}], failed: []});
});
});
it("contains the key data", function() {
return expect(credentials).to.eventually.have.property("key", "myKeyData");
});
context("error occurs", () => {
let promise;
it("contains the certificate data", function() {
return expect(credentials).to.eventually.have.property("cert", "myCertData");
});
beforeEach(() => {
const connection = new Connection( { address: "testapi" } );
it("contains the CA data", function() {
return expect(credentials).to.eventually.have.deep.property("ca[0]", "myCaData");
});
fakes.stream = new FakeStream("abcd1234", "400", { "reason" : "BadDeviceToken" });
fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
it("includes passphrase", function() {
return expect(credentials).to.eventually.have.property("passphrase", "apntest");
});
});
});
promise = connection.pushNotification(notificationDouble(), "abcd1234");
});
describe("credential file cannot be parsed", function() {
beforeEach(function() {
loadStub.returns(Q({ cert: "myCertData", key: "myKeyData" }));
parseStub.throws(new Error("unable to parse key"));
});
it("resolves with the device token, status code and response in the failed array", () => {
return expect(promise).to.eventually.deep.equal({ sent: [], failed: [{"device": "abcd1234", "status": "400", "response": { "reason" : "BadDeviceToken" }}]});
});
});
});
it("should resolve with the credentials", function() {
var credentials = Connection({ cert: "myUnparseableCert.pem", key: "myUnparseableKey.pem" }).loadCredentials();
return expect(credentials).to.become({ cert: "myCertData", key: "myKeyData" });
});
context("no new stream is returned but the endpoint later wakes up", () => {
let notification, promise;
it("should log an error", function() {
var debug = sinon.spy();
var reset = Connection.__set__("debug", debug);
var credentials = Connection({ cert: "myUnparseableCert.pem", key: "myUnparseableKey.pem" }).loadCredentials();
beforeEach( done => {
const connection = new Connection( { address: "testapi" } );
return credentials.finally(function() {
reset();
expect(debug).to.be.calledWith(sinon.match(function(err) {
return err.message ? err.message.match(/unable to parse key/) : false;
}, "\"unable to parse key\""));
});
});
fakes.stream = new FakeStream("abcd1234", "200");
fakes.endpointManager.getStream.onCall(0).returns(null);
fakes.endpointManager.getStream.onCall(1).returns(fakes.stream);
it("should not attempt to validate", function() {
var credentials = Connection({ cert: "myUnparseableCert.pem", key: "myUnparseableKey.pem" }).loadCredentials();
return credentials.finally(function() {
expect(validateStub).to.not.be.called;
});
});
});
notification = notificationDouble();
promise = connection.pushNotification(notification, "abcd1234");
describe("credential validation fails", function() {
it("should be rejected", function() {
loadStub.returns(Q({ cert: "myCertData", key: "myMismatchedKeyData" }));
parseStub.returnsArg(0);
validateStub.throws(new Error("certificate and key do not match"));
expect(fakes.stream.headers).to.not.be.called;
var credentials = Connection({ cert: "myCert.pem", key: "myMistmatchedKey.pem" }).loadCredentials();
return expect(credentials).to.eventually.be.rejectedWith(/certificate and key do not match/);
});
});
fakes.endpointManager.emit("wakeup");
describe("credential file cannot be loaded", function() {
it("should be rejected", function() {
loadStub.returns(Q.reject(new Error("ENOENT, no such file or directory")));
promise.then( () => done(), done );
});
var credentials = Connection({ cert: "noSuchFile.pem", key: "myKey.pem" }).loadCredentials();
return expect(credentials).to.eventually.be.rejectedWith("ENOENT, no such file or directory");
});
});
});
it("sends the required headers to the newly available stream", () => {
expect(fakes.stream.headers).to.be.calledWithMatch( {
":scheme": "https",
":method": "POST",
":authority": "testapi",
":path": "/3/device/abcd1234",
});
});
describe("createSocket", function() {
var socketDouble, socketStub, removeSocketStub;
it("writes the notification data to the pipe", () => {
expect(fakes.stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer(notification.compile())));
});
});
});
before(function() {
var loadCredentialsStub = sinon.stub(Connection.prototype, "loadCredentials");
loadCredentialsStub.returns(Q({
pfx: "pfxData",
key: "keyData",
cert: "certData",
ca: ["caData1", "caData2"],
passphrase: "apntest" }));
});
context("when 5 tokens are passed", () => {
after(function() {
Connection.prototype.loadCredentials.restore();
});
beforeEach(function() {
socketDouble = new events.EventEmitter();
socketDouble.end = sinon.spy();
beforeEach(() => {
fakes.streams = [
new FakeStream("abcd1234", "200"),
new FakeStream("adfe5969", "400", { reason: "MissingTopic" }),
new FakeStream("abcd1335", "410", { reason: "BadDeviceToken", timestamp: 123456789 }),
new FakeStream("bcfe4433", "200"),
new FakeStream("aabbc788", "413", { reason: "PayloadTooLarge" }),
];
});
socketStub = sinon.stub();
socketStub.callsArg(2);
socketStub.returns(socketDouble);
context("streams are always returned", () => {
let promise;
removeSocketStub = Connection.__set__("createSocket", socketStub);
});
beforeEach( done => {
const connection = new Connection( { address: "testapi" } );
afterEach(function() {
socketDouble.removeAllListeners();
removeSocketStub();
});
fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
fakes.endpointManager.getStream.onCall(2).returns(fakes.streams[2]);
fakes.endpointManager.getStream.onCall(3).returns(fakes.streams[3]);
fakes.endpointManager.getStream.onCall(4).returns(fakes.streams[4]);
it("loadCredentialss the module", function(done) {
var connection = Connection({ pfx: "myCredentials.pfx" });
return connection.createSocket().finally(function() {
expect(connection.loadCredentials).to.have.been.calledOnce;
done();
});
});
promise = connection.pushNotification(notificationDouble(), ["abcd1234", "adfe5969", "abcd1335", "bcfe4433", "aabbc788"]);
promise.then( () => done(), done);
});
describe("with valid credentials", function() {
it("resolves", function() {
var connection = Connection({
cert: "myCert.pem",
key: "myKey.pem"
});
return expect(connection.createSocket()).to.be.fulfilled;
});
it("sends the required headers for each stream", () => {
expect(fakes.streams[0].headers).to.be.calledWithMatch( { ":path": "/3/device/abcd1234" } );
expect(fakes.streams[1].headers).to.be.calledWithMatch( { ":path": "/3/device/adfe5969" } );
expect(fakes.streams[2].headers).to.be.calledWithMatch( { ":path": "/3/device/abcd1335" } );
expect(fakes.streams[3].headers).to.be.calledWithMatch( { ":path": "/3/device/bcfe4433" } );
expect(fakes.streams[4].headers).to.be.calledWithMatch( { ":path": "/3/device/aabbc788" } );
});
describe("the call to create socket", function() {
var connect;
it("writes the notification data for each stream", () => {
fakes.streams.forEach( stream => {
expect(stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer(notificationDouble().compile())));
});
});
it("passes PFX data", function() {
connect = Connection({
pfx: "myCredentials.pfx",
passphrase: "apntest"
}).createSocket();
return connect.then(function() {
var socketOptions = socketStub.args[0][1];
expect(socketOptions.pfx).to.equal("pfxData");
});
});
it("resolves with the sent notifications", () => {
return expect(promise.get("sent")).to.eventually.deep.equal([{device: "abcd1234"}, {device: "bcfe4433"}]);
});
it("passes the passphrase", function() {
connect = Connection({
passphrase: "apntest",
cert: "myCert.pem",
key: "myKey.pem"
}).createSocket();
return connect.then(function() {
var socketOptions = socketStub.args[0][1];
expect(socketOptions.passphrase).to.equal("apntest");
});
});
it("resolves with the device token, status code and response of the unsent notifications", () => {
return expect(promise.get("failed")).to.eventually.deep.equal([
{ device: "adfe5969", status: "400", response: { reason: "MissingTopic" }},
{ device: "abcd1335", status: "410", response: { reason: "BadDeviceToken", timestamp: 123456789 }},
{ device: "aabbc788", status: "413", response: { reason: "PayloadTooLarge" }},
]);
});
});
it("passes the cert", function() {
connect = Connection({
cert: "myCert.pem",
key: "myKey.pem"
}).createSocket();
return connect.then(function() {
var socketOptions = socketStub.args[0][1];
expect(socketOptions.cert).to.equal("certData");
});
});
context("some streams return, others wake up later", () => {
let promise;
it("passes the key", function() {
connect = Connection({
cert: "test/credentials/support/cert.pem",
key: "test/credentials/support/key.pem"
}).createSocket();
return connect.then(function() {
var socketOptions = socketStub.args[0][1];
expect(socketOptions.key).to.equal("keyData");
});
});
beforeEach( done => {
const connection = new Connection( { address: "testapi" } );
it("passes the ca certificates", function() {
connect = Connection({
cert: "test/credentials/support/cert.pem",
key: "test/credentials/support/key.pem",
ca: [ "test/credentials/support/issuerCert.pem" ]
}).createSocket();
return connect.then(function() {
var socketOptions = socketStub.args[0][1];
expect(socketOptions.ca[0]).to.equal("caData1");
});
});
});
});
fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
describe("intialization failure", function() {
it("is rejected", function() {
var connection = Connection({ pfx: "a-non-existant-file-which-really-shouldnt-exist.pfx" });
connection.on("error", function() {});
connection.loadCredentials = sinon.stub();
promise = connection.pushNotification(notificationDouble(), ["abcd1234", "adfe5969", "abcd1335", "bcfe4433", "aabbc788"]);
promise.then( () => done(), done);
connection.loadCredentials.returns(Q.reject(new Error("loadCredentials failed")));
setTimeout(() => {
fakes.endpointManager.getStream.reset();
fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[2]);
fakes.endpointManager.getStream.onCall(1).returns(null);
fakes.endpointManager.emit("wakeup");
}, 1);
return expect(connection.createSocket()).to.be.rejectedWith("loadCredentials failed");
});
});
setTimeout(() => {
fakes.endpointManager.getStream.reset();
fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[3]);
fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[4]);
fakes.endpointManager.emit("wakeup");
}, 2);
});
describe("timeout option", function() {
var clock, timeoutRestore;
beforeEach(function() {
clock = lolex.createClock();
timeoutRestore = Connection.__set__({
"setTimeout": clock.setTimeout,
"clearTimeout": clock.clearTimeout
});
});
it("sends the correct device ID for each stream", () => {
expect(fakes.streams[0].headers).to.be.calledWithMatch({":path": "/3/device/abcd1234"});
expect(fakes.streams[1].headers).to.be.calledWithMatch({":path": "/3/device/adfe5969"});
expect(fakes.streams[2].headers).to.be.calledWithMatch({":path": "/3/device/abcd1335"});
expect(fakes.streams[3].headers).to.be.calledWithMatch({":path": "/3/device/bcfe4433"});
expect(fakes.streams[4].headers).to.be.calledWithMatch({":path": "/3/device/aabbc788"});
});
afterEach(function() {
timeoutRestore();
});
it("writes the notification data for each stream", () => {
fakes.streams.forEach( stream => {
expect(stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer(notificationDouble().compile())));
});
});
it("ends the socket when connection takes too long", function() {
var connection = Connection({connectTimeout: 3000}).createSocket();
socketStub.onCall(0).returns(socketDouble);
process.nextTick(function(){
clock.tick(5000);
});
it("resolves with the sent notifications", () => {
return expect(promise.get("sent")).to.eventually.deep.equal([{device: "abcd1234"}, {device: "bcfe4433"}]);
});
return connection.then(function() {
throw "connection did not time out";
}, function() {
expect(socketDouble.end).to.have.been.called;
});
});
it("resolves with the device token, status code and response of the unsent notifications", () => {
return expect(promise.get("failed")).to.eventually.deep.equal([
{ device: "adfe5969", status: "400", response: { reason: "MissingTopic" }},
{ device: "abcd1335", status: "410", response: { reason: "BadDeviceToken", timestamp: 123456789 }},
{ device: "aabbc788", status: "413", response: { reason: "PayloadTooLarge" }},
]);
});
});
});
});
});
it("does not end the socket when the connnection succeeds", function() {
var connection = Connection({connectTimeout: 3000}).createSocket();
function notificationDouble() {
return {
headers: sinon.stub().returns({}),
payload: { aps: { badge: 1 } },
compile: function() { return JSON.stringify(this.payload); }
};
}
return connection.then(function() {
clock.tick(5000);
expect(socketDouble.end).to.not.have.been.called;
});
});
function FakeStream(deviceId, statusCode, response) {
const fakeStream = new stream.Transform({
transform: sinon.spy(function(chunk, encoding, callback) {
expect(this.headers).to.be.calledOnce;
it("does not end the socket when the connection fails", function() {
var connection = Connection({connectTimeout: 3000}).createSocket();
socketStub.onCall(0).returns(socketDouble);
process.nextTick(function() {
socketDouble.emit("close");
});
const headers = this.headers.firstCall.args[0];
expect(headers[":path"].substring(10)).to.equal(deviceId);
return connection.then(function() {
throw "connection should have failed";
}, function() {
clock.tick(5000);
expect(socketDouble.end).to.not.have.been.called;
});
});
this.emit("headers", {
":status": statusCode
});
callback(null, new Buffer(JSON.stringify(response) || ""));
})
});
fakeStream.headers = sinon.stub();
it("does not fire when disabled", function() {
var connection = Connection({connectTimeout: 0}).createSocket();
socketStub.onCall(0).returns(socketDouble);
process.nextTick(function() {
clock.tick(100000);
socketDouble.emit("close");
});
return connection.then(function() {
throw "connection should have failed";
}, function() {
expect(socketDouble.end).to.not.have.been.called;
});
});
context("timeout fires before socket is created", function() {
it("does not throw", function() {
var connection = Connection({connectTimeout: 100});
connection.credentialsPromise = Q.defer();
connection.createSocket();
expect(function() { clock.tick(500); }).to.not.throw(TypeError);
});
});
context("after timeout fires", function() {
it("does not throw if socket connects", function() {
var connection = Connection({connectTimeout: 100});
socketStub.onCall(0).returns(socketDouble);
connection.loadCredentials().then(function() {
clock.tick(500);
});
return connection.createSocket().then(null, function() {
connection.deferredConnection = null;
expect(socketStub.getCall(0).args[2]).to.not.throw(TypeError);
});
});
});
});
});
describe("validNotification", function() {
describe("notification is shorter than max allowed", function() {
it("returns true", function() {
var connection = Connection();
var notification = { length: function() { return 128; }};
expect(connection.validNotification(notification)).to.equal(true);
});
});
describe("notification is the maximum length", function() {
it("returns true", function() {
var connection = Connection();
var notification = { length: function() { return 2048; }};
expect(connection.validNotification(notification)).to.equal(true);
});
});
describe("notification too long", function() {
it("returns false", function() {
var connection = Connection();
var notification = { length: function() { return 2176; }};
expect(connection.validNotification(notification)).to.equal(false);
});
});
describe("VoIP flag set", function() {
it("allows longer payload", function() {
var connection = Connection({"voip": true});
var notification = { length: function() { return 4096; }};
expect(connection.validNotification(notification)).to.equal(true);
});
});
});
});
return fakeStream;
}

@@ -12,107 +12,89 @@ var loadCredentials = require("../../lib/credentials/load");

it("should eventually load a pfx file from disk", function () {
it("should load a pfx file from disk", function () {
return expect(loadCredentials({ pfx: "test/support/initializeTest.pfx" })
.get("pfx").post("toString"))
.to.eventually.equal(pfx.toString());
.pfx.toString()).to.equal(pfx.toString());
});
it("should eventually provide pfx data from memory", function () {
return expect(loadCredentials({ pfx: pfx }).get("pfx").post("toString"))
.to.eventually.equal(pfx.toString());
it("should provide pfx data from memory", function () {
return expect(loadCredentials({ pfx: pfx }).pfx.toString())
.to.equal(pfx.toString());
});
it("should eventually provide pfx data explicitly passed in pfxData parameter", function () {
return expect(loadCredentials({ pfxData: pfx }).get("pfx").post("toString"))
.to.eventually.equal(pfx.toString());
it("should provide pfx data explicitly passed in pfxData parameter", function () {
return expect(loadCredentials({ pfxData: pfx }).pfx.toString())
.to.equal(pfx.toString());
});
it("should eventually load a certificate from disk", function () {
it("should load a certificate from disk", function () {
return expect(loadCredentials({ cert: "test/support/initializeTest.crt", key: null})
.get("cert").post("toString"))
.to.eventually.equal(cert.toString());
.cert.toString()).to.equal(cert.toString());
});
it("should eventually provide a certificate from a Buffer", function () {
return expect(loadCredentials({ cert: cert, key: null})
.get("cert").post("toString"))
.to.eventually.equal(cert.toString());
it("should provide a certificate from a Buffer", function () {
return expect(loadCredentials({ cert: cert, key: null}).cert.toString())
.to.equal(cert.toString());
});
it("should eventually provide a certificate from a String", function () {
return expect(loadCredentials({ cert: cert.toString(), key: null})
.get("cert"))
.to.eventually.equal(cert.toString());
it("should provide a certificate from a String", function () {
return expect(loadCredentials({ cert: cert.toString(), key: null}).cert)
.to.equal(cert.toString());
});
it("should eventually provide certificate data explicitly passed in the certData parameter", function () {
return expect(loadCredentials({ certData: cert, key: null})
.get("cert").post("toString"))
.to.eventually.equal(cert.toString());
it("should provide certificate data explicitly passed in the certData parameter", function () {
return expect(loadCredentials({ certData: cert, key: null}).cert.toString())
.to.equal(cert.toString());
});
it("should eventually load a key from disk", function () {
it("should load a key from disk", function () {
return expect(loadCredentials({ cert: null, key: "test/support/initializeTest.key"})
.get("key").post("toString"))
.to.eventually.equal(key.toString());
.key.toString()).to.equal(key.toString());
});
it("should eventually provide a key from a Buffer", function () {
return expect(loadCredentials({ cert: null, key: key})
.get("key").post("toString"))
.to.eventually.equal(key.toString());
it("should provide a key from a Buffer", function () {
return expect(loadCredentials({ cert: null, key: key}).key.toString())
.to.equal(key.toString());
});
it("should eventually provide a key from a String", function () {
return expect(loadCredentials({ cert: null, key: key.toString()})
.get("key"))
.to.eventually.equal(key.toString());
it("should provide a key from a String", function () {
return expect(loadCredentials({ cert: null, key: key.toString()}).key)
.to.equal(key.toString());
});
it("should eventually provide key data explicitly passed in the keyData parameter", function () {
return expect(loadCredentials({ cert: null, keyData: key})
.get("key").post("toString"))
.to.eventually.equal(key.toString());
it("should provide key data explicitly passed in the keyData parameter", function () {
return expect(loadCredentials({ cert: null, keyData: key}).key.toString())
.to.equal(key.toString());
});
it("should eventually load a single CA certificate from disk", function () {
it("should load a single CA certificate from disk", function () {
return expect(loadCredentials({ cert: null, key: null, ca: "test/support/initializeTest.crt" })
.get("ca").get(0).post("toString"))
.to.eventually.equal(cert.toString());
.ca[0].toString()).to.equal(cert.toString());
});
it("should eventually provide a single CA certificate from a Buffer", function () {
return expect(loadCredentials({ cert: null, key: null, ca: cert })
.get("ca").get(0).post("toString"))
.to.eventually.equal(cert.toString());
it("should provide a single CA certificate from a Buffer", function () {
return expect(loadCredentials({ cert: null, key: null, ca: cert }).ca[0].toString())
.to.equal(cert.toString());
});
it("should eventually provide a single CA certificate from a String", function () {
return expect(loadCredentials({ cert: null, key: null, ca: cert.toString() })
.get("ca").get(0))
.to.eventually.equal(cert.toString());
it("should provide a single CA certificate from a String", function () {
return expect(loadCredentials({ cert: null, key: null, ca: cert.toString() }).ca[0])
.to.equal(cert.toString());
});
it("should eventually load an array of CA certificates", function (done) {
loadCredentials({ cert: null, key: null, ca: ["test/support/initializeTest.crt", cert, cert.toString()] })
.get("ca").spread(function(cert1, cert2, cert3) {
var certString = cert.toString();
if (cert1.toString() === certString &&
cert2.toString() === certString &&
cert3.toString() === certString) {
done();
}
else {
done(new Error("provided certificates did not match"));
}
}, done);
it("should load an array of CA certificates", function () {
const certString = cert.toString();
return expect(loadCredentials({ cert: null, key: null,
ca: ["test/support/initializeTest.crt", cert, certString]
}).ca.map( cert => cert.toString() ))
.to.deep.equal([certString, certString, certString]);
});
it("returns undefined if no CA values are specified", function() {
return expect(loadCredentials({ cert: null, key: null, ca: null}).get("ca")).to.eventually.be.undefined;
return expect(loadCredentials({ cert: null, key: null, ca: null}).ca)
.to.be.undefined;
});
it("should inclue the passphrase in the resolved value", function() {
return expect(loadCredentials({ passphrase: "apntest" }).get("passphrase"))
.to.eventually.equal("apntest");
return expect(loadCredentials({ passphrase: "apntest" }).passphrase)
.to.equal("apntest");
});
});
});

@@ -1,43 +0,36 @@

var sinon = require("sinon");
var rewire = require("rewire");
var parseCredentials = rewire("../../lib/credentials/parse");
"use strict";
var APNCertificate = require("../../lib/credentials/APNCertificate");
var APNKey = require("../../lib/credentials/APNKey");
const sinon = require("sinon");
const APNCertificate = require("../../lib/credentials/APNCertificate");
const APNKey = require("../../lib/credentials/APNKey");
describe("parseCredentials", function() {
var reset;
var pkcs12Spy, pemKeySpy, pemCertSpy;
let fakes, parseCredentials;
var pfxKey = new APNKey({n: 1, e: 1 });
var pfxCert = new APNCertificate({publicKey: {}, validity: {}, subject: {} });
const pfxKey = new APNKey({n: 1, e: 1 });
const pfxCert = new APNCertificate({publicKey: {}, validity: {}, subject: {} });
var pemKey = new APNKey({n: 2, e: 1 });
var pemCert = new APNCertificate({publicKey: {}, validity: {}, subject: {} });
const pemKey = new APNKey({n: 2, e: 1 });
const pemCert = new APNCertificate({publicKey: {}, validity: {}, subject: {} });
beforeEach(function() {
pkcs12Spy = sinon.stub();
fakes = {
parsePkcs12: sinon.stub(),
parsePemKey: sinon.stub(),
parsePemCert: sinon.stub(),
};
pemKeySpy = sinon.stub();
pemKeySpy.withArgs("pemkey").returns(pemKey);
fakes.parsePemKey.withArgs("pemkey").returns(pemKey);
pemCertSpy = sinon.stub();
pemCertSpy.withArgs("pemcert").returns(pemCert);
fakes.parsePemKey.withArgs("pemcert").returns(pemCert);
reset = parseCredentials.__set__({
"parsePkcs12": pkcs12Spy,
"parsePemKey": pemKeySpy,
"parsePemCert": pemCertSpy,
});
parseCredentials = require("../../lib/credentials/parse")(fakes);
});
afterEach(function() {
reset();
});
describe("with PFX file", function() {
it("returns the parsed key", function() {
pkcs12Spy.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
fakes.parsePkcs12.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
var parsed = parseCredentials({ pfx: "pfxData" });
const parsed = parseCredentials({ pfx: "pfxData" });
expect(parsed.key).to.be.an.instanceof(APNKey);

@@ -47,5 +40,5 @@ });

it("returns the parsed certificates", function() {
pkcs12Spy.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
fakes.parsePkcs12.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
var parsed = parseCredentials({ pfx: "pfxData" });
const parsed = parseCredentials({ pfx: "pfxData" });
expect(parsed.certificates[0]).to.be.an.instanceof(APNCertificate);

@@ -56,8 +49,8 @@ });

beforeEach(function() {
pkcs12Spy.withArgs("encryptedPfxData", "apntest").returns({ key: pfxKey, certificates: [pfxCert] });
pkcs12Spy.withArgs("encryptedPfxData", sinon.match.any).throws(new Error("unable to read credentials, incorrect passphrase"));
fakes.parsePkcs12.withArgs("encryptedPfxData", "apntest").returns({ key: pfxKey, certificates: [pfxCert] });
fakes.parsePkcs12.withArgs("encryptedPfxData", sinon.match.any).throws(new Error("unable to read credentials, incorrect passphrase"));
});
it("returns the parsed key", function() {
var parsed = parseCredentials({ pfx: "encryptedPfxData", passphrase: "apntest" });
const parsed = parseCredentials({ pfx: "encryptedPfxData", passphrase: "apntest" });
expect(parsed.key).to.be.an.instanceof(APNKey);

@@ -82,5 +75,5 @@ });

it("returns the parsed key", function() {
pemKeySpy.withArgs("pemKeyData").returns(pemKey);
fakes.parsePemKey.withArgs("pemKeyData").returns(pemKey);
var parsed = parseCredentials({ key: "pemKeyData" });
const parsed = parseCredentials({ key: "pemKeyData" });
expect(parsed.key).to.be.an.instanceof(APNKey);

@@ -91,8 +84,8 @@ });

beforeEach(function() {
pemKeySpy.withArgs("encryptedPemKeyData", "apntest").returns(pemKey);
pemKeySpy.withArgs("encryptedPemKeyData", sinon.match.any).throws(new Error("unable to load key, incorrect passphrase"));
fakes.parsePemKey.withArgs("encryptedPemKeyData", "apntest").returns(pemKey);
fakes.parsePemKey.withArgs("encryptedPemKeyData", sinon.match.any).throws(new Error("unable to load key, incorrect passphrase"));
});
it("returns the parsed key", function() {
var parsed = parseCredentials({ key: "encryptedPemKeyData", passphrase: "apntest" });
const parsed = parseCredentials({ key: "encryptedPemKeyData", passphrase: "apntest" });
expect(parsed.key).to.be.an.instanceof(APNKey);

@@ -117,5 +110,5 @@ });

it("returns the parsed certificate", function() {
pemCertSpy.withArgs("pemCertData").returns([pemCert]);
fakes.parsePemCert.withArgs("pemCertData").returns([pemCert]);
var parsed = parseCredentials({ cert: "pemCertData" });
const parsed = parseCredentials({ cert: "pemCertData" });
expect(parsed.certificates[0]).to.be.an.instanceof(APNCertificate);

@@ -127,7 +120,7 @@ });

it("it prefers PFX to PEM", function() {
pkcs12Spy.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
pemKeySpy.withArgs("pemKeyData").returns(pemKey);
pemCertSpy.withArgs("pemCertData").returns([pemCert]);
fakes.parsePkcs12.withArgs("pfxData").returns({ key: pfxKey, certificates: [pfxCert] });
fakes.parsePemKey.withArgs("pemKeyData").returns(pemKey);
fakes.parsePemCert.withArgs("pemCertData").returns([pemCert]);
var parsed = parseCredentials({ pfx: "pfxData", key: "pemKeyData", cert: "pemCertData"});
const parsed = parseCredentials({ pfx: "pfxData", key: "pemKeyData", cert: "pemCertData"});
expect(parsed.key).to.equal(pfxKey);

@@ -134,0 +127,0 @@ expect(parsed.certificates[0]).to.equal(pfxCert);

@@ -25,15 +25,15 @@ var resolve = require("../../lib/credentials/resolve");

describe("with file path", function() {
it("eventually returns a Buffer for valid path", function() {
it("returns a Buffer for valid path", function() {
return expect(resolve("test/support/initializeTest.key"))
.to.eventually.satisfy(Buffer.isBuffer);
.to.satisfy(Buffer.isBuffer);
});
it("eventually returns contents for value path", function () {
it("returns contents for value path", function () {
return expect(resolve("test/support/initializeTest.key")
.post("toString")).to.eventually.equal(key.toString());
.toString()).to.equal(key.toString());
});
it("is eventually rejected for invalid path", function() {
return expect(resolve("test/support/fail/initializeTest.key"))
.to.eventually.be.rejected;
it("throws for invalid path", function() {
return expect(() => { resolve("test/support/fail/initializeTest.key") })
.to.throw;
});

@@ -46,2 +46,2 @@ });

});
});
});

@@ -1,28 +0,28 @@

var apn = require("../");
var Device = require("../lib/device");
describe("Device", function() {
describe("constructor", function () {
describe("constructor", function () {
// Issue #149
it("should error when given a device string which contains no hex characters and results in 0 length token", function () {
expect(function () {
apn.Device("som string without hx lttrs");
}).to.throw();
});
// Issue #149
it("should error when given a device string which contains no hex characters and results in 0 length token", function () {
expect(function () {
Device("som string without hx lttrs");
}).to.throw();
});
it("should error when given a device string which contains an odd number of hex characters", function () {
expect(function () {
apn.Device("01234");
}).to.throw();
});
it("should error when given a device string which contains an odd number of hex characters", function () {
expect(function () {
Device("01234");
}).to.throw();
});
it("should return a Device object containing the correct token when given a hex string", function () {
expect(apn.Device("<0123 4567 89AB CDEF>").toString()).to.equal("0123456789abcdef");
});
it("should return a Device object containing the correct token when given a hex string", function () {
expect(Device("<0123 4567 89AB CDEF>").toString()).to.equal("0123456789abcdef");
});
it("should return a Device object containing the correct token when given a Buffer", function () {
var buf = new Buffer([1, 35, 69, 103, 137, 171, 205, 239]);
expect(apn.Device(buf).toString()).to.equal("0123456789abcdef");
});
});
});
it("should return a Device object containing the correct token when given a Buffer", function () {
var buf = new Buffer([1, 35, 69, 103, 137, 171, 205, 239]);
expect(Device(buf).toString()).to.equal("0123456789abcdef");
});
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc