1
0

APNS.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. 'use strict';
  2. import apn from '@parse/node-apn';
  3. import Parse from 'parse';
  4. import log from 'npmlog';
  5. const LOG_PREFIX = 'parse-server-push-adapter APNS';
  6. export class APNS {
  7. /**
  8. * Create a new provider for the APN service.
  9. * @constructor
  10. * @param {Object|Array} args An argument or a list of arguments to config APNS provider
  11. * @param {Object} args.token {Object} Configuration for Provider Authentication Tokens. (Defaults to: null i.e. fallback to Certificates)
  12. * @param {Buffer|String} args.token.key The filename of the provider token key (as supplied by Apple) to load from disk, or a Buffer/String containing the key data.
  13. * @param {String} args.token.keyId The ID of the key issued by Apple
  14. * @param {String} args.token.teamId ID of the team associated with the provider token key
  15. * @param {Buffer|String} args.cert The filename of the connection certificate to load from disk, or a Buffer/String containing the certificate data.
  16. * @param {Buffer|String} args.key {Buffer|String} The filename of the connection key to load from disk, or a Buffer/String containing the key data.
  17. * @param {Buffer|String} args.pfx path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer containing the PFX data. If supplied will always be used instead of certificate and key above.
  18. * @param {String} args.passphrase The passphrase for the provider key, if required
  19. * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
  20. * @param {String} args.topic Specififies an App-Id for this Provider
  21. * @param {String} args.bundleId DEPRECATED: Specifies an App-ID for this Provider
  22. * @param {Number} args.connectionRetryLimit The maximum number of connection failures that will be tolerated before apn.Provider will "give up". (Defaults to: 3)
  23. */
  24. constructor(args) {
  25. // Define class members
  26. this.providers = [];
  27. // Since for ios, there maybe multiple cert/key pairs, typePushConfig can be an array.
  28. let apnsArgsList = [];
  29. if (Array.isArray(args)) {
  30. apnsArgsList = apnsArgsList.concat(args);
  31. } else if (typeof args === 'object') {
  32. apnsArgsList.push(args);
  33. } else {
  34. throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'APNS Configuration is invalid');
  35. }
  36. // Create Provider from each arg-object
  37. for (const apnsArgs of apnsArgsList) {
  38. // rewrite bundleId to topic for backward-compatibility
  39. if (apnsArgs.bundleId) {
  40. log.warn(LOG_PREFIX, 'bundleId is deprecated, use topic instead');
  41. apnsArgs.topic = apnsArgs.bundleId
  42. }
  43. const provider = APNS._createProvider(apnsArgs);
  44. this.providers.push(provider);
  45. }
  46. // Sort the providers based on priority ascending, high pri first
  47. this.providers.sort((s1, s2) => {
  48. return s1.priority - s2.priority;
  49. });
  50. // Set index-property of providers
  51. for (let index = 0; index < this.providers.length; index++) {
  52. this.providers[index].index = index;
  53. }
  54. }
  55. /**
  56. * Send apns request.
  57. *
  58. * @param {Object} data The data we need to send, the format is the same with api request body
  59. * @param {Array} allDevices An array of devices
  60. * @returns {Object} A promise which is resolved immediately
  61. */
  62. send(data, allDevices) {
  63. const coreData = data && data.data;
  64. if (!coreData || !allDevices || !Array.isArray(allDevices)) {
  65. log.warn(LOG_PREFIX, 'invalid push payload');
  66. return;
  67. }
  68. const expirationTime = data['expiration_time'] || coreData['expiration_time'];
  69. const collapseId = data['collapse_id'] || coreData['collapse_id'];
  70. const pushType = data['push_type'] || coreData['push_type'];
  71. const priority = data['priority'] || coreData['priority'];
  72. let allPromises = [];
  73. const devicesPerAppIdentifier = {};
  74. // Start by clustering the devices per appIdentifier
  75. allDevices.forEach(device => {
  76. const appIdentifier = device.appIdentifier;
  77. devicesPerAppIdentifier[appIdentifier] = devicesPerAppIdentifier[appIdentifier] || [];
  78. devicesPerAppIdentifier[appIdentifier].push(device);
  79. });
  80. for (const key in devicesPerAppIdentifier) {
  81. const devices = devicesPerAppIdentifier[key];
  82. const appIdentifier = devices[0].appIdentifier;
  83. const providers = this._chooseProviders(appIdentifier);
  84. // No Providers found
  85. if (!providers || providers.length === 0) {
  86. const errorPromises = devices.map(device => APNS._createErrorPromise(device.deviceToken, 'No Provider found'));
  87. allPromises = allPromises.concat(errorPromises);
  88. continue;
  89. }
  90. const headers = { expirationTime: expirationTime, topic: appIdentifier, collapseId: collapseId, pushType: pushType, priority: priority }
  91. const notification = APNS._generateNotification(coreData, headers);
  92. const deviceIds = devices.map(device => device.deviceToken);
  93. const promise = this.sendThroughProvider(notification, deviceIds, providers);
  94. allPromises.push(promise.then(this._handlePromise.bind(this)));
  95. }
  96. return Promise.all(allPromises).then((results) => {
  97. // flatten all
  98. return [].concat.apply([], results);
  99. });
  100. }
  101. sendThroughProvider(notification, devices, providers) {
  102. return providers[0]
  103. .send(notification, devices)
  104. .then((response) => {
  105. if (response.failed
  106. && response.failed.length > 0
  107. && providers && providers.length > 1) {
  108. const devices = response.failed.map((failure) => { return failure.device; });
  109. // Reset the failures as we'll try next connection
  110. response.failed = [];
  111. return this.sendThroughProvider(notification,
  112. devices,
  113. providers.slice(1, providers.length)).then((retryResponse) => {
  114. response.failed = response.failed.concat(retryResponse.failed);
  115. response.sent = response.sent.concat(retryResponse.sent);
  116. return response;
  117. });
  118. } else {
  119. return response;
  120. }
  121. });
  122. }
  123. static _validateAPNArgs(apnsArgs) {
  124. if (apnsArgs.topic) {
  125. return true;
  126. }
  127. return !(apnsArgs.cert || apnsArgs.key || apnsArgs.pfx);
  128. }
  129. /**
  130. * Creates an Provider base on apnsArgs.
  131. */
  132. static _createProvider(apnsArgs) {
  133. // if using certificate, then topic must be defined
  134. if (!APNS._validateAPNArgs(apnsArgs)) {
  135. throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs);
  136. }
  137. const provider = new apn.Provider(apnsArgs);
  138. // Sets the topic on this provider
  139. provider.topic = apnsArgs.topic;
  140. // Set the priority of the providers, prod cert has higher priority
  141. if (apnsArgs.production) {
  142. provider.priority = 0;
  143. } else {
  144. provider.priority = 1;
  145. }
  146. return provider;
  147. }
  148. /**
  149. * Generate the apns Notification from the data we get from api request.
  150. * @param {Object} coreData The data field under api request body
  151. * @param {Object} headers The header properties for the notification (topic, expirationTime, collapseId, pushType, priority)
  152. * @returns {Object} A apns Notification
  153. */
  154. static _generateNotification(coreData, headers) {
  155. const notification = new apn.Notification();
  156. const payload = {};
  157. for (const key in coreData) {
  158. switch (key) {
  159. case 'aps':
  160. notification.aps = coreData.aps;
  161. break;
  162. case 'alert':
  163. notification.setAlert(coreData.alert);
  164. break;
  165. case 'title':
  166. notification.setTitle(coreData.title);
  167. break;
  168. case 'badge':
  169. notification.setBadge(coreData.badge);
  170. break;
  171. case 'sound':
  172. notification.setSound(coreData.sound);
  173. break;
  174. case 'content-available':
  175. notification.setContentAvailable(coreData['content-available'] === 1);
  176. break;
  177. case 'mutable-content':
  178. notification.setMutableContent(coreData['mutable-content'] === 1);
  179. break;
  180. case 'targetContentIdentifier':
  181. notification.setTargetContentIdentifier(coreData.targetContentIdentifier);
  182. break;
  183. case 'interruptionLevel':
  184. notification.setInterruptionLevel(coreData.interruptionLevel);
  185. break;
  186. case 'category':
  187. notification.setCategory(coreData.category);
  188. break;
  189. case 'threadId':
  190. notification.setThreadId(coreData.threadId);
  191. break;
  192. default:
  193. payload[key] = coreData[key];
  194. break;
  195. }
  196. }
  197. notification.payload = payload;
  198. notification.topic = headers.topic;
  199. notification.expiry = Math.round(headers.expirationTime / 1000);
  200. notification.collapseId = headers.collapseId;
  201. // set alert as default push type. If push type is not set notifications are not delivered to devices running iOS 13, watchOS 6 and later.
  202. notification.pushType = 'alert';
  203. if (headers.pushType) {
  204. notification.pushType = headers.pushType;
  205. }
  206. if (headers.priority) {
  207. // if headers priority is not set 'node-apn' defaults it to 5 which is min. required value for background pushes to launch the app in background.
  208. notification.priority = headers.priority
  209. }
  210. return notification;
  211. }
  212. /**
  213. * Choose appropriate providers based on device appIdentifier.
  214. *
  215. * @param {String} appIdentifier appIdentifier for required provider
  216. * @returns {Array} Returns Array with appropriate providers
  217. */
  218. _chooseProviders(appIdentifier) {
  219. // If the device we need to send to does not have appIdentifier, any provider could be a qualified provider
  220. /*if (!appIdentifier || appIdentifier === '') {
  221. return this.providers.map((provider) => provider.index);
  222. }*/
  223. // Otherwise we try to match the appIdentifier with topic on provider
  224. const qualifiedProviders = this.providers.filter((provider) => appIdentifier === provider.topic);
  225. if (qualifiedProviders.length > 0) {
  226. return qualifiedProviders;
  227. }
  228. // If qualifiedProviders empty, add all providers without topic
  229. return this.providers
  230. .filter((provider) => !provider.topic || provider.topic === '');
  231. }
  232. _handlePromise(response) {
  233. const promises = [];
  234. response.sent.forEach((token) => {
  235. log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token.device);
  236. promises.push(APNS._createSuccesfullPromise(token.device));
  237. });
  238. response.failed.forEach((failure) => {
  239. promises.push(APNS._handlePushFailure(failure));
  240. });
  241. return Promise.all(promises);
  242. }
  243. static _handlePushFailure(failure) {
  244. if (failure.error) {
  245. log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error);
  246. return APNS._createErrorPromise(failure.device, failure.error);
  247. } else if (failure.status && failure.response && failure.response.reason) {
  248. log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason);
  249. return APNS._createErrorPromise(failure.device, failure.response.reason);
  250. } else {
  251. log.error(LOG_PREFIX, 'APNS error transmitting to device with unkown error');
  252. return APNS._createErrorPromise(failure.device, 'Unkown status');
  253. }
  254. }
  255. /**
  256. * Creates an errorPromise for return.
  257. *
  258. * @param {String} token Device-Token
  259. * @param {String} errorMessage ErrrorMessage as string
  260. */
  261. static _createErrorPromise(token, errorMessage) {
  262. return Promise.resolve({
  263. transmitted: false,
  264. device: {
  265. deviceToken: token,
  266. deviceType: 'ios'
  267. },
  268. response: { error: errorMessage }
  269. });
  270. }
  271. /**
  272. * Creates an successfulPromise for return.
  273. *
  274. * @param {String} token Device-Token
  275. */
  276. static _createSuccesfullPromise(token) {
  277. return Promise.resolve({
  278. transmitted: true,
  279. device: {
  280. deviceToken: token,
  281. deviceType: 'ios'
  282. }
  283. });
  284. }
  285. }
  286. export default APNS;