/lib/connection.js
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;