feat(push): Prepare codebase for data payloads

This commit is contained in:
Edouard Oger 2016-04-13 11:52:14 -07:00
Родитель cf5be7f492
Коммит b60c46491f
18 изменённых файлов: 961 добавлений и 559 удалений

Просмотреть файл

@ -9,7 +9,8 @@ node_js:
- "0.10"
- "4"
sudo: false
dist: trusty
sudo: required
addons:
apt:
@ -17,6 +18,9 @@ addons:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
env:
global:
@ -39,7 +43,7 @@ notifications:
skip_join: false
before_install:
- npm install -g npm@2
- if [ "$TRAVIS_NODE_VERSION" == "0.10" ]; then npm install -g npm@2; fi
- npm config set spin false
install:

Просмотреть файл

@ -1274,8 +1274,10 @@ or updates existing device details for this user/session
If no device `id` is specified,
both `name` and `type` must be provided.
If a device `id` is specified,
at least one of `name`, `type`, `pushCallback` and `pushPublicKey`
at least one of `name`, `type`, `pushCallback` or the tuple (`pushCallback`, `pushPublicKey` and `pushAuthKey`)
must be present.
Beware that if you provide `pushCallback` without the couple (`pushPublicKey` and `pushAuthKey`), both of
the keys will be reset to an empty string.
### Request
@ -1297,7 +1299,8 @@ https://api-accounts.dev.lcip.org/v1/account/device \
"name": "My Phone",
"type": "mobile",
"pushCallback": "https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef",
"pushPublicKey": "468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370"
"pushPublicKey": "BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA",
"pushAuthKey": "w3b14Zjc-Afj2SDOLOyong"
}'
```
@ -1328,7 +1331,8 @@ with an object that contains the device id in the JSON body:
"name": "My Phone",
"type": "mobile",
"pushCallback": "https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef",
"pushPublicKey": "468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370"
"pushPublicKey": "BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA",
"pushAuthKey": "w3b14Zjc-Afj2SDOLOyong"
}
```
@ -1373,7 +1377,8 @@ with an array of device details in the JSON body:
"name": "My Phone",
"type": "mobile",
"pushCallback": "https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef",
"pushPublicKey": "468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370"
"pushPublicKey": "BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA",
"pushAuthKey": "w3b14Zjc-Afj2SDOLOyong"
},
{
"id": "0f7aa00356e5416e82b3bef7bc409eef",
@ -1382,7 +1387,8 @@ with an array of device details in the JSON body:
"name": "My Desktop",
"type": null,
"pushCallback": "https://updates.push.services.mozilla.com/update/d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c75",
"pushPublicKey": "468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370"
"pushPublicKey": "BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA",
"pushAuthKey": "w3b14Zjc-Afj2SDOLOyong"
}
]
```

Просмотреть файл

@ -437,6 +437,7 @@
* type
* pushCallback
* pushPublicKey
* pushAuthKey
* output
* id
* createdAt
@ -444,6 +445,7 @@
* type
* pushCallback
* pushPublicKey
* pushAuthKey
* db-write
* Devices
@ -457,6 +459,7 @@
* type
* pushCallback
* pushPublicKey
* pushAuthKey
## /account/device

Просмотреть файл

@ -95,4 +95,5 @@
* type
* pushCallback
* pushPublicKey
* pushAuthKey

Просмотреть файл

@ -438,9 +438,10 @@ module.exports = function (
name: item.name,
type: item.type,
pushCallback: item.callbackURL,
pushPublicKey: item.callbackPublicKey
pushPublicKey: item.callbackPublicKey,
pushAuthKey: item.callbackAuthKey
}, {
ignore: [ 'name', 'type', 'pushCallback' ]
ignore: [ 'name', 'type', 'pushCallback', 'pushPublicKey', 'pushAuthKey' ]
})
})
},
@ -521,7 +522,8 @@ module.exports = function (
name: deviceInfo.name,
type: deviceInfo.type,
callbackURL: deviceInfo.pushCallback,
callbackPublicKey: deviceInfo.pushPublicKey
callbackPublicKey: deviceInfo.pushPublicKey,
callbackAuthKey: deviceInfo.pushAuthKey
})
)
.then(
@ -565,7 +567,8 @@ module.exports = function (
name: deviceInfo.name,
type: deviceInfo.type,
callbackURL: deviceInfo.pushCallback,
callbackPublicKey: deviceInfo.pushPublicKey
callbackPublicKey: deviceInfo.pushPublicKey,
callbackAuthKey: deviceInfo.pushAuthKey
})
)
.then(

Просмотреть файл

@ -2,40 +2,45 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var request = require('request')
var webpush = require('web-push')
var P = require('./promise')
var ERR_NO_PUSH_CALLBACK = 'No Push Callback'
var ERR_DATA_BUT_NO_KEYS = 'Data payload present but missing key(s)'
var LOG_OP_NOTIFY_UPDATE = 'push.notifyUpdate'
var LOG_OP_PUSH_TO_DEVICES = 'push.pushToDevices'
var reasonToEvents = {
accountVerify: {
send: 'push.send',
success: 'push.success',
resetSettings: 'push.reset_settings',
failed: 'push.failed',
noCallback: 'push.no_push_callback'
send: 'push.account_verify.send',
success: 'push.account_verify.success',
resetSettings: 'push.account_verify.reset_settings',
failed: 'push.account_verify.failed',
noCallback: 'push.account_verify.no_push_callback',
noKeys: 'push.account_verify.data_but_no_keys'
},
passwordReset: {
send: 'push.password_reset.send',
success: 'push.password_reset.success',
resetSettings: 'push.password_reset.reset_settings',
failed: 'push.password_reset.failed',
noCallback: 'push.password_reset.no_push_callback'
noCallback: 'push.password_reset.no_push_callback',
noKeys: 'push.password_reset.data_but_no_keys'
},
passwordChange: {
send: 'push.password_change.send',
success: 'push.password_change.success',
resetSettings: 'push.password_change.reset_settings',
failed: 'push.password_change.failed',
noCallback: 'push.password_change.no_push_callback'
noCallback: 'push.password_change.no_push_callback',
noKeys: 'push.password_change.data_but_no_keys'
}
}
module.exports = function (log, db) {
function reportPushError(err, deviceId) {
log.error({
op: LOG_OP_NOTIFY_UPDATE,
op: LOG_OP_PUSH_TO_DEVICES,
deviceId: deviceId,
err: err
})
@ -49,55 +54,83 @@ module.exports = function (log, db) {
* @promise
*/
notifyUpdate: function notifyUpdate(uid, reason) {
var events = reasonToEvents[reason] || reasonToEvents.accountVerify
reason = reason || 'accountVerify'
return this.pushToDevices(uid, reason)
},
/**
* Send a push notification with or without data to all the devices in the account (except the ones in the excludedDeviceIds)
*
* @param uid
* @param reason
* @param data
* @param excludedDeviceIds
* @promise
*/
pushToDevices: function pushToDevices(uid, reason, data, excludedDeviceIds) {
var events = reasonToEvents[reason]
return db.devices(uid).then(
function (devices) {
devices.forEach(function (device) {
var deviceId = device.id.toString('hex')
return P.all(
devices.map(function(device) {
var deviceId = device.id.toString('hex')
log.trace({
op: LOG_OP_NOTIFY_UPDATE,
deviceId: deviceId,
pushCallback: device.pushCallback
})
if (excludedDeviceIds && excludedDeviceIds.indexOf(deviceId) !== -1) {
return
}
if (device.pushCallback) {
// send the push notification
log.increment(events.send)
request.post({
url: device.pushCallback,
headers: {
'ttl': '0'
}
}, function (err, response) {
if (err) {
// 404 or 410 error from the push servers means
// the push settings need to be reset.
// the clients will check this and re-register push endpoints
if (response && (response.statusCode === 404 || response.statusCode === 410)) {
// reset device push configuration
// Warning: this method is called without any session tokens or auth validation.
device.pushCallback = ''
device.pushPublicKey = ''
db.updateDevice(uid, device.id, device).catch(function (err) {
reportPushError(err, deviceId)
})
log.increment(events.resetSettings)
} else {
reportPushError(err, deviceId)
log.increment(events.failed)
}
} else {
log.increment(events.success)
}
log.trace({
op: LOG_OP_PUSH_TO_DEVICES,
deviceId: deviceId,
pushCallback: device.pushCallback
})
} else {
// keep track if there are any devices with no push urls.
reportPushError(new Error(ERR_NO_PUSH_CALLBACK), deviceId)
log.increment(events.noCallback)
}
})
if (device.pushCallback) {
// send the push notification
log.increment(events.send)
var pushParams = { 'TTL': '0' }
if (data) {
if (!device.pushPublicKey || !device.pushAuthKey) {
reportPushError(new Error(ERR_DATA_BUT_NO_KEYS), deviceId)
log.increment(events.noKeys)
return
}
pushParams.userPublicKey = device.pushPublicKey
pushParams.userAuth = device.pushAuthKey
pushParams.payload = data
}
return webpush.sendNotification(device.pushCallback, pushParams)
.then(
function () {
log.increment(events.success)
},
function (err) {
// 404 or 410 error from the push servers means
// the push settings need to be reset.
// the clients will check this and re-register push endpoints
if (err.statusCode === 404 || err.statusCode === 410) {
// reset device push configuration
// Warning: this method is called without any session tokens or auth validation.
device.pushCallback = ''
device.pushPublicKey = ''
device.pushAuthKey = ''
return db.updateDevice(uid, device.id, device).catch(function (err) {
reportPushError(err, deviceId)
}).then(function() {
log.increment(events.resetSettings)
})
} else {
reportPushError(err, deviceId)
log.increment(events.failed)
}
}
)
} else {
// keep track if there are any devices with no push urls.
reportPushError(new Error(ERR_NO_PUSH_CALLBACK), deviceId)
log.increment(events.noCallback)
}
}))
})
}
}

Просмотреть файл

@ -6,6 +6,7 @@ var validators = require('./validators')
var HEX_STRING = validators.HEX_STRING
var BASE64_JWT = validators.BASE64_JWT
var DISPLAY_SAFE_UNICODE = validators.DISPLAY_SAFE_UNICODE
var URLSAFEBASE64 = validators.URLSAFEBASE64
var butil = require('../crypto/butil')
var openid = require('openid')
@ -66,9 +67,9 @@ module.exports = function (
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).required(),
type: isA.string().max(16).required(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
// We're not yet ready to store pubkey values, don't let clients submit them.
pushPublicKey: isA.string().length(64).regex(HEX_STRING).allow('').forbidden()
})
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
.optional(),
metricsContext: metricsContext.schema
}
@ -85,8 +86,9 @@ module.exports = function (
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).required(),
type: isA.string().max(16).required(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
pushPublicKey: isA.string().length(64).regex(HEX_STRING).optional().allow('')
})
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
.optional()
}
}
@ -305,9 +307,9 @@ module.exports = function (
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).optional(),
type: isA.string().max(16).optional(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
// We're not yet ready to store pubkey values, don't let clients submit them.
pushPublicKey: isA.string().length(64).regex(HEX_STRING).allow('').forbidden()
})
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
.optional(),
metricsContext: metricsContext.schema
}
@ -325,8 +327,9 @@ module.exports = function (
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).optional(),
type: isA.string().max(16).optional(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
pushPublicKey: isA.string().length(64).regex(HEX_STRING).optional().allow('')
})
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
.optional()
}
}
@ -814,20 +817,20 @@ module.exports = function (
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).optional(),
type: isA.string().max(16).optional(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
// We're not yet ready to store pubkey values, don't let clients submit them.
pushPublicKey: isA.string().length(64).regex(HEX_STRING).allow('').forbidden()
}).or('name', 'type', 'pushCallback', 'pushPublicKey'),
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).or('name', 'type', 'pushCallback', 'pushPublicKey', 'pushAuthKey').and('pushPublicKey', 'pushAuthKey'),
isA.object({
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE).required(),
type: isA.string().max(16).required(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
// We're not yet ready to store pubkey values, don't let clients submit them.
pushPublicKey: isA.string().length(64).regex(HEX_STRING).allow('').forbidden()
})
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
)
},
response: {
schema: {
schema: isA.object({
id: isA.string().length(32).regex(HEX_STRING).required(),
createdAt: isA.number().positive().optional(),
// We previously allowed devices to register with arbitrry unicode names,
@ -835,8 +838,9 @@ module.exports = function (
name: isA.string().max(255).optional(),
type: isA.string().max(16).optional(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow(''),
pushPublicKey: isA.string().length(64).regex(HEX_STRING).optional().allow('')
}
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow(''),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('')
}).and('pushPublicKey', 'pushAuthKey')
}
},
handler: function (request, reply) {
@ -855,6 +859,10 @@ module.exports = function (
throw error.featureNotEnabled()
}
}
if (payload.pushCallback && (!payload.pushPublicKey || !payload.pushAuthKey)) {
payload.pushPublicKey = ''
payload.pushAuthKey = ''
}
var operation = payload.id ? 'updateDevice' : 'createDevice'
db[operation](sessionToken.uid, sessionToken.tokenId, payload).then(
function (device) {
@ -910,8 +918,9 @@ module.exports = function (
name: isA.string().max(255).required(),
type: isA.string().max(16).required(),
pushCallback: isA.string().uri({ scheme: 'https' }).max(255).optional().allow('').allow(null),
pushPublicKey: isA.string().length(64).regex(HEX_STRING).optional().allow(null)
}))
pushPublicKey: isA.string().max(88).regex(URLSAFEBASE64).optional().allow('').allow(null),
pushAuthKey: isA.string().max(24).regex(URLSAFEBASE64).optional().allow('').allow(null)
}).and('pushPublicKey', 'pushAuthKey'))
}
},
handler: function (request, reply) {

Просмотреть файл

@ -12,6 +12,8 @@ module.exports.HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
// Match an encoded JWT.
module.exports.BASE64_JWT = /^(?:[a-zA-Z0-9-_]+[=]{0,2}\.){2}[a-zA-Z0-9-_]+[=]{0,2}$/
module.exports.URLSAFEBASE64 = /^[a-zA-Z0-9-_]*$/
// Match display-safe unicode characters.
// We're pretty liberal with what's allowed in a unicode string,
// but we exclude the following classes of characters:

Просмотреть файл

@ -100,6 +100,7 @@ module.exports = function (log, inherits, Token) {
this.deviceCreatedAt = data.deviceCreatedAt
this.callbackURL = data.callbackURL
this.callbackPublicKey = data.callbackPublicKey
this.callbackAuthKey = data.callbackAuthKey
}
return SessionToken

921
npm-shrinkwrap.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -28,6 +28,7 @@
"readmeFilename": "README.md",
"dependencies": {
"aws-sdk": "2.2.10",
"base64url": "1.0.6",
"binary-split": "0.1.2",
"bluebird": "2.10.2",
"convict": "1.3.0",
@ -48,7 +49,8 @@
"request": "2.65.0",
"scrypt-hash": "1.1.13",
"through": "2.3.8",
"uuid": "1.4.1"
"uuid": "1.4.1",
"web-push": "2.1.1"
},
"devDependencies": {
"ass": "git://github.com/jrgm/ass.git#5be99ee7abc9fcf63f9ebcc37b151b9c822146d1",

Просмотреть файл

@ -7,7 +7,7 @@ node ./scripts/gen_keys.js
# Force install of mysql-patcher
(cd node_modules/fxa-auth-db-mysql && npm install &>/var/tmp/db-mysql.out)
mysql -e 'DROP DATABASE IF EXISTS fxa'
mysql -u root -e 'DROP DATABASE IF EXISTS fxa'
node ./node_modules/fxa-auth-db-mysql/bin/db_patcher.js
# Start backgrounded fxa-auth-db-mysql server

Просмотреть файл

@ -18,7 +18,7 @@ var pushManager = new PushManager({
})
test(
'notifyUpdate sends notifications using a real push server',
'pushToDevices sends notifications using a real push server',
function (t) {
pushManager.getSubscription().then(function (subscription) {
@ -31,7 +31,9 @@ test(
'lastAccessTime': 1449235471335,
'name': 'My Phone',
'type': 'mobile',
'pushCallback': subscription.endpoint
'pushCallback': subscription.endpoint,
'pushPublicKey': 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc',
'pushAuthKey': 'GSsIiaD2Mr83iPqwFNK4rw'
}
])
}
@ -39,14 +41,14 @@ test(
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.success') {
if (log === 'push.account_verify.success') {
t.end()
}
}
})
var push = proxyquire('../../lib/push', {})(thisMockLog, mockDbResult)
push.notifyUpdate(mockUid)
push.pushToDevices(mockUid, 'accountVerify', new Buffer('foodata'))
})
}

Просмотреть файл

@ -6,6 +6,7 @@ require('ass')
var test = require('../ptaptest')
var uuid = require('uuid')
var crypto = require('crypto')
var base64url = require('base64url')
var log = { trace: console.log, info: console.log }
var config = require('../../config').getProperties()
@ -217,7 +218,8 @@ test(
name: '',
type: 'mobile',
pushCallback: 'https://foo/bar',
pushPublicKey: crypto.randomBytes(32)
pushPublicKey: base64url(Buffer.concat([new Buffer('\x04'), crypto.randomBytes(64)])),
pushAuthKey: base64url(crypto.randomBytes(16))
}
return dbConn.then(function (db) {
return db.emailRecord(ACCOUNT.email)
@ -263,7 +265,8 @@ test(
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.deepEqual(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
return db.createDevice(ACCOUNT.uid, sessionTokenId, deviceInfo)
.then(function () {
t.fail('adding a device with a duplicate session token should have failed')
@ -285,12 +288,14 @@ test(
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.deepEqual(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
deviceInfo.id = device.id
deviceInfo.name = 'wibble'
deviceInfo.type = 'desktop'
deviceInfo.pushCallback = ''
deviceInfo.pushPublicKey = ''
deviceInfo.pushAuthKey = ''
return db.updateDevice(ACCOUNT.uid, sessionTokenId, deviceInfo)
.catch(function (err) {
t.fail('updating a new device should not have failed')
@ -307,7 +312,8 @@ test(
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.deepEqual(device.pushPublicKey, zeroBuffer32, 'device.pushPublicKey is correct')
t.equal(device.pushPublicKey, '', 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, '', 'device.pushAuthKey is correct')
return db.deleteDevice(ACCOUNT.uid, deviceInfo.id)
.catch(function () {
t.fail('deleting a device should not have failed')

Просмотреть файл

@ -5,8 +5,10 @@
require('ass')
var tap = require('tap')
var test = tap.test
var P = require('../../lib/promise')
var uuid = require('uuid')
var crypto = require('crypto')
var base64url = require('base64url')
var proxyquire = require('proxyquire')
var log = { trace: console.log, info: console.log }
@ -66,25 +68,26 @@ test(
name: 'my push device',
type: 'mobile',
pushCallback: 'https://foo/bar',
pushPublicKey: crypto.randomBytes(32)
pushPublicKey: base64url(Buffer.concat([new Buffer('\x04'), crypto.randomBytes(64)])),
pushAuthKey: base64url(crypto.randomBytes(16))
}
// two tests below, first for unknown 400 level error the device push info will stay the same
// second, for a known 400 error we reset the device
var mocksKnown400 = {
request: {
post: function (url, cb) {
return cb(new Error('Failed 400 level'), {
statusCode: 410
})
'web-push': {
sendNotification: function (endpoint, params) {
var err = new Error('Failed 400 level')
err.statusCode = 410
return P.reject(err)
}
}
}
var mocksUnknown400 = {
request: {
post: function (url, cb) {
return cb(new Error('Failed 429 level'), {
statusCode: 429
})
'web-push': {
sendNotification: function (endpoint, params) {
var err = new Error('Failed 429 level')
err.statusCode = 429
return P.reject(err)
}
}
}
@ -107,6 +110,7 @@ test(
t.equal(device.name, deviceInfo.name)
t.equal(device.pushCallback, deviceInfo.pushCallback)
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey)
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey)
})
.then(function () {
return db.devices(ACCOUNT.uid)
@ -114,7 +118,7 @@ test(
.then(function () {
var pushWithUnknown400 = proxyquire('../../lib/push', mocksUnknown400)(mockLog, db)
return pushWithUnknown400.notifyUpdate(ACCOUNT.uid)
return pushWithUnknown400.pushToDevices(ACCOUNT.uid, 'accountVerify')
})
.then(function () {
return db.devices(ACCOUNT.uid)
@ -123,12 +127,13 @@ test(
var device = devices[0]
t.equal(device.name, deviceInfo.name)
t.equal(device.pushCallback, deviceInfo.pushCallback)
t.deepEqual(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
})
.then(function () {
var pushWithKnown400 = proxyquire('../../lib/push', mocksKnown400)(mockLog, db)
return pushWithKnown400.notifyUpdate(ACCOUNT.uid)
return pushWithKnown400.pushToDevices(ACCOUNT.uid, 'accountVerify')
})
.then(function () {
return db.devices(ACCOUNT.uid)
@ -137,7 +142,8 @@ test(
var device = devices[0]
t.equal(device.name, deviceInfo.name)
t.equal(device.pushCallback, '')
t.deepEqual(device.pushPublicKey, zeroBuffer32, 'device.pushPublicKey is correct')
t.equal(device.pushPublicKey, '', 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, '', 'device.pushAuthKey is correct')
t.end()
})
})

Просмотреть файл

@ -4,6 +4,7 @@
var tap = require('tap')
var proxyquire = require('proxyquire')
var sinon = require('sinon')
var test = tap.test
var P = require('../../lib/promise')
@ -26,27 +27,29 @@ var mockDbResult = {
'name': 'My Phone',
'type': 'mobile',
'pushCallback': 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef',
'pushPublicKey': '468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370'
'pushPublicKey': 'BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA=',
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong=='
},
{
'id': '0f7aa00356e5416e82b3bef7bc409eef',
'id': '3a45e6d0dae543qqdKyqjuvAiEupsnOd',
'isCurrentDevice': false,
'lastAccessTime': 1417699471335,
'name': 'My Desktop',
'type': null,
'pushCallback': 'https://updates.push.services.mozilla.com/update/d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c75',
'pushPublicKey': '468601214f60f4828b6cd5d51d9d99d212e7c73657978955f0f5a5b7e2fa1370'
'pushPublicKey': 'BCp93zru09_hab2Bg37LpTNG__Pw6eMPEP2hrQpwuytoj3h4chXpGc-3qqdKyqjuvAiEupsnOd_RLyc7erJHWgA=',
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong=='
}
])
}
}
test(
'notifyUpdate does not throw on empty device result',
'pushToDevices does not throw on empty device result',
function (t) {
var thisMockLog = mockLog({
increment: function (name) {
if (name === 'push.success') {
if (name === 'push.account_verify.success') {
t.fail('must not call push.success')
}
}
@ -54,7 +57,7 @@ test(
try {
var push = require('../../lib/push')(thisMockLog, mockDbEmpty)
push.notifyUpdate(mockUid).catch(function (err) {
push.pushToDevices(mockUid).catch(function (err) {
t.fail('must not throw')
throw err
})
@ -66,12 +69,12 @@ test(
)
test(
'notifyUpdate sends notifications',
'pushToDevices sends notifications',
function (t) {
var successCalled = 0
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.success') {
if (log === 'push.account_verify.success') {
// notification sent
successCalled++
}
@ -84,21 +87,92 @@ test(
})
var mocks = {
request: {
post: function (url, cb) {
t.equal(url.headers.ttl, '0', 'sends the proper ttl header')
return cb()
'web-push': {
sendNotification: function (endpoint, params) {
t.equal(params.TTL, '0', 'sends the proper ttl header')
return P.resolve()
}
}
}
var push = proxyquire('../../lib/push', mocks)(thisMockLog, mockDbResult)
push.notifyUpdate(mockUid, 'accountVerify')
push.pushToDevices(mockUid, 'accountVerify')
}
)
test(
'notifyUpdate catches devices with no push callback',
'pushToDevices does not send notification to an excluded device',
function (t) {
var mocks = {
'web-push': {
sendNotification: function (endpoint, params) {
t.end()
return P.resolve()
}
}
}
mockDbResult.devices().then(function(devices) {
var push = proxyquire('../../lib/push', mocks)(mockLog(), mockDbResult)
push.pushToDevices(mockUid, 'accountVerify', null, [devices[0].id])
})
}
)
test(
'pushToDevices sends data',
function (t) {
var count = 0
var mocks = {
'web-push': {
sendNotification: function (endpoint, params) {
count++
t.ok(params.userPublicKey)
t.ok(params.userAuth)
t.deepEqual(params.payload, new Buffer('foobar'))
if (count === 2) {
t.end()
}
return P.resolve()
}
}
}
var push = proxyquire('../../lib/push', mocks)(mockLog(), mockDbResult)
push.pushToDevices(mockUid, 'accountVerify', new Buffer('foobar'))
}
)
test(
'pushToDevices fails if data is present but both keys are not present',
function (t) {
var mockDbNoKeys = {
devices: function () {
return P.resolve([{
'id': 'foo',
'name': 'My Phone',
'pushCallback': 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef',
'pushAuthKey': 'bogus'
}])
}
}
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.account_verify.data_but_no_keys') {
// data detected but device had no keys
t.end()
}
}
})
var push = require('../../lib/push')(thisMockLog, mockDbNoKeys)
push.pushToDevices(mockUid, 'accountVerify', new Buffer('foobar'))
}
)
test(
'pushToDevices catches devices with no push callback',
function (t) {
var mockDbNoCallback = {
devices: function () {
@ -111,7 +185,7 @@ test(
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.no_push_callback') {
if (log === 'push.account_verify.no_push_callback') {
// device had no push callback
t.end()
}
@ -119,12 +193,12 @@ test(
})
var push = require('../../lib/push')(thisMockLog, mockDbNoCallback)
push.notifyUpdate(mockUid, 'accountVerify')
push.pushToDevices(mockUid, 'accountVerify')
}
)
test(
'notifyUpdate reports errors when requests fail',
'pushToDevices reports errors when web-push fails',
function (t) {
var mockDb = {
devices: function (/* uid */) {
@ -139,28 +213,28 @@ test(
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.failed') {
// request failed
if (log === 'push.account_verify.failed') {
// web-push failed
t.end()
}
}
})
var mocks = {
request: {
post: function (url, cb) {
return cb(new Error('Failed'))
'web-push': {
sendNotification: function (endpoint, params) {
return P.reject(new Error('Failed'))
}
}
}
var push = proxyquire('../../lib/push', mocks)(thisMockLog, mockDb)
push.notifyUpdate(mockUid, 'accountVerify')
push.pushToDevices(mockUid, 'accountVerify')
}
)
test(
'notifyUpdate resets device push data when push server responds with a 400 level error',
'pushToDevices resets device push data when push server responds with a 400 level error',
function (t) {
var mockDb = {
devices: function (/* uid */) {
@ -178,25 +252,71 @@ test(
var thisMockLog = mockLog({
increment: function (log) {
if (log === 'push.reset_settings') {
// request failed
if (log === 'push.account_verify.reset_settings') {
// web-push failed
t.end()
}
}
})
var mocks = {
request: {
post: function (url, cb) {
return cb(new Error('Failed 400 level'), {
statusCode: 410
})
'web-push': {
sendNotification: function (endpoint, params) {
var err = new Error('Failed')
err.statusCode = 410
return P.reject(err)
}
}
}
var push = proxyquire('../../lib/push', mocks)(thisMockLog, mockDb)
push.notifyUpdate(mockUid, 'accountVerify')
push.pushToDevices(mockUid, 'accountVerify')
}
)
test(
'notifyUpdate calls pushToDevices',
function (t) {
try {
var push = require('../../lib/push')(mockLog(), mockDbEmpty)
sinon.spy(push, 'pushToDevices')
push.notifyUpdate(mockUid, 'passwordReset').catch(function (err) {
t.fail('must not throw')
throw err
})
.then(function() {
t.ok(push.pushToDevices.calledOnce, 'pushToDevices was called')
t.equal(push.pushToDevices.getCall(0).args[0], mockUid)
t.equal(push.pushToDevices.getCall(0).args[1], 'passwordReset')
push.pushToDevices.restore()
t.end()
})
} catch (e) {
t.fail('must not throw')
}
}
)
test(
'notifyUpdate without a 2nd arg calls pushToDevices with a accountVerify reason',
function (t) {
try {
var push = require('../../lib/push')(mockLog(), mockDbEmpty)
sinon.spy(push, 'pushToDevices')
push.notifyUpdate(mockUid).catch(function (err) {
t.fail('must not throw')
throw err
})
.then(function() {
t.ok(push.pushToDevices.calledOnce, 'pushToDevices was called')
t.equal(push.pushToDevices.getCall(0).args[0], mockUid)
t.equal(push.pushToDevices.getCall(0).args[1], 'accountVerify')
push.pushToDevices.restore()
t.end()
})
} catch (e) {
t.fail('must not throw')
}
}
)

Просмотреть файл

@ -7,6 +7,7 @@ var TestServer = require('../test_server')
var Client = require('../client')
var config = require('../../config').getProperties()
var crypto = require('crypto')
var base64url = require('base64url')
var P = require('../../lib/promise')
TestServer.start(config)
@ -19,7 +20,9 @@ TestServer.start(config)
var deviceInfo = {
name: 'test device',
type: 'desktop',
pushCallback: 'https://foo/bar'
pushCallback: 'https://foo/bar',
pushPublicKey: base64url(Buffer.concat([new Buffer('\x04'), crypto.randomBytes(64)])),
pushAuthKey: base64url(crypto.randomBytes(16))
}
return Client.create(config.publicUrl, email, password, { device: deviceInfo })
.then(
@ -30,6 +33,8 @@ TestServer.start(config)
t.equal(client.device.name, deviceInfo.name, 'device.name is correct')
t.equal(client.device.type, deviceInfo.type, 'device.type is correct')
t.equal(client.device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.equal(client.device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(client.device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
return client.devices()
.then(
function (devices) {
@ -39,6 +44,8 @@ TestServer.start(config)
t.equal(devices[0].name, deviceInfo.name, 'devices returned correct name')
t.equal(devices[0].type, deviceInfo.type, 'devices returned correct type')
t.equal(devices[0].pushCallback, deviceInfo.pushCallback, 'devices returned correct pushCallback')
t.equal(devices[0].pushPublicKey, deviceInfo.pushPublicKey, 'devices returned correct pushPublicKey')
t.equal(devices[0].pushAuthKey, deviceInfo.pushAuthKey, 'devices returned correct pushAuthKey')
return client.updateDevice({
id: client.device.id,
name: 'new name'
@ -56,6 +63,8 @@ TestServer.start(config)
t.equal(devices[0].name, 'new name', 'devices returned correct name')
t.equal(devices[0].type, deviceInfo.type, 'devices returned correct type')
t.equal(devices[0].pushCallback, deviceInfo.pushCallback, 'devices returned correct pushCallback')
t.equal(devices[0].pushPublicKey, deviceInfo.pushPublicKey, 'devices returned correct pushPublicKey')
t.equal(devices[0].pushAuthKey, deviceInfo.pushAuthKey, 'devices returned correct pushAuthKey')
return client.destroyDevice(devices[0].id)
}
)
@ -82,7 +91,9 @@ TestServer.start(config)
var deviceInfo = {
name: 'a different device name',
type: 'mobile',
pushCallback: ''
pushCallback: '',
pushPublicKey: '',
pushAuthKey: ''
}
return Client.createAndVerify(config.publicUrl, email, password, server.mailbox)
.then(
@ -97,6 +108,8 @@ TestServer.start(config)
t.equal(client.device.name, deviceInfo.name, 'device.name is correct')
t.equal(client.device.type, deviceInfo.type, 'device.type is correct')
t.equal(client.device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.equal(client.device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(client.device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
return client.devices()
.then(
function (devices) {
@ -104,6 +117,8 @@ TestServer.start(config)
t.equal(devices[0].name, deviceInfo.name, 'devices returned correct name')
t.equal(devices[0].type, deviceInfo.type, 'devices returned correct type')
t.equal(devices[0].pushCallback, deviceInfo.pushCallback, 'devices returned correct pushCallback')
t.equal(devices[0].pushPublicKey, '', 'devices returned correct pushPublicKey')
t.equal(devices[0].pushAuthKey, '', 'devices returned correct pushAuthKey')
return client.destroyDevice(devices[0].id)
}
)
@ -123,7 +138,9 @@ TestServer.start(config)
var deviceInfo = {
name: 'test device',
type: 'mobile',
pushCallback: ''
pushCallback: '',
pushPublicKey: '',
pushAuthKey: ''
}
return client.devices()
.then(
@ -139,6 +156,8 @@ TestServer.start(config)
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, deviceInfo.pushCallback, 'device.pushCallback is correct')
t.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
t.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
}
)
.then(
@ -152,6 +171,8 @@ TestServer.start(config)
t.equal(devices[0].name, deviceInfo.name, 'devices returned correct name')
t.equal(devices[0].type, deviceInfo.type, 'devices returned correct type')
t.equal(devices[0].pushCallback, '', 'devices returned empty pushCallback')
t.equal(devices[0].pushPublicKey, '', 'devices returned correct pushPublicKey')
t.equal(devices[0].pushAuthKey, '', 'devices returned correct pushAuthKey')
return client.destroyDevice(devices[0].id)
}
)
@ -186,6 +207,8 @@ TestServer.start(config)
t.equal(device.name, deviceInfo.name, 'device.name is correct')
t.equal(device.type, deviceInfo.type, 'device.type is correct')
t.equal(device.pushCallback, undefined, 'device.pushCallback is undefined')
t.equal(device.pushPublicKey, undefined, 'device.pushPublicKey is undefined')
t.equal(device.pushAuthKey, undefined, 'device.pushAuthKey is undefined')
}
)
.then(
@ -199,6 +222,8 @@ TestServer.start(config)
t.equal(devices[0].name, deviceInfo.name, 'devices returned correct name')
t.equal(devices[0].type, deviceInfo.type, 'devices returned correct type')
t.equal(devices[0].pushCallback, null, 'devices returned undefined pushCallback')
t.equal(devices[0].pushPublicKey, null, 'devices returned undefined pushPublicKey')
t.equal(devices[0].pushAuthKey, null, 'devices returned undefined pushAuthKey')
return client.destroyDevice(devices[0].id)
}
)
@ -257,39 +282,6 @@ TestServer.start(config)
}
)
test(
'device registration with the not-yet-accepted pushPublicKey field',
function (t) {
var email = server.uniqueEmail()
var password = 'test password'
return Client.create(config.publicUrl, email, password)
.then(
function (client) {
var deviceInfo = {
id: crypto.randomBytes(16).toString('hex'),
name: 'test device',
type: 'mobile',
pushCallback: 'https://some.fake.url',
pushPublicKey: crypto.randomBytes(32).toString('hex')
}
return client.updateDevice(deviceInfo)
.then(
function () {
t.fail('request should have failed')
}
)
.catch(
function (err) {
t.equal(err.code, 400, 'err.code was 400')
t.equal(err.errno, 107, 'err.errno was 107')
t.equal(err.validation.keys[0], 'pushPublicKey', 'pushPublicKey was rejected')
}
)
}
)
}
)
test(
'device registration with unsupported characters in the name',
function (t) {
@ -388,6 +380,50 @@ TestServer.start(config)
}
)
test(
'update device with callbackUrl but without keys resets the keys',
function (t) {
var email = server.uniqueEmail()
var password = 'test password'
var deviceInfo = {
name: 'test device',
type: 'desktop',
pushCallback: 'https://foo/bar',
pushPublicKey: base64url(Buffer.concat([new Buffer('\x04'), crypto.randomBytes(64)])),
pushAuthKey: base64url(crypto.randomBytes(16))
}
return Client.create(config.publicUrl, email, password, { device: deviceInfo })
.then(
function (client) {
return client.devices()
.then(
function (devices) {
t.equal(devices[0].pushCallback, deviceInfo.pushCallback, 'devices returned correct pushCallback')
t.equal(devices[0].pushPublicKey, deviceInfo.pushPublicKey, 'devices returned correct pushPublicKey')
t.equal(devices[0].pushAuthKey, deviceInfo.pushAuthKey, 'devices returned correct pushAuthKey')
return client.updateDevice({
id: client.device.id,
pushCallback: 'https://bar/foo'
})
}
)
.then(
function () {
return client.devices()
}
)
.then(
function (devices) {
t.equal(devices[0].pushCallback, 'https://bar/foo', 'devices returned correct pushCallback')
t.equal(devices[0].pushPublicKey, '', 'devices returned newly empty pushPublicKey')
t.equal(devices[0].pushAuthKey, '', 'devices returned newly empty pushAuthKey')
}
)
}
)
}
)
test(
// Regression test for https://github.com/mozilla/fxa-auth-server/issues/1197
'devices list, sessionToken.lastAccessTime === 0 (regression test for #1197)',

Просмотреть файл

@ -6,6 +6,9 @@ var test = require('../ptaptest')
var url = require('url')
var Client = require('../client')
var TestServer = require('../test_server')
var crypto = require('crypto')
var base64url = require('base64url')
var config = require('../../config').getProperties()
@ -368,7 +371,9 @@ TestServer.start(config)
device: {
name: 'baz',
type: 'mobile',
pushCallback: 'https://example.com/qux'
pushCallback: 'https://example.com/qux',
pushPublicKey: base64url(Buffer.concat([new Buffer('\x04'), crypto.randomBytes(64)])),
pushAuthKey: base64url(crypto.randomBytes(16))
}
})
.then(