fxa-auth-server/lib/devices.js

186 строки
5.7 KiB
JavaScript

/* 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/. */
'use strict';
const isA = require('joi');
const validators = require('./routes/validators');
const {
DISPLAY_SAFE_UNICODE_WITH_NON_BMP,
HEX_STRING,
URL_SAFE_BASE_64
} = validators;
const PUSH_SERVER_REGEX = require('../config').get('push.allowedServerRegex');
const SCHEMA = {
id: isA.string().length(32).regex(HEX_STRING),
location: isA.object({
city: isA.string().optional().allow(null),
country: isA.string().optional().allow(null),
state: isA.string().optional().allow(null),
stateCode: isA.string().optional().allow(null)
}),
name: isA.string().max(255).regex(DISPLAY_SAFE_UNICODE_WITH_NON_BMP),
// We previously allowed devices to register with arbitrary unicode names,
// so we can't assert DISPLAY_SAFE_UNICODE_WITH_NON_BMP in the response schema.
nameResponse: isA.string().max(255).allow(''),
type: isA.string().max(16),
pushCallback: validators.pushCallbackUrl({ scheme: 'https' }).regex(PUSH_SERVER_REGEX).max(255).allow(''),
pushPublicKey: isA.string().max(88).regex(URL_SAFE_BASE_64).allow(''),
pushAuthKey: isA.string().max(24).regex(URL_SAFE_BASE_64).allow(''),
pushEndpointExpired: isA.boolean().strict(),
// An object mapping command names to metadata bundles.
availableCommands: isA.object().pattern(validators.DEVICE_COMMAND_NAME, isA.string().max(2048))
};
module.exports = (log, db, push) => {
return { isSpuriousUpdate, upsert, synthesizeName };
// Clients have been known to send spurious device updates,
// which generates lots of unnecessary database load.
// Check if anything has actually changed.
function isSpuriousUpdate (payload, token) {
if (! token.deviceId || payload.id !== token.deviceId) {
return false;
}
if (payload.name && payload.name !== token.deviceName) {
return false;
}
if (payload.type && payload.type !== token.deviceType) {
return false;
}
if (payload.pushCallback && payload.pushCallback !== token.deviceCallbackURL) {
return false;
}
if (payload.pushPublicKey && payload.pushPublicKey !== token.deviceCallbackPublicKey) {
return false;
}
if (payload.availableCommands) {
if (! token.deviceAvailableCommands) {
return false;
}
if (! isLike(token.deviceAvailableCommands, payload.availableCommands)) {
return false;
}
if (! isLike(payload.availableCommands, token.deviceAvailableCommands)) {
return false;
}
}
return true;
}
function upsert (request, credentials, deviceInfo) {
let operation, event, result;
if (deviceInfo.id) {
operation = 'updateDevice';
event = 'device.updated';
} else {
operation = 'createDevice';
event = 'device.created';
if (! deviceInfo.name) {
deviceInfo.name = credentials.client && credentials.client.name || '';
}
}
deviceInfo.sessionTokenId = credentials.id;
deviceInfo.refreshTokenId = credentials.refreshTokenId;
const isPlaceholderDevice = ! deviceInfo.id && ! deviceInfo.name && ! deviceInfo.type;
return db[operation](credentials.uid, deviceInfo)
.then(device => {
result = device;
return request.emitMetricsEvent(event, {
uid: credentials.uid,
device_id: result.id,
is_placeholder: isPlaceholderDevice
});
})
.then(() => {
if (operation === 'createDevice') {
// Clients expect this notification to always include a name,
// so try to synthesize one if necessary.
let deviceName = result.name;
if (! deviceName) {
deviceName = synthesizeName(deviceInfo);
}
if (credentials.tokenVerified) {
db.devices(credentials.uid).then(devices => {
const otherDevices = devices.filter(device => device.id !== result.id);
return push.notifyDeviceConnected(credentials.uid, otherDevices, deviceName);
});
}
if (isPlaceholderDevice) {
log.info('device:createPlaceholder', {
uid: credentials.uid,
id: result.id
});
}
return log.notifyAttachedServices('device:create', request, {
uid: credentials.uid,
id: result.id,
type: result.type,
timestamp: result.createdAt,
isPlaceholder: isPlaceholderDevice
});
}
})
.then(() => {
delete result.sessionTokenId;
delete result.refreshTokenId;
return result;
});
}
function synthesizeName (device) {
const uaBrowser = device.uaBrowser;
const uaBrowserVersion = device.uaBrowserVersion;
const uaOS = device.uaOS;
const uaOSVersion = device.uaOSVersion;
const uaFormFactor = device.uaFormFactor;
let result = '';
if (uaBrowser) {
if (uaBrowserVersion) {
const splitIndex = uaBrowserVersion.indexOf('.');
result = `${uaBrowser} ${splitIndex === -1 ? uaBrowserVersion : uaBrowserVersion.substr(0, splitIndex)}`;
} else {
result = uaBrowser;
}
if (uaOS || uaFormFactor) {
result += ', ';
}
}
if (uaFormFactor) {
return `${result}${uaFormFactor}`;
}
if (uaOS) {
result += uaOS;
if (uaOSVersion) {
result += ` ${uaOSVersion}`;
}
}
return result;
}
};
module.exports.schema = SCHEMA;
function isLike (object, archetype) {
return Object.entries(archetype).every(([ key, value ]) => object[key] === value);
}