From e7944b3d9881321644d283256e181abd893f24e2 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 12 Jul 2018 12:56:15 +0200 Subject: [PATCH] iOS push notifications, use node-apn (#10517) * fixing typos in comments. yes, I am that kind of nerd * replacing push-notify with node-apn in deps and in pushNotifications.js * updating calling code and tests to use node-apn * updating APN configs to new format * migrating team ID and key ID to config.json * update code to use env variables and add correct topic --- config.json.example | 3 + package-lock.json | 153 +++++++++++++++++------ package.json | 2 +- test/api/unit/libs/pushNotifications.js | 13 +- website/common/script/ops/buy/buy.js | 2 +- website/server/libs/pushNotifications.js | 58 ++++----- 6 files changed, 148 insertions(+), 83 deletions(-) diff --git a/config.json.example b/config.json.example index 0feacd1184..260d57989b 100644 --- a/config.json.example +++ b/config.json.example @@ -78,6 +78,9 @@ "PUSH_CONFIGS": { "GCM_SERVER_API_KEY": "", "APN_ENABLED": "false", + "APN_KEY_ID": "xxxxxxxxxx", + "APN_KEY": "xxxxxxxxxx", + "APN_TEAM_ID": "aaabbbcccd", "FCM_SERVER_API_KEY": "" }, "SITE_HTTP_AUTH": { diff --git a/package-lock.json b/package-lock.json index 6ebf9d7c15..e8de82d050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -801,13 +801,25 @@ } }, "apn": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/apn/-/apn-1.7.8.tgz", - "integrity": "sha1-Hp2kKPtXr6lX5UIjvvc0LALCTNo=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/apn/-/apn-2.2.0.tgz", + "integrity": "sha512-YIypYzPVJA9wzNBLKZ/mq2l1IZX/2FadPvwmSv4ZeR0VH7xdNITQ6Pucgh0Uw6ZZKC+XwheaJ57DFZAhJ0FvPg==", "requires": { - "debug": "2.6.9", - "node-forge": "0.6.49", - "q": "1.5.1" + "debug": "3.1.0", + "http2": "https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", + "jsonwebtoken": "8.3.0", + "node-forge": "0.7.5", + "verror": "1.10.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } } }, "append-buffer": { @@ -5036,6 +5048,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -8792,6 +8809,14 @@ "jsbn": "0.1.1" } }, + "ecdsa-sig-formatter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", + "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "requires": { + "safe-buffer": "5.1.2" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -13292,6 +13317,10 @@ "sshpk": "1.14.1" } }, + "http2": { + "version": "https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", + "integrity": "sha512-ad4u4I88X9AcUgxCRW3RLnbh7xHWQ1f5HbrXa7gEy2x4Xgq+rq+auGx5I+nUDE2YYuqteGIlbxrwQXkIaYTfnQ==" + }, "httpntlm": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz", @@ -14768,6 +14797,29 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" }, + "jsonwebtoken": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", + "integrity": "sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag==", + "requires": { + "jws": "3.1.5", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.once": "4.1.1", + "ms": "2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -14806,6 +14858,25 @@ "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", "dev": true }, + "jwa": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", + "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.10", + "safe-buffer": "5.1.2" + } + }, + "jws": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", + "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "requires": { + "jwa": "1.1.6", + "safe-buffer": "5.1.2" + } + }, "kareem": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.1.0.tgz", @@ -16418,6 +16489,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, "lodash.initial": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.initial/-/lodash.initial-4.1.1.tgz", @@ -16433,16 +16509,36 @@ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, "lodash.istypedarray": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", @@ -16484,6 +16580,11 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.pairs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", @@ -18033,11 +18134,6 @@ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.4.1.tgz", "integrity": "sha512-NNY/MpBkALb9jJmjpBlIi6GRoLveLUM0pJzgbp9vY9F7IQEb/HREC/nxrixechcQwd1NevOhJnWWV8QQQRE+OA==" }, - "mpns": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mpns/-/mpns-2.1.3.tgz", - "integrity": "sha512-gPLNoVqwYoKUmNYZ2shMSdaE2XvHSRxWNzyG4DUi6Av7MSujyeOw/nj61nnQeuV/vke5E0Dni468xn0qxTHIZQ==" - }, "mquery": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz", @@ -18457,9 +18553,9 @@ } }, "node-forge": { - "version": "0.6.49", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.49.tgz", - "integrity": "sha1-8e6V1ddGI5OP4Z1piqWibVTS9g8=" + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" }, "node-gcm": { "version": "0.14.10", @@ -18655,9 +18751,9 @@ } }, "node-rdkafka": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/node-rdkafka/-/node-rdkafka-2.3.3.tgz", - "integrity": "sha512-2J54zC9+Zj0iRQttmQs1Ubv8aHhmh04XjP3vk39uco7l6tp8BYYHG4XRsoqKOGGKjBLctGpFHr9g97WBE1pTbg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/node-rdkafka/-/node-rdkafka-2.3.4.tgz", + "integrity": "sha512-ilaAOrEpDF3TGTlItsxU5pQXG+qjN1gKbhSvs9CoLXZaItt2EN6oU+kEdO6UkRQLKO6/Kv4m296cBrr0JCmiTw==", "optional": true, "requires": { "bindings": "1.3.0", @@ -20335,11 +20431,6 @@ "pinkie": "2.0.4" } }, - "pipe-event": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/pipe-event/-/pipe-event-0.1.0.tgz", - "integrity": "sha1-pfXgPlqXsrdJPUsqBgzYPazLmmE=" - }, "pixelsmith": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/pixelsmith/-/pixelsmith-2.2.1.tgz", @@ -22718,19 +22809,6 @@ } } }, - "push-notify": { - "version": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", - "requires": { - "apn": "1.7.8", - "bluebird": "3.5.1", - "lodash": "4.17.10", - "mpns": "2.1.3", - "node-gcm": "0.14.10", - "pipe-event": "0.1.0", - "q": "1.5.1", - "wns": "0.5.3" - } - }, "pusher": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/pusher/-/pusher-1.5.1.tgz", @@ -27870,11 +27948,6 @@ "dev": true, "optional": true }, - "wns": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/wns/-/wns-0.5.3.tgz", - "integrity": "sha1-APToXPz44zg9y9gYmJBvH2rUhF8=" - }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/package.json b/package.json index 14f63a5dd2..358253dbb0 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "apidoc": "^0.17.5", "autoprefixer": "^8.5.0", "aws-sdk": "^2.239.1", + "apn": "^2.2.0", "axios": "^0.18.0", "axios-progress-bar": "^1.2.0", "babel-core": "^6.26.3", @@ -78,7 +79,6 @@ "postcss-easy-import": "^3.0.0", "ps-tree": "^1.0.0", "pug": "^2.0.3", - "push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", "pusher": "^1.3.0", "rimraf": "^2.4.3", "sass-loader": "^7.0.0", diff --git a/test/api/unit/libs/pushNotifications.js b/test/api/unit/libs/pushNotifications.js index 41696859b3..0967ba349d 100644 --- a/test/api/unit/libs/pushNotifications.js +++ b/test/api/unit/libs/pushNotifications.js @@ -1,6 +1,6 @@ import { model as User } from '../../../../website/server/models/user'; import requireAgain from 'require-again'; -import pushNotify from 'push-notify'; +import apn from 'apn/mock'; import nconf from 'nconf'; import gcmLib from 'node-gcm'; // works with FCM notifications too @@ -24,7 +24,7 @@ describe('pushNotifications', () => { sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy); - sandbox.stub(pushNotify, 'apn').returns({ + sandbox.stub(apn.Provider.prototype, 'send').returns({ on: () => null, send: apnSendSpy, }); @@ -104,10 +104,7 @@ describe('pushNotifications', () => { }, }; - sendPushNotification(user, details); - expect(apnSendSpy).to.have.been.calledOnce; - expect(apnSendSpy).to.have.been.calledWithMatch({ - token: '123', + const expectedNotification = new apn.Notification({ alert: message, sound: 'default', category: 'fun', @@ -117,6 +114,10 @@ describe('pushNotifications', () => { b: true, }, }); + + sendPushNotification(user, details); + expect(apnSendSpy).to.have.been.calledOnce; + expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123'); expect(fcmSendSpy).to.not.have.been.called; }); }); diff --git a/website/common/script/ops/buy/buy.js b/website/common/script/ops/buy/buy.js index f564af45a5..13a995060f 100644 --- a/website/common/script/ops/buy/buy.js +++ b/website/common/script/ops/buy/buy.js @@ -15,7 +15,7 @@ import {BuyGemOperation} from './buyGem'; import {BuyQuestWithGemOperation} from './buyQuestGem'; // @TODO: remove the req option style. Dependency on express structure is an anti-pattern -// We should either have more parms or a set structure validated by a Type checker +// We should either have more params or a set structure validated by a Type checker // @TODO: when we are sure buy is the only function used, let's move the buy files to a folder diff --git a/website/server/libs/pushNotifications.js b/website/server/libs/pushNotifications.js index 52b0f55759..41cfb4bd1f 100644 --- a/website/server/libs/pushNotifications.js +++ b/website/server/libs/pushNotifications.js @@ -1,48 +1,26 @@ import _ from 'lodash'; import nconf from 'nconf'; -// @TODO remove this lib and use directly the apn module -import pushNotify from 'push-notify'; +import apn from 'apn'; import logger from './logger'; -import { - S3, -} from './aws'; import gcmLib from 'node-gcm'; // works with FCM notifications too const FCM_API_KEY = nconf.get('PUSH_CONFIGS:FCM_SERVER_API_KEY'); const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined; -let apn; - +let apnProvider; // Load APN certificate and key from S3 const APN_ENABLED = nconf.get('PUSH_CONFIGS:APN_ENABLED') === 'true'; -const S3_BUCKET = nconf.get('S3:bucket'); if (APN_ENABLED) { - Promise.all([ - S3.getObject({ - Bucket: S3_BUCKET, - Key: 'apple_apn/cert.pem', - }).promise(), - S3.getObject({ - Bucket: S3_BUCKET, - Key: 'apple_apn/key.pem', - }).promise(), - ]) - .then(([certObj, keyObj]) => { - let cert = certObj.Body.toString(); - let key = keyObj.Body.toString(); - - apn = pushNotify.apn({ - key, - cert, - }); - - apn.on('error', err => logger.error('APN error', err)); - apn.on('transmissionError', (errorCode, notification, device) => { - logger.error('APN transmissionError', errorCode, notification, device); - }); - }); + apnProvider = APN_ENABLED ? new apn.Provider({ + token: { + key: nconf.get('PUSH_CONFIGS:APN_KEY'), + keyId: nconf.get('PUSH_CONFIGS:APN_KEY_ID'), + teamId: nconf.get('PUSH_CONFIGS:APN_TEAM_ID'), + }, + production: nconf.get('IS_PROD'), + }) : undefined; } function sendNotification (user, details = {}) { @@ -76,14 +54,24 @@ function sendNotification (user, details = {}) { break; case 'ios': - if (apn) { - apn.send({ - token: pushDevice.regId, + if (apnProvider) { + const notification = new apn.Notification({ alert: details.message, sound: 'default', category: details.category, + topic: 'com.habitrpg.ios.Habitica', payload, }); + apnProvider.send(notification, pushDevice.regId) + .then((response) => { + response.failed.forEach((failure) => { + if (failure.error) { + logger.error('APN error', failure.error); + } else { + logger.error('APN transmissionError', failure.status, notification, failure.device); + } + }); + }); } break; }