diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a46fdf7..a332140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,27 +1,41 @@ name: ci on: push: - branches: [ master, release, alpha, beta ] + branches: + - master pull_request: - branches: [ '**' ] + branches: + - '**' jobs: test: - runs-on: ubuntu-18.04 - timeout-minutes: 30 + strategy: + matrix: + include: + - name: Node.js 14 + NODE_VERSION: 14 + - name: Node.js 16 + NODE_VERSION: 16 + - name: Node.js 18 + NODE_VERSION: 18 + - name: Node.js 20 + NODE_VERSION: 20 + fail-fast: false + name: ${{ matrix.name }} + timeout-minutes: 15 + runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Setup Node - uses: actions/setup-node@v2 + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.NODE_VERSION }} + uses: actions/setup-node@v1 with: - node-version: 14 + node-version: ${{ matrix.NODE_VERSION }} - name: Cache Node.js modules uses: actions/cache@v2 with: path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-node- + ${{ runner.os }}-node-${{ matrix.NODE_VERSION }}- - name: Install dependencies run: npm ci - name: Build package diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2c93e..a65b022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [4.2.1](https://github.com/parse-community/parse-server-push-adapter/compare/4.2.0...4.2.1) (2023-10-02) + + +### Bug Fixes + +* Upgrade @parse/node-apn from 5.2.1 to 5.2.3 ([#221](https://github.com/parse-community/parse-server-push-adapter/issues/221)) ([7aaab38](https://github.com/parse-community/parse-server-push-adapter/commit/7aaab38b8c97215ea9e63f87fc627450646c714e)) + +# [4.2.0](https://github.com/parse-community/parse-server-push-adapter/compare/4.1.3...4.2.0) (2023-08-06) + + +### Features + +* Upgrade @parse/node-apn from 5.1.3 to 5.2.1 ([#220](https://github.com/parse-community/parse-server-push-adapter/issues/220)) ([3b932d1](https://github.com/parse-community/parse-server-push-adapter/commit/3b932d1e40ddf81d38fcd7f3bbb71bbdcf848978)) + +## [4.1.3](https://github.com/parse-community/parse-server-push-adapter/compare/4.1.2...4.1.3) (2023-05-20) + + +### Bug Fixes + +* Validate push notification payload; fixes a security vulnerability in which the adapter can crash Parse Server due to an invalid push notification payload ([#217](https://github.com/parse-community/parse-server-push-adapter/issues/217)) ([598cb84](https://github.com/parse-community/parse-server-push-adapter/commit/598cb84d0866b7c5850ca96af920e8cb5ba243ec)) + ## [4.1.2](https://github.com/parse-community/parse-server-push-adapter/compare/4.1.1...4.1.2) (2022-03-27) diff --git a/README.md b/README.md index 7b7158d..337e4e1 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ The official Push Notification adapter for Parse Server. See [Parse Server Push - [Install Push Adapter](#install-push-adapter) - [Configure Parse Server](#configure-parse-server) - - # Silent Notifications If you have migrated from parse.com and you are seeing situations where silent (newsstand-like presentless) notifications are failing to deliver please ensure that your payload is setting the content-available attribute to Int(1) and not "1" This value will be explicitly checked. @@ -44,30 +42,26 @@ This will produce a more verbose output for all the push sending attempts ## Install Push Adapter ``` -npm install --save @parse/push-adapter@VERSION +npm install --save @parse/push-adapter@ ``` -Replace VERSION with the version you want to install. +Replace `` with the version you want to install. ## Configure Parse Server ```js const PushAdapter = require('@parse/push-adapter').default; -const pushOptions = { - ios: { /* iOS push options */ } , - android: { /* android push options */ } -} -// starting 3.0.0 -const options = { - appId: "****", - masterKey: "****", +const parseServerOptions = { push: { - adapter: new PushAdapter(pushOptions), + adapter: new PushAdapter({ + ios: { + /* Apple push notification options */ + }, + android: { + /* Android push options */ + } + }) }, - /* ... */ + /* Other Parse Server options */ } - -const server = new ParseServer(options); - -/* continue with the initialization of parse-server */ ``` diff --git a/package-lock.json b/package-lock.json index dd446bf..4704388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@parse/push-adapter", - "version": "4.1.2", + "version": "4.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -330,13 +330,13 @@ } }, "@parse/node-apn": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.1.3.tgz", - "integrity": "sha512-Bwhmbm895lEIF2772PJ8dSvBjrtOG9/q/TDMxmX40IgZxQFoXS73+JUIKTq3CA7SUB/Szu5roJINQ0L2U/1MJw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.3.tgz", + "integrity": "sha512-uBUTTbzk0YyMOcE5qTcNdit5v1BdaECCRSQYbMGU/qY1eHwBaqeWOYd8rwi2Caga3K7IZyQGhpvL4/56H+uvrQ==", "requires": { "debug": "4.3.3", - "jsonwebtoken": "8.5.1", - "node-forge": "1.3.0", + "jsonwebtoken": "9.0.0", + "node-forge": "1.3.1", "verror": "1.10.1" } }, @@ -1980,7 +1980,7 @@ "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=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "cache-base": { "version": "1.0.1", @@ -3549,6 +3549,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -4153,20 +4159,24 @@ "dev": true }, "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "requires": { "jws": "^3.2.2", - "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.0.0", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^5.6.0" + "semver": "^7.3.8" + }, + "dependencies": { + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "jsprim": { @@ -4290,46 +4300,23 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, - "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=" + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true }, "lodash.uniqby": { "version": "4.7.0", @@ -4350,7 +4337,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -4713,9 +4699,9 @@ } }, "node-forge": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", - "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, "normalize-package-data": { "version": "3.0.3", @@ -5589,11 +5575,6 @@ "lru-cache": "^6.0.0" } }, - "http-cache-semantics": { - "version": "4.1.0", - "bundled": true, - "dev": true - }, "http-proxy-agent": { "version": "4.0.1", "bundled": true, @@ -8377,7 +8358,8 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true }, "semver-diff": { "version": "3.1.1", @@ -9580,8 +9562,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 0082ffc..b58d685 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@parse/push-adapter", - "version": "4.1.2", + "version": "4.2.1", "description": "Base parse-server-push-adapter", "main": "lib/index.js", "files": [ @@ -40,7 +40,7 @@ "semantic-release": "17.4.6" }, "dependencies": { - "@parse/node-apn": "5.1.3", + "@parse/node-apn": "5.2.3", "@parse/node-gcm": "1.0.2", "npmlog": "4.1.2", "parse": "3.4.0" diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index e6c8359..ba9ac4f 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -104,7 +104,7 @@ describe('APNS', () => { var prodApnsConnection = apns.providers[0]; expect(prodApnsConnection.index).toBe(0); - + // TODO: Remove this checking onec we inject APNS var prodApnsOptions = prodApnsConnection.client.config; expect(prodApnsOptions.cert).toBe(args[1].cert); @@ -239,7 +239,7 @@ describe('APNS', () => { expect(notification.pushType).toEqual('alert'); done(); }); - + it('can generate APNS notification from raw data', (done) => { //Mock request data let data = { @@ -259,17 +259,17 @@ describe('APNS', () => { let collapseId = "collapseIdentifier"; let pushType = "background"; let priority = 5; - + let notification = APNS._generateNotification(data, { expirationTime: expirationTime, collapseId: collapseId, pushType: pushType, priority: priority }); - + expect(notification.expiry).toEqual(Math.round(expirationTime / 1000)); expect(notification.collapseId).toEqual(collapseId); expect(notification.pushType).toEqual(pushType); expect(notification.priority).toEqual(priority); - + let stringifiedJSON = notification.compile(); let jsonObject = JSON.parse(stringifiedJSON); - + expect(jsonObject.aps.alert).toEqual({ "loc-key" : "GAME_PLAY_REQUEST_FORMAT", "loc-args" : [ "Jenna", "Frank"] }); expect(jsonObject.aps.badge).toEqual(100); expect(jsonObject.aps.sound).toEqual('test'); @@ -315,6 +315,20 @@ describe('APNS', () => { done(); }); + it('does log on invalid APNS notification', async () => { + const args = { + cert: new Buffer('testCert'), + key: new Buffer('testKey'), + production: true, + topic: 'topic' + }; + const log = require('npmlog'); + const spy = spyOn(log, 'warn'); + const apns = new APNS(args); + apns.send(); + expect(spy).toHaveBeenCalled(); + }); + it('can send APNS notification', (done) => { let args = { cert: new Buffer('testCert'), diff --git a/spec/GCM.spec.js b/spec/GCM.spec.js index 2414ed7..edee3c6 100644 --- a/spec/GCM.spec.js +++ b/spec/GCM.spec.js @@ -10,7 +10,7 @@ function mockSender(gcm) { {"error":"InvalidRegistration"}, {"error":"InvalidRegistration"}, {"error":"InvalidRegistration"}] }*/ - + let tokens = options.registrationTokens; const response = { multicast_id: 7680139367771848000, @@ -58,6 +58,14 @@ describe('GCM', () => { done(); }); + it('does log on invalid APNS notification', async () => { + const log = require('npmlog'); + const spy = spyOn(log, 'warn'); + const gcm = new GCM({apiKey: 'apiKey'}); + gcm.send(); + expect(spy).toHaveBeenCalled(); + }); + it('can generate GCM Payload without expiration time', (done) => { //Mock request data var requestData = { diff --git a/src/APNS.js b/src/APNS.js index bde1ae7..c78fec1 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -70,7 +70,11 @@ export class APNS { * @returns {Object} A promise which is resolved immediately */ send(data, allDevices) { - let coreData = data.data; + let coreData = data && data.data; + if (!coreData || !allDevices || !Array.isArray(allDevices)) { + log.warn(LOG_PREFIX, 'invalid push payload'); + return; + } let expirationTime = data['expiration_time'] || coreData['expiration_time']; let collapseId = data['collapse_id'] || coreData['collapse_id']; let pushType = data['push_type'] || coreData['push_type']; diff --git a/src/GCM.js b/src/GCM.js index 548f35e..2b1f91a 100644 --- a/src/GCM.js +++ b/src/GCM.js @@ -26,6 +26,10 @@ GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax; * @returns {Object} A promise which is resolved after we get results from gcm */ GCM.prototype.send = function(data, devices) { + if (!data || !devices || !Array.isArray(devices)) { + log.warn(LOG_PREFIX, 'invalid push payload'); + return; + } let pushId = randomString(10); // Make a new array devices=devices.slice(0);