node-apn/lib/feedback.js

312 строки
9.5 KiB
JavaScript

var Device = require('./device');
var Errors = require('./errors');
var fs = require('fs');
var q = require('q');
var tls = require('tls');
var sysu = require('util');
var util = require('./util');
var events = require('events');
var debug = function() {};
if(process.env.DEBUG) {
try {
debug = require('debug')('apnfb');
}
catch (e) {
console.log("Notice: 'debug' module is not available. This should be installed with `npm install debug` to enable debug messages", e);
debug = function() {};
}
}
/**
* Create a new connection to the APN Feedback.
* @constructor
* @param {Object} [options]
* @config {String} [cert="cert.pem"] The filename of the connection certificate to load from disk
* @config {Buffer|String} [certData] The certificate data. If supplied, will be used instead of loading from disk.
* @config {String} [key="key.pem"] The filename of the connection key to load from disk
* @config {Buffer|String} [keyData] The key data. If supplied will be used instead of loading from disk.
* @config {Buffer[]|String[]} [ca] An array of strings or Buffers of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. - You may need to use this as some environments don't include the CA used by Apple.
* @config {String} [pfx] File path for private key, certificate and CA certs in PFX or PKCS12 format. If supplied will be used instead of certificate and key above
* @config {Buffer|String} [pfxData] PFX or PKCS12 format data containing the private key, certificate and CA certs. If supplied will be used instead of loading from disk.
* @config {String} [passphrase] The passphrase for the connection key, if required
* @config {String} [address="feedback.push.apple.com"] The feedback server to connect to.
* @config {Number} [port=2195] Feedback server port
* @config {Function} [feedback] Deprecated ** A callback which accepts 2 parameters (timestamp, {@link Device}) or an array of (timestamp, {@link Device}) object tuples, depending on the value of batchFeedback option. See: {@link <a href="https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3">Communicating with APS</a>.
* @config {Boolean} [batchFeedback=true] If true, the feedback callback will only be called once per connection with an array of timestamp and device token tuples.
* @config {Function} [errorCallback] Deprecated ** Callback which will capture connection errors
* @config {Number} [interval=3600] Interval to automatically connect to the Feedback service.
*/
function Feedback(options) {
this.options = {
cert: 'cert.pem', /* Certificate file */
certData: null, /* Certificate data */
key: 'key.pem', /* Key file */
keyData: null, /* Key data */
ca: null, /* Certificate Authority */
pfx: null, /* PFX File */
pfxData: null, /* PFX Data */
passphrase: null, /* Passphrase for key */
address: 'feedback.push.apple.com', /* feedback address */
port: 2196, /* feedback port */
rejectUnauthorized: true, /* Set this to false incase using a local proxy, reject otherwise */
feedback: false, /* **Deprecated**: Use `feedback` event instead, enable feedback service, set to callback */
batchFeedback: true, /* If the feedback callback should only be called once per connection. */
errorCallback: false, /* error handler to catch connection exceptions */
interval: 3600 /* interval in seconds to connect to feedback service */
};
util.extend(this.options, options);
this.certData = null;
this.keyData = null;
this.pfxData = null;
this.deferredInitialize = null;
this.deferredConnection = null;
this.readBuffer = null;
this.interval = null;
events.EventEmitter.call(this);
if (typeof this.options.errorCallback == 'function') {
this.on('error', this.options.errorCallback);
}
if (typeof this.options.feedback == 'function') {
this.on('feedback', this.options.feedback);
}
process.nextTick(function() {
if(this.listeners('feedback').length === 0) {
debug("WARNING: A `feedback` listener has not been specified. Data may be lost.");
}
}.bind(this));
this.start();
}
sysu.inherits(Feedback, events.EventEmitter);
/**
* @private
*/
Feedback.prototype.checkInitialized = function () {
if ((this.keyData && this.certData) || this.pfxData) {
this.deferredInitialize.resolve();
}
};
/**
* @private
*/
Feedback.prototype.initialize = function () {
if (this.deferredInitialize) {
return this.deferredInitialize.promise;
}
debug("Initialising module");
this.deferredInitialize = q.defer();
if (this.options.pfx !== null || this.options.pfxData !== null) {
if (this.options.pfxData) {
this.pfxData = this.options.pfxData;
}
else {
fs.readFile(this.options.pfx, function (err, data) {
if (err) {
this.deferredInitialize.reject(err);
return;
}
this.pfxData = data;
this.checkInitialized();
}.bind(this));
}
}
else {
if (this.options.certData) {
this.certData = this.options.certData;
}
else {
fs.readFile(this.options.cert, function (err, data) {
if (err) {
this.deferredInitialize.reject(err);
return;
}
this.certData = data.toString();
this.checkInitialized();
}.bind(this));
}
if (this.options.keyData) {
this.keyData = this.options.keyData;
}
else {
fs.readFile(this.options.key, function (err, data) {
if (err) {
this.deferredInitialize.reject(err);
return;
}
this.keyData = data.toString();
this.checkInitialized();
}.bind(this));
}
}
this.checkInitialized();
return this.deferredInitialize.promise;
};
/**
* You should call {@link Feedback#start} instead of this method
* @private
*/
Feedback.prototype.connect = function () {
if(this.deferredConnection) {
return this.deferredConnection.promise;
}
debug("Initialising connection");
this.deferredConnection = q.defer();
this.initialize().then(function() {
var socketOptions = {};
if (this.pfxData !== null) {
socketOptions.pfx = this.pfxData;
}
else {
socketOptions.key = this.keyData;
socketOptions.cert = this.certData;
socketOptions.ca = this.options.ca;
}
socketOptions.passphrase = this.options.passphrase;
socketOptions.rejectUnauthorized = this.options.rejectUnauthorized;
this.socket = tls.connect(
this.options['port'],
this.options['address'],
socketOptions,
function () {
debug("Connection established");
this.deferredConnection.resolve();
}.bind(this));
this.readBuffer = new Buffer(0);
this.feedbackData = [];
this.socket.on('data', this.receive.bind(this));
this.socket.on("error", this.destroyConnection.bind(this));
this.socket.once('close', this.resetConnection.bind(this));
}.bind(this)).fail(function (error) {
debug("Module initialisation error:", error);
this.emit('error', error);
this.deferredConnection.reject(error);
this.deferredConnection = null;
}.bind(this));
return this.deferredConnection.promise;
};
/**
* @private
*/
Feedback.prototype.receive = function (data) {
var time = 0;
var tokenLength = 0;
var token = null;
debug("Received packet of length: %d", data.length);
var newBuffer = new Buffer(this.readBuffer.length + data.length);
this.readBuffer.copy(newBuffer);
data.copy(newBuffer, this.readBuffer.length);
this.readBuffer = newBuffer;
while (this.readBuffer.length > 6) {
time = this.readBuffer.readUInt32BE(0);
tokenLength = this.readBuffer.readUInt16BE(4);
if ((this.readBuffer.length - 6) < tokenLength) {
return;
}
token = new Buffer(tokenLength);
this.readBuffer.copy(token, 0, 6, 6 + tokenLength);
debug("Parsed device token: %s, timestamp: %d", token.toString("hex"), time);
var device = new Device(token);
if (!this.options.batchFeedback) {
debug("Emitting feedback event");
this.emit('feedback', time, device);
} else {
this.feedbackData.push({ time: time, device: device });
}
this.readBuffer = this.readBuffer.slice(6 + tokenLength);
}
};
/**
* @private
*/
Feedback.prototype.destroyConnection = function (err) {
debug("Destroying connection");
if(err) {
this.emit('feedbackError', err);
}
if (this.socket) {
this.socket.destroySoon();
}
};
/**
* @private
*/
Feedback.prototype.resetConnection = function () {
debug("Resetting connection");
if (this.options.batchFeedback) {
debug("Emitting all feedback tokens");
this.emit('feedback', this.feedbackData);
this.feedbackData = [];
}
if(this.deferredConnection.promise.isPending()) {
debug("Connection error occurred before TLS Handshake");
this.deferredConnection.reject(new Error("Unable to connect"));
}
this.socket = null;
this.deferredConnection = null;
};
/**
* Connect to the feedback service, also initialise the timer if an interval is specified.
*/
Feedback.prototype.start = function () {
debug("Starting feedback service");
this.cancel();
if (this.options.interval > 0) {
debug("Feedback service interval set at: %d", this.options.interval);
this.interval = setInterval(this.request.bind(this), this.options.interval * 1000);
}
this.request();
};
/**
* @private
*/
Feedback.prototype.request = function () {
debug("Performing feedback request");
this.connect().fail(function (error) {
this.emit('feedbackError');
}.bind(this));
};
/**
* Cancel the timer to stop the Feedback service periodically connecting.
*/
Feedback.prototype.cancel = function () {
debug("Cancelling feedback interval");
if (this.interval !== undefined) {
clearInterval(this.interval);
}
};
module.exports = Feedback;