PageRenderTime 91ms CodeModel.GetById 2ms app.highlight 65ms RepoModel.GetById 18ms app.codeStats 0ms

/lib/connection.js

https://github.com/justinleoye/node-apn
JavaScript | 780 lines | 548 code | 94 blank | 138 comment | 140 complexity | 671208172f1e369118a2d3d97c12db3d MD5 | raw file
  1var Errors = require('./errors');
  2
  3var fs   = require('fs');
  4var q    = require('q');
  5var tls  = require('tls');
  6var net  = require('net');
  7var sysu = require('util');
  8var util = require('./util');
  9var Device = require('./device');
 10var events = require('events');
 11var debug = function() {};
 12if(process.env.DEBUG) {
 13	try {
 14		debug = require('debug')('apn');
 15	}
 16	catch (e) {
 17		console.log("Notice: 'debug' module is not available. This should be installed with `npm install debug` to enable debug messages", e);
 18		debug = function() {};
 19	}
 20}
 21
 22/**
 23 * Create a new connection to the APN service.
 24 * @constructor
 25 * @param {Object} [options]
 26 * @config {Buffer|String} [cert="cert.pem"] The filename of the connection certificate to load from disk, or a Buffer/String containing the certificate data.
 27 * @config {Buffer|String} [key="key.pem"] The filename of the connection key to load from disk, or a Buffer/String containing the key data.
 28 * @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).
 29 * @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.
 30 * @config {String} [passphrase] The passphrase for the connection key, if required
 31 * @config {Boolean} [production=(NODE_ENV=='production')] Specifies which environment to connect to: Production (if true) or Sandbox. (Defaults to false, unless NODE_ENV == "production")
 32 * @config {Number} [port=2195] Gateway port
 33 * @config {Boolean} [rejectUnauthorized=true] Reject Unauthorized property to be passed through to tls.connect()
 34 * @config {Boolean} [enhanced=true] Whether to use the enhanced notification format (recommended)
 35 * @config {Function} [errorCallback] A callback which accepts 2 parameters (err, notification). Use `transmissionError` event instead.
 36 * @config {Number} [cacheLength=100] Number of notifications to cache for error purposes (See doc/apn.markdown)
 37 * @config {Boolean} [autoAdjustCache=false] Whether the cache should grow in response to messages being lost after errors. (Will still emit a 'cacheTooSmall' event)
 38 * @config {Number} [maxConnections=1] The maximum number of connections to create for sending messages.
 39 * @config {Number} [connectionTimeout=0] The duration the socket should stay alive with no activity in milliseconds. 0 = Disabled.
 40 * @config {Boolean} [buffersNotifications=true] Whether to buffer notifications and resend them after failure.
 41 * @config {Boolean} [fastMode=false] Whether to aggresively empty the notification buffer while connected.
 42 * @config {Boolean} [legacy=false] Whether to use the old (pre-iOS 7) protocol format.
 43 */
 44function Connection (options) {
 45	if(false === (this instanceof Connection)) {
 46        return new Connection(options);
 47    }
 48	this.options = {
 49		cert: 'cert.pem',
 50		key: 'key.pem',
 51		ca: null,
 52		pfx: null,
 53		passphrase: null,
 54		production: (process.env.NODE_ENV === "production"),
 55		address: null,
 56		port: 2195,
 57		rejectUnauthorized: true,
 58		enhanced: true,
 59		cacheLength: 100,
 60		autoAdjustCache: true,
 61		maxConnections: 1,
 62		connectionTimeout: 0,
 63		buffersNotifications: true,
 64		fastMode: false,
 65		legacy: false,
 66		disableNagle: false
 67	};
 68
 69	for (var key in options) {
 70		if (options[key] == null) {
 71			debug("Option [" + key + "] set to null. This may cause unexpected behaviour.");
 72		}
 73	}
 74
 75	util.extend(this.options, options);
 76
 77	if (this.options.gateway != null) {
 78		this.options.address = this.options.gateway;
 79	}
 80
 81	if (this.options.address == null) {
 82		if (this.options.production) {
 83			this.options.address = "gateway.push.apple.com";
 84		}
 85		else {
 86			this.options.address = "gateway.sandbox.push.apple.com";
 87		}
 88	}
 89
 90	if (this.options.pfx || this.options.pfxData) {
 91		if (!options.cert) {
 92			this.options.cert = null;
 93		}
 94		if (!options.key) {
 95			this.options.key = null;
 96		}
 97	}
 98
 99	this.initializationPromise = null;
100	this.deferredConnection = null;
101
102	this.sockets = [];
103	this.notificationBuffer  = [];
104
105	this.socketId = 0;
106
107	this.failureCount = 0;
108
109	// when true, we end all sockets after the pending notifications reach 0
110	this.shutdownPending = false;
111
112	events.EventEmitter.call(this);
113}
114
115sysu.inherits(Connection, events.EventEmitter);
116
117/**
118 * You should never need to call this method, initialization and connection is handled by {@link Connection#sendNotification}
119 * @private
120 */
121Connection.prototype.initialize = function () {
122	if (this.initializationPromise) {
123		return this.initializationPromise;
124	}
125
126	debug("Initialising module");
127	var readFile = q.nfbind(fs.readFile);
128
129	// Prepare PKCS#12 data if available
130	var pfxPromise = null;
131	if(this.options.pfx != null || this.options.pfxData != null) {
132		if(this.options.pfxData) {
133			pfxPromise = this.options.pfxData;
134		}
135		else if(Buffer.isBuffer(this.options.pfx)) {
136			pfxPromise = this.options.pfx;
137		}
138		else {
139			pfxPromise = readFile(this.options.pfx);
140		}
141	}
142
143	// Prepare Certificate data if available.
144	var certPromise = null;
145	if (this.options.certData) {
146		certPromise = this.options.certData;
147	}
148	else if(Buffer.isBuffer(this.options.cert) || checkPEMType(this.options.cert, "CERTIFICATE")) {
149		certPromise = this.options.cert;
150	}
151	else if(this.options.cert){
152		// Nothing has matched so attempt to load from disk
153		certPromise = readFile(this.options.cert);
154	}
155
156	// Prepare Key data if available
157	var keyPromise = null;
158	if (this.options.keyData) {
159		keyPromise = this.options.keyData;
160	}
161	else if(Buffer.isBuffer(this.options.key) || checkPEMType(this.options.key, "PRIVATE KEY")) {
162		keyPromise = this.options.key;
163	}
164	else if(this.options.key) {
165		keyPromise = readFile(this.options.key);
166	}
167
168	// Prepare Certificate Authority data if available.
169	var caPromises = [];
170	if (this.options.ca != null && !sysu.isArray(this.options.ca)) {
171		this.options.ca = [ this.options.ca ];
172	}
173	for(var i in this.options.ca) {
174		var ca = this.options.ca[i];
175		if(Buffer.isBuffer(ca) || checkPEMType(ca, "CERTIFICATE")) {
176			caPromises.push(ca);
177		}
178		else if (ca){
179			caPromises.push(readFile(ca));
180		}
181	}
182	if (caPromises.length == 0) {
183		caPromises = undefined;
184	}
185	else {
186		caPromises = q.all(caPromises);
187	}
188
189	this.initializationPromise = q.all([pfxPromise, certPromise, keyPromise, caPromises]);
190	return this.initializationPromise;
191};
192
193function checkPEMType(input, type) {
194	if (input == null) {
195		return;
196	}
197	var matches = input.match(/\-\-\-\-\-BEGIN ([A-Z\s*]+)\-\-\-\-\-/);
198	
199	if (matches != null) {
200		return matches[1].indexOf(type) >= 0;
201	}
202	return false;
203}
204
205/**
206 * You should never need to call this method, initialisation and connection is handled by {@link Connection#pushNotification}
207 * @private
208 */
209Connection.prototype.connect = function () {
210	if (this.deferredConnection) {
211		return this.deferredConnection.promise;
212	}
213
214	debug("Initialising connection");
215	this.deferredConnection = q.defer();
216	this.initialize().spread(function (pfxData, certData, keyData, caData) {
217		var socketOptions = {};
218
219		socketOptions.pfx = pfxData;
220		socketOptions.cert = certData;
221		socketOptions.key = keyData;
222		socketOptions.ca = caData;
223		socketOptions.passphrase = this.options.passphrase;
224		socketOptions.rejectUnauthorized = this.options.rejectUnauthorized;
225
226		// We pass in our own Stream to delay connection until we have attached the
227		//  event listeners below.
228		socketOptions.socket = new net.Socket();
229
230		this.socket = tls.connect(
231			this.options['port'],
232			this.options['address'],
233			socketOptions,
234			function () {
235				debug("Connection established");
236				this.emit('connected', this.sockets.length + 1);
237				this.deferredConnection.resolve();
238			}.bind(this));
239
240
241		socketOptions.socket.setNoDelay(this.options.disableNagle);
242		socketOptions.socket.setKeepAlive(true);
243		if (this.options.connectionTimeout > 0) {
244			socketOptions.socket.setTimeout(this.options.connectionTimeout);
245		}
246
247		this.socket.on("error", this.errorOccurred.bind(this, this.socket));
248		this.socket.on("timeout", this.socketTimeout.bind(this, this.socket));
249		this.socket.on("data", this.handleTransmissionError.bind(this, this.socket));
250		this.socket.on("drain", this.socketDrained.bind(this, this.socket, true));
251		this.socket.once("close", this.socketClosed.bind(this, this.socket));
252
253		// The actual connection is delayed until after all the event listeners have
254		//  been attached.
255		if ("function" == typeof this.socket.connect ) {
256			this.socket.connect(this.options['port'], this.options['address']);
257		}
258		else {
259			socketOptions.socket.connect(this.options['port'], this.options['address']);
260		}
261	}.bind(this)).fail(function (error) {
262		debug("Module initialisation error:", error);
263
264		// 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.
265		while(this.notificationBuffer.length > 0) {
266			var notification = this.notificationBuffer.shift();
267			this.raiseError(error, notification.notification, notification.recipient);
268			this.emit('transmissionError', Errors['moduleInitialisationFailed'], notification.notification, notification.recipient);
269		}
270		this.deferredConnection.reject(error);
271		this.deferredConnection = null;
272	}.bind(this));
273
274	return this.deferredConnection.promise;
275};
276
277/**
278 * @private
279 */
280Connection.prototype.createConnection = function() {
281	this.connect().then(function () {
282		this.failureCount = 0;
283
284		this.socket.socketId = this.socketId++;
285		this.socket.currentId = 0;
286		this.socket.cachedNotifications = [];
287
288		this.sockets.push(this.socket);
289	}.bind(this)).fail(function (error) {
290		// Exponential backoff when connections fail.
291		var delay = Math.pow(2, this.failureCount++) * 1000;
292		this.raiseError(error);
293		this.emit('error', error);
294
295		return q.delay(delay);
296	}.bind(this)).finally(function () {
297		this.deferredConnection = null;
298		this.socket = undefined;
299		this.serviceBuffer();
300	}.bind(this));
301};
302
303/**
304 * @private
305 */
306Connection.prototype.initialisingConnection = function() {
307	if(this.deferredConnection !== null) {
308		return true;
309	}
310	return false;
311};
312
313/**
314 * @private
315 */
316 Connection.prototype.serviceBuffer = function() {
317
318	var socket = null;
319	var repeat = false;
320	if(this.options.fastMode) {
321		repeat = true;
322	}
323	do {
324		socket = null;
325		if (this.notificationBuffer.length === 0) break;
326		for (var i = this.sockets.length - 1; i >= 0; i--) {
327			if(this.socketAvailable(this.sockets[i])) {
328				socket = this.sockets[i];
329				break;
330			}
331		}
332		if (socket !== null) {
333			debug("Transmitting notification from buffer");
334			if(this.transmitNotification(socket, this.notificationBuffer.shift())) {
335				this.socketDrained(socket, !repeat);
336			}
337		}
338		else if (!this.initialisingConnection() && this.sockets.length < this.options.maxConnections) {
339			this.createConnection();
340			repeat = false;
341		}
342		else {
343			repeat = false;
344		}
345	} while(repeat);
346	debug("%d left to send", this.notificationBuffer.length);
347
348	if (this.notificationBuffer.length === 0 && this.shutdownPending) {
349		debug("closing connections");
350
351		for (var i = this.sockets.length - 1; i >= 0; i--) {
352			var socket = this.sockets[i];
353			if (!socket.busy) {
354				// We delay before closing connections to ensure we don't miss any error packets from the service.
355				setTimeout(socket.end.bind(socket), 2500);
356			}
357		}
358		if (this.sockets.length == 0) {
359			this.shutdownPending = false;
360		}
361	}
362 };
363
364/**
365 * @private
366 */
367Connection.prototype.errorOccurred = function(socket, err) {
368	debug("Socket error occurred", socket.socketId, err);
369
370	if(socket.transmissionErrorOccurred && err.code == 'EPIPE') {
371		debug("EPIPE occurred after a transmission error which we can ignore");
372		return;
373	}
374
375	this.emit('socketError', err);
376	if(this.socket == socket && this.deferredConnection && this.deferredConnection.promise.isPending()) {
377		this.deferredConnection.reject(err);
378	}
379	else {
380		this.raiseError(err, null);
381	}
382
383	if(socket.busy && socket.cachedNotifications.length > 0) {
384		// A notification was in flight. It should be buffered for resending.
385		this.bufferNotification(socket.cachedNotifications[socket.cachedNotifications.length - 1]);
386	}
387
388	this.destroyConnection(socket);
389};
390
391/**
392 * @private
393 */
394Connection.prototype.socketAvailable = function(socket) {
395	if (!socket || !socket.writable || socket.busy || socket.transmissionErrorOccurred) {
396		return false;
397	}
398	return true;
399};
400
401/**
402 * @private
403 */
404Connection.prototype.socketDrained = function(socket, serviceBuffer) {
405	debug("Socket drained", socket.socketId);
406	socket.busy = false;
407	if(this.options.enhanced) {
408		var notification = socket.cachedNotifications[socket.cachedNotifications.length - 1];
409		this.emit('transmitted', notification.notification, notification.recipient);
410	}
411	if(serviceBuffer === true && !this.runningOnNextTick) {
412		// There is a possibility that this could add multiple invocations to the 
413		// call stack unnecessarily. It will be resolved within one event loop but 
414		// should be mitigated if possible, this.nextTick aims to solve this, 
415		// ensuring "serviceBuffer" is only called once per loop.
416		var nextRun = function() { this.runningOnNextTick = false; this.serviceBuffer(); }.bind(this);
417		if('function' === typeof setImmediate) {
418			setImmediate(nextRun);
419		}
420		else {
421			process.nextTick(nextRun);
422		}
423		this.runningOnNextTick = true;
424	}
425};
426
427/**
428 * @private
429 */
430 Connection.prototype.socketTimeout = function(socket) {
431	debug("Socket timeout", socket.socketId);
432	this.emit('timeout');
433	this.destroyConnection(socket);
434 };
435
436/**
437 * @private
438 */
439Connection.prototype.destroyConnection = function(socket) {
440	debug("Destroying connection", socket.socketId);
441	if (socket) {
442		socket.destroy();
443	}
444};
445
446/**
447 * @private
448 */
449Connection.prototype.socketClosed = function(socket) {
450	debug("Socket closed", socket.socketId);
451
452	if (socket === this.socket && this.deferredConnection.promise.isPending()) {
453		debug("Connection error occurred before TLS Handshake");
454		this.deferredConnection.reject(new Error("Unable to connect"));
455	}
456	else {
457		var index = this.sockets.indexOf(socket);
458		if (index > -1) {
459			this.sockets.splice(index, 1);
460		}
461
462		this.emit('disconnected', this.sockets.length);
463	}
464
465	this.serviceBuffer();
466};
467
468/**
469 * Use this method to modify the cache length after initialisation.
470 */
471Connection.prototype.setCacheLength = function(newLength) {
472	this.options.cacheLength = newLength;
473};
474
475/**
476 * @private
477 */
478Connection.prototype.bufferNotification = function (notification) {
479	if (notification.retryLimit === 0) {
480		this.raiseError(Errors['retryLimitExceeded'], notification);
481		this.emit('transmissionError', Errors['retryLimitExceeded'], notification.notification, notification.recipient);
482		return;
483	}
484	notification.retryLimit -= 1;
485	this.notificationBuffer.push(notification);
486};
487
488/**
489 * @private
490 */
491Connection.prototype.prepareNotification = function (notification, device) {
492	var recipient = device;
493	// If a device token hasn't been given then we should raise an error.
494	if (recipient === undefined) {
495		process.nextTick(function () {
496			this.raiseError(Errors['missingDeviceToken'], notification);
497			this.emit('transmissionError', Errors['missingDeviceToken'], notification);
498		}.bind(this));
499		return;
500	}
501
502	// If we have been passed a token instead of a `Device` then we should convert.
503	if (!(recipient instanceof Device)) {
504		try {
505			recipient = new Device(recipient);
506		}
507		catch (e) {
508			// If an exception has been thrown it's down to an invalid token.
509			process.nextTick(function () {
510				this.raiseError(Errors['invalidToken'], notification, device);
511				this.emit('transmissionError', Errors['invalidToken'], notification, device);
512			}.bind(this));
513			return;
514		}
515	}
516
517	var retryLimit = (notification.retryLimit < 0) ? -1 : notification.retryLimit + 1;
518	this.bufferNotification( { "notification": notification, "recipient": recipient, "retryLimit": retryLimit } );
519};
520
521/**
522 * @private
523 */
524Connection.prototype.cacheNotification = function (socket, notification) {
525	socket.cachedNotifications.push(notification);
526	if (socket.cachedNotifications.length > this.options.cacheLength) {
527		debug("Clearing notification %d from the cache", socket.cachedNotifications[0]['_uid']);
528		socket.cachedNotifications.splice(0, socket.cachedNotifications.length - this.options.cacheLength);
529	}
530};
531
532/**
533 * @private
534 */
535Connection.prototype.handleTransmissionError = function (socket, data) {
536	if (data[0] == 8) {
537		socket.transmissionErrorOccurred = true;
538		if (!this.options.enhanced) {
539			return;
540		}
541
542		var errorCode = data[1];
543		var identifier = data.readUInt32BE(2);
544		var notification = null;
545		var foundNotification = false;
546		var temporaryCache = [];
547
548		debug("Notification %d caused an error: %d", identifier, errorCode);
549
550		while (socket.cachedNotifications.length) {
551			notification = socket.cachedNotifications.shift();
552			if (notification['_uid'] == identifier) {
553				foundNotification = true;
554				break;
555			}
556			temporaryCache.push(notification);
557		}
558
559		if (foundNotification) {
560			while (temporaryCache.length) {
561				temporaryCache.shift();
562			}
563			this.emit('transmissionError', errorCode, notification.notification, notification.recipient);
564			this.raiseError(errorCode, notification.notification, notification.recipient);
565		}
566		else {
567			socket.cachedNotifications = temporaryCache;
568
569			if(socket.cachedNotifications.length > 0) {
570				var differentialSize = socket.cachedNotifications[0]['_uid'] - identifier;
571				this.emit('cacheTooSmall', differentialSize);
572				if(this.options.autoAdjustCache) {
573					this.options.cacheLength += differentialSize * 2;
574				}
575			}
576
577			this.emit('transmissionError', errorCode, null);
578			this.raiseError(errorCode, null);
579		}
580
581		var count = socket.cachedNotifications.length;
582		if(this.options.buffersNotifications) {
583			debug("Buffering %d notifications for resending", count);
584			for (var i = 0; i < count; ++i) {
585				notification = socket.cachedNotifications.shift();
586				this.bufferNotification(notification);
587			}
588		}
589	}
590	else {
591		debug("Unknown data received: ", data);
592	}
593};
594
595/**
596 * @private
597 */
598Connection.prototype.raiseError = function(errorCode, notification, recipient) {
599	debug("Raising error:", errorCode, notification, recipient);
600
601	if(errorCode instanceof Error) {
602		debug("Error occurred with trace:", errorCode.stack);
603	}
604
605	if (notification && typeof notification.errorCallback == 'function' ) {
606		notification.errorCallback(errorCode, recipient);
607	} else if (typeof this.options.errorCallback == 'function') {
608		this.options.errorCallback(errorCode, notification, recipient);
609	}
610};
611
612/**
613 * @private
614 * @return {Boolean} Write completed, returns true if socketDrained should be called by the caller of this method.
615 */
616Connection.prototype.transmitNotification = function(socket, notification) {
617	if (!this.socketAvailable(socket)) {
618		this.bufferNotification(notification);
619		return;
620	}
621
622	var token = notification.recipient.token;
623	var encoding = notification.notification.encoding || 'utf8';
624	var message = notification.notification.compile();
625	var messageLength = Buffer.byteLength(message, encoding);
626	var position = 0;
627	var data;
628
629	notification._uid = socket.currentId++;
630	if (socket.currentId > 0xffffffff) {
631		socket.currentId = 0;
632	}
633	if (this.options.legacy) {
634		if (this.options.enhanced) {
635			data = new Buffer(1 + 4 + 4 + 2 + token.length + 2 + messageLength);
636			// Command
637			data[position] = 1;
638			position++;
639
640			// Identifier
641			data.writeUInt32BE(notification._uid, position);
642			position += 4;
643
644			// Expiry
645			data.writeUInt32BE(notification.notification.expiry, position);
646			position += 4;
647			this.cacheNotification(socket, notification);
648		}
649		else {
650			data = new Buffer(1 + 2 + token.length + 2 + messageLength);
651			//Command
652			data[position] = 0;
653			position++;
654		}
655		// Token Length
656		data.writeUInt16BE(token.length, position);
657		position += 2;
658		// Device Token
659		position += token.copy(data, position, 0);
660		// Payload Length
661		data.writeUInt16BE(messageLength, position);
662		position += 2;
663		//Payload
664		position += data.write(message, position, encoding);
665	}
666	else {
667		// New Protocol uses framed notifications consisting of multiple items
668		// 1: Device Token
669		// 2: Payload
670		// 3: Notification Identifier
671		// 4: Expiration Date
672		// 5: Priority
673		// Each item has a 3 byte header: Type (1), Length (2) followed by data
674		// The frame layout is hard coded for now as original dynamic system had a
675		// significant performance penalty
676
677		var frameLength = 3 + token.length + 3 + messageLength + 3 + 4;
678		if(notification.notification.expiry > 0) {
679			frameLength += 3 + 4;
680		}
681		if(notification.notification.priority != 10) {
682			frameLength += 3 + 1;
683		}
684
685		// Frame has a 5 byte header: Type (1), Length (4) followed by items.
686		data = new Buffer(5 + frameLength);
687		data[position] = 2; position += 1;
688
689		// Frame Length
690		data.writeUInt32BE(frameLength, position); position += 4;
691
692		// Token Item
693		data[position] = 1; position += 1;
694		data.writeUInt16BE(token.length, position); position += 2;
695		position += token.copy(data, position, 0);
696
697		// Payload Item
698		data[position] = 2; position += 1;
699		data.writeUInt16BE(messageLength, position); position += 2;
700		position += data.write(message, position, encoding);
701
702		// Identifier Item
703		data[position] = 3; position += 1;
704		data.writeUInt16BE(4, position); position += 2;
705		data.writeUInt32BE(notification._uid, position); position += 4;
706
707		if(notification.notification.expiry > 0) {
708			// Expiry Item
709			data[position] = 4; position += 1;
710			data.writeUInt16BE(4, position); position += 2;
711			data.writeUInt32BE(notification.notification.expiry, position); position += 4;
712		}
713		if(notification.notification.priority != 10) {
714			// Priority Item
715			data[position] = 5; position += 1;
716			data.writeUInt16BE(1, position); position += 2;
717			data[position] = notification.notification.priority; position += 1;
718		}
719
720		this.cacheNotification(socket, notification);
721	}
722
723	socket.busy = true;
724	return socket.write(data);
725};
726
727Connection.prototype.validNotification = function (notification, recipient) {
728	var messageLength = notification.length();
729
730	if (messageLength > 256) {
731		process.nextTick(function () {
732			this.raiseError(Errors['invalidPayloadSize'], notification, recipient);
733			this.emit('transmissionError', Errors['invalidPayloadSize'], notification, recipient);
734		}.bind(this));
735		return false;
736	}
737	notification.compile();
738	return true;
739};
740
741/**
742 * Queue a notification for delivery to recipients
743 * @param {Notification} notification The Notification object to be sent
744 * @param {Device|String|Buffer|Device[]|String[]|Buffer[]} recipient The token(s) for devices the notification should be delivered to.
745 * @since v1.3.0
746 */
747Connection.prototype.pushNotification = function (notification, recipient) {
748	if (!this.validNotification(notification, recipient)) {
749		return;
750	}
751	if (sysu.isArray(recipient)) {
752		for (var i = recipient.length - 1; i >= 0; i--) {
753			this.prepareNotification(notification, recipient[i]);
754		}
755	}
756	else {
757		this.prepareNotification(notification, recipient);
758	}
759
760	this.serviceBuffer();
761};
762
763/**
764 * Send a notification to the APN service
765 * @param {Notification} notification The notification object to be sent
766 * @deprecated Since v1.3.0, use pushNotification instead
767 */
768Connection.prototype.sendNotification = function (notification) {
769	return this.pushNotification(notification, notification.device);
770};
771
772/**
773 * End connections with APNS once we've finished sending all notifications
774 */
775Connection.prototype.shutdown = function () {
776	debug("Shutdown pending");
777	this.shutdownPending = true;
778};
779
780module.exports = Connection;