2016-01-01 00:07:00 +03:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* 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/. */
|
|
|
|
|
2016-04-13 21:52:14 +03:00
|
|
|
var webpush = require('web-push')
|
|
|
|
var P = require('./promise')
|
2016-01-01 00:07:00 +03:00
|
|
|
|
|
|
|
var ERR_NO_PUSH_CALLBACK = 'No Push Callback'
|
2016-04-13 21:52:14 +03:00
|
|
|
var ERR_DATA_BUT_NO_KEYS = 'Data payload present but missing key(s)'
|
2016-06-21 04:15:00 +03:00
|
|
|
var ERR_TOO_MANY_DEVICES = 'Too many devices connected to account'
|
2016-01-01 00:07:00 +03:00
|
|
|
|
2016-04-13 21:52:14 +03:00
|
|
|
var LOG_OP_PUSH_TO_DEVICES = 'push.pushToDevices'
|
2016-01-01 00:07:00 +03:00
|
|
|
|
2016-04-28 00:09:51 +03:00
|
|
|
var PUSH_PAYLOAD_SCHEMA_VERSION = 1
|
|
|
|
var PUSH_COMMANDS = {
|
2016-05-24 21:39:36 +03:00
|
|
|
DEVICE_CONNECTED: 'fxaccounts:device_connected',
|
|
|
|
DEVICE_DISCONNECTED: 'fxaccounts:device_disconnected'
|
2016-04-28 00:09:51 +03:00
|
|
|
}
|
|
|
|
|
2016-05-24 21:39:36 +03:00
|
|
|
var TTL_DEVICE_DISCONNECTED = 5 * 3600 // 5 hours
|
|
|
|
|
2016-06-21 04:15:00 +03:00
|
|
|
// An arbitrary, but very generous, limit on the number of active devices.
|
|
|
|
// Currently only for metrics purposes, not enforced.
|
|
|
|
var MAX_ACTIVE_DEVICES = 200
|
|
|
|
|
2016-04-19 20:05:50 +03:00
|
|
|
var reasonToEvents = {
|
|
|
|
accountVerify: {
|
2016-04-13 21:52:14 +03:00
|
|
|
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'
|
2016-04-19 20:05:50 +03:00
|
|
|
},
|
|
|
|
passwordReset: {
|
|
|
|
send: 'push.password_reset.send',
|
|
|
|
success: 'push.password_reset.success',
|
|
|
|
resetSettings: 'push.password_reset.reset_settings',
|
|
|
|
failed: 'push.password_reset.failed',
|
2016-04-13 21:52:14 +03:00
|
|
|
noCallback: 'push.password_reset.no_push_callback',
|
|
|
|
noKeys: 'push.password_reset.data_but_no_keys'
|
2016-04-19 20:05:50 +03:00
|
|
|
},
|
|
|
|
passwordChange: {
|
|
|
|
send: 'push.password_change.send',
|
|
|
|
success: 'push.password_change.success',
|
|
|
|
resetSettings: 'push.password_change.reset_settings',
|
|
|
|
failed: 'push.password_change.failed',
|
2016-04-13 21:52:14 +03:00
|
|
|
noCallback: 'push.password_change.no_push_callback',
|
|
|
|
noKeys: 'push.password_change.data_but_no_keys'
|
2016-04-28 00:09:51 +03:00
|
|
|
},
|
|
|
|
deviceConnected: {
|
|
|
|
send: 'push.device_connected.send',
|
|
|
|
success: 'push.device_connected.success',
|
|
|
|
resetSettings: 'push.device_connected.reset_settings',
|
|
|
|
failed: 'push.device_connected.failed',
|
|
|
|
noCallback: 'push.device_connected.no_push_callback',
|
|
|
|
noKeys: 'push.device_connected.data_but_no_keys'
|
2016-05-24 21:39:36 +03:00
|
|
|
},
|
|
|
|
deviceDisconnected: {
|
|
|
|
send: 'push.device_disconnected.send',
|
|
|
|
success: 'push.device_disconnected.success',
|
|
|
|
resetSettings: 'push.device_disconnected.reset_settings',
|
|
|
|
failed: 'push.device_disconnected.failed',
|
|
|
|
noCallback: 'push.device_disconnected.no_push_callback',
|
|
|
|
noKeys: 'push.device_disconnected.data_but_no_keys'
|
2016-04-19 20:05:50 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-01 00:07:00 +03:00
|
|
|
module.exports = function (log, db) {
|
2016-05-10 02:42:02 +03:00
|
|
|
/**
|
|
|
|
* Reports push errors to logs
|
|
|
|
*
|
|
|
|
* @param err
|
|
|
|
* Error object
|
|
|
|
* @param deviceId
|
|
|
|
* The device id
|
|
|
|
*/
|
2016-06-18 10:47:09 +03:00
|
|
|
function reportPushError(err, uid, deviceId) {
|
2016-01-15 20:44:52 +03:00
|
|
|
log.error({
|
2016-04-13 21:52:14 +03:00
|
|
|
op: LOG_OP_PUSH_TO_DEVICES,
|
2016-06-18 10:47:09 +03:00
|
|
|
uid: uid,
|
2016-01-15 20:44:52 +03:00
|
|
|
deviceId: deviceId,
|
|
|
|
err: err
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-05-10 02:42:02 +03:00
|
|
|
/**
|
|
|
|
* Reports push increment actions to logs
|
|
|
|
*
|
|
|
|
* @param name
|
|
|
|
* Name of the push action
|
|
|
|
*/
|
|
|
|
function incrementPushAction(name) {
|
|
|
|
if (name) {
|
|
|
|
log.info({
|
|
|
|
op: LOG_OP_PUSH_TO_DEVICES,
|
|
|
|
name: name
|
|
|
|
})
|
|
|
|
log.increment(name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-24 21:39:36 +03:00
|
|
|
/**
|
|
|
|
* Copy sendPush authorized options from an existing options object
|
|
|
|
* to a new one
|
|
|
|
*
|
|
|
|
* @param options
|
|
|
|
*/
|
|
|
|
function filterOptions(options) {
|
|
|
|
var allowedProps = ['TTL', 'data']
|
|
|
|
return allowedProps.reduce(function(filtered, prop) {
|
|
|
|
if (options[prop]) {
|
|
|
|
filtered[prop] = options[prop]
|
|
|
|
}
|
|
|
|
return filtered
|
|
|
|
}, {})
|
|
|
|
}
|
|
|
|
|
2016-01-01 00:07:00 +03:00
|
|
|
return {
|
|
|
|
/**
|
2016-05-24 21:39:36 +03:00
|
|
|
* Notifies all devices that there was an update to the account
|
2016-01-01 00:07:00 +03:00
|
|
|
*
|
|
|
|
* @param uid
|
2016-05-24 21:39:36 +03:00
|
|
|
* @param reason
|
2016-01-01 00:07:00 +03:00
|
|
|
* @promise
|
|
|
|
*/
|
2016-04-19 20:05:50 +03:00
|
|
|
notifyUpdate: function notifyUpdate(uid, reason) {
|
2016-04-13 21:52:14 +03:00
|
|
|
reason = reason || 'accountVerify'
|
|
|
|
return this.pushToDevices(uid, reason)
|
|
|
|
},
|
|
|
|
|
2016-04-28 00:09:51 +03:00
|
|
|
/**
|
2016-05-24 21:39:36 +03:00
|
|
|
* Notifies all devices (except the one who joined) that a new device joined the account
|
2016-04-28 00:09:51 +03:00
|
|
|
*
|
|
|
|
* @param uid
|
|
|
|
* @param deviceName
|
|
|
|
* @param currentDeviceId
|
|
|
|
* @promise
|
|
|
|
*/
|
|
|
|
notifyDeviceConnected: function notifyDeviceConnected(uid, deviceName, currentDeviceId) {
|
|
|
|
var data = new Buffer(JSON.stringify({
|
|
|
|
version: PUSH_PAYLOAD_SCHEMA_VERSION,
|
|
|
|
command: PUSH_COMMANDS.DEVICE_CONNECTED,
|
|
|
|
data: {
|
|
|
|
deviceName: deviceName
|
|
|
|
}
|
|
|
|
}))
|
2016-05-24 21:39:36 +03:00
|
|
|
var options = { data: data, excludedDeviceIds: [currentDeviceId] }
|
|
|
|
return this.pushToDevices(uid, 'deviceConnected', options)
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Notifies a device that it is now disconnected from the account
|
|
|
|
*
|
|
|
|
* @param uid
|
|
|
|
* @param idToDisconnect
|
|
|
|
* @promise
|
|
|
|
*/
|
|
|
|
notifyDeviceDisconnected: function notifyDeviceDisconnected(uid, idToDisconnect) {
|
|
|
|
var data = new Buffer(JSON.stringify({
|
|
|
|
version: PUSH_PAYLOAD_SCHEMA_VERSION,
|
|
|
|
command: PUSH_COMMANDS.DEVICE_DISCONNECTED,
|
|
|
|
data: {
|
|
|
|
id: idToDisconnect
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
var options = { data: data, TTL: TTL_DEVICE_DISCONNECTED }
|
|
|
|
return this.pushToDevice(uid, idToDisconnect, 'deviceDisconnected', options)
|
2016-04-28 00:09:51 +03:00
|
|
|
},
|
|
|
|
|
2016-04-13 21:52:14 +03:00
|
|
|
/**
|
|
|
|
* 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
|
2016-05-24 21:39:36 +03:00
|
|
|
* @param {Object} options
|
|
|
|
* @param {String} options.excludedDeviceIds
|
|
|
|
* @param {String} options.data
|
|
|
|
* @param {String} options.TTL (in seconds)
|
2016-04-13 21:52:14 +03:00
|
|
|
* @promise
|
|
|
|
*/
|
2016-05-24 21:39:36 +03:00
|
|
|
pushToDevices: function pushToDevices(uid, reason, options) {
|
|
|
|
options = options || {}
|
|
|
|
var self = this
|
|
|
|
return db.devices(uid).then(
|
|
|
|
function (devices) {
|
|
|
|
if (options.excludedDeviceIds) {
|
|
|
|
devices = devices.filter(function(device) {
|
|
|
|
return options.excludedDeviceIds.indexOf(device.id.toString('hex')) === -1
|
|
|
|
})
|
|
|
|
}
|
|
|
|
var pushOptions = filterOptions(options)
|
|
|
|
return self.sendPush(uid, devices, reason, pushOptions)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a push notification with or without data to one device in the account
|
|
|
|
*
|
|
|
|
* @param uid
|
|
|
|
* @param id
|
|
|
|
* @param reason
|
|
|
|
* @param {Object} options
|
|
|
|
* @param {String} options.data
|
|
|
|
* @param {String} options.TTL (in seconds)
|
|
|
|
* @promise
|
|
|
|
*/
|
|
|
|
pushToDevice: function pushToDevice(uid, id, reason, options) {
|
|
|
|
options = options || {}
|
|
|
|
var self = this
|
2016-01-01 00:07:00 +03:00
|
|
|
return db.devices(uid).then(
|
|
|
|
function (devices) {
|
2016-05-24 21:39:36 +03:00
|
|
|
for (var i = 0; i < devices.length; i++) {
|
|
|
|
if (devices[i].id.toString('hex') === id) {
|
|
|
|
var pushOptions = filterOptions(options)
|
|
|
|
return self.sendPush(uid, [devices[i]], reason, pushOptions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return P.reject('Device id not found in devices')
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2016-01-01 00:07:00 +03:00
|
|
|
|
2016-05-24 21:39:36 +03:00
|
|
|
/**
|
|
|
|
* Send a push notification with or without data to a list of devices
|
|
|
|
*
|
|
|
|
* @param uid
|
|
|
|
* @param devices
|
|
|
|
* @param reason
|
|
|
|
* @param {Object} options
|
|
|
|
* @param {String} options.data
|
|
|
|
* @param {String} options.TTL (in seconds)
|
|
|
|
* @promise
|
|
|
|
*/
|
|
|
|
sendPush: function sendPush(uid, devices, reason, options) {
|
|
|
|
options = options || {}
|
|
|
|
var events = reasonToEvents[reason]
|
2016-06-21 04:15:00 +03:00
|
|
|
// There's no spec-compliant way to error out as a result of having
|
|
|
|
// too many devices to notify. For now, just log metrics about it.
|
|
|
|
if (devices.length > MAX_ACTIVE_DEVICES) {
|
|
|
|
reportPushError(new Error(ERR_TOO_MANY_DEVICES), uid, null)
|
|
|
|
}
|
2016-06-21 04:14:16 +03:00
|
|
|
return P.each(devices, function(device) {
|
|
|
|
var deviceId = device.id.toString('hex')
|
2016-05-24 21:39:36 +03:00
|
|
|
|
2016-06-21 04:14:16 +03:00
|
|
|
log.trace({
|
|
|
|
op: LOG_OP_PUSH_TO_DEVICES,
|
|
|
|
uid: uid,
|
|
|
|
deviceId: deviceId,
|
|
|
|
pushCallback: device.pushCallback
|
|
|
|
})
|
2016-05-24 21:39:36 +03:00
|
|
|
|
2016-06-21 04:14:16 +03:00
|
|
|
if (device.pushCallback) {
|
|
|
|
// send the push notification
|
|
|
|
incrementPushAction(events.send)
|
|
|
|
var pushParams = { 'TTL': options.TTL || '0' }
|
|
|
|
if (options.data) {
|
|
|
|
if (!device.pushPublicKey || !device.pushAuthKey) {
|
|
|
|
reportPushError(new Error(ERR_DATA_BUT_NO_KEYS), uid, deviceId)
|
|
|
|
incrementPushAction(events.noKeys)
|
|
|
|
return
|
2016-05-24 21:39:36 +03:00
|
|
|
}
|
2016-06-21 04:14:16 +03:00
|
|
|
pushParams.userPublicKey = device.pushPublicKey
|
|
|
|
pushParams.userAuth = device.pushAuthKey
|
|
|
|
pushParams.payload = options.data
|
|
|
|
}
|
|
|
|
return webpush.sendNotification(device.pushCallback, pushParams)
|
|
|
|
.then(
|
|
|
|
function () {
|
|
|
|
incrementPushAction(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) {
|
2016-06-18 10:47:09 +03:00
|
|
|
reportPushError(err, uid, deviceId)
|
2016-06-21 04:14:16 +03:00
|
|
|
}).then(function() {
|
|
|
|
incrementPushAction(events.resetSettings)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
reportPushError(err, uid, deviceId)
|
|
|
|
incrementPushAction(events.failed)
|
2016-04-13 21:52:14 +03:00
|
|
|
}
|
2016-06-21 04:14:16 +03:00
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// keep track if there are any devices with no push urls.
|
|
|
|
reportPushError(new Error(ERR_NO_PUSH_CALLBACK), uid, deviceId)
|
|
|
|
incrementPushAction(events.noCallback)
|
|
|
|
}
|
|
|
|
})
|
2016-01-01 00:07:00 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|