UNPKG

@signalapp/mock-server

Version:
253 lines (252 loc) 9.2 kB
"use strict"; // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDevicesKeysResult = exports.serviceIdKindFromQuery = exports.assertJsonValue = exports.fromURLSafeBase64 = exports.fromBase64 = exports.toURLSafeBase64 = exports.toBase64 = exports.generateDevicePassword = exports.generateRegistrationId = exports.daysToSeconds = exports.getTodayInSeconds = exports.addressToString = exports.PromiseQueue = exports.parseAuthHeader = exports.generateRandomE164 = void 0; const assert_1 = __importDefault(require("assert")); const is_plain_obj_1 = __importDefault(require("is-plain-obj")); const node_crypto_1 = __importDefault(require("node:crypto")); const node_util_1 = __importDefault(require("node:util")); const constants_1 = require("./constants"); const types_1 = require("./types"); function generateRandomE164() { // Generate random number let number = '+141549'; for (let i = 0; i < 5; i++) { number += Math.floor(Math.random() * 10).toString(); } return number; } exports.generateRandomE164 = generateRandomE164; function parseAuthHeader(header, options) { if (!header) { return { error: 'Missing Authorization header' }; } const [basic, base64] = header.split(/\s+/g, 2); if (basic.toLowerCase() !== 'basic') { return { error: `Unsupported authorization type ${basic}` }; } let username; let password; try { const decoded = Buffer.from(base64, 'base64').toString(); [username, password] = decoded.split(':', 2); } catch (error) { (0, assert_1.default)(error instanceof Error); return { error: error.message }; } if (!username) { return { error: 'Missing username' }; } if (!password && !options?.allowEmptyPassword) { return { error: 'Missing password' }; } return { username, password }; } exports.parseAuthHeader = parseAuthHeader; class PromiseQueue { defaultTimeout; entries = []; resolvers = []; constructor(config = {}) { this.defaultTimeout = config.timeout; } get size() { return this.entries.length; } async pushAndWait(value, timeout = this.defaultTimeout) { // We were waiting for `.shift()` already const resolveEntry = this.resolvers.shift(); if (resolveEntry) { resolveEntry(value); return; } // Not waiting for `.shift()` - queue. return await new Promise((resolve, reject) => { let timer; const entry = { value, resolvePush() { if (timer !== undefined) { clearTimeout(timer); } timer = undefined; resolve(); }, }; const cancel = () => { const index = this.entries.indexOf(entry); if (index === -1) { throw new Error('PromiseQueue entries bookkeeping error'); } this.entries.splice(index, 1); reject(new Error('PromiseQueue pushAndWait timeout')); }; if (timeout !== undefined) { timer = setTimeout(cancel, timeout); } this.entries.push(entry); }); } push(value) { // We were waiting for `.shift()` already const resolveEntry = this.resolvers.shift(); if (resolveEntry) { resolveEntry(value); return; } this.entries.push({ value }); } async shift(timeout = this.defaultTimeout) { // `.pushAndWait()` was called before us const entry = this.entries.shift(); if (entry) { if (entry.resolvePush) { entry.resolvePush(); } return entry.value; } return await new Promise((resolve, reject) => { let timer; const resolveEntry = (value) => { if (timer !== undefined) { clearTimeout(timer); } timer = undefined; resolve(value); }; const cancel = () => { const index = this.resolvers.indexOf(resolveEntry); if (index === -1) { throw new Error('PromiseQueue resolvers bookkeeping error'); } this.resolvers.splice(index, 1); reject(new Error('PromiseQueue shift timeout')); }; if (timeout !== undefined) { timer = setTimeout(cancel, timeout); } this.resolvers.push(resolveEntry); }); } } exports.PromiseQueue = PromiseQueue; function addressToString(address) { return `${address.name()}.${address.deviceId()}`; } exports.addressToString = addressToString; function getTodayInSeconds() { const now = Date.now() / 1000; return now - (now % constants_1.DAY_IN_SECONDS); } exports.getTodayInSeconds = getTodayInSeconds; function daysToSeconds(days) { return days * constants_1.DAY_IN_SECONDS; } exports.daysToSeconds = daysToSeconds; function generateRegistrationId() { return Math.max(1, (Math.random() * 0x4000) | 0); } exports.generateRegistrationId = generateRegistrationId; function generateDevicePassword() { return node_crypto_1.default.randomBytes(10).toString('hex'); } exports.generateDevicePassword = generateDevicePassword; function toBase64(buf) { return Buffer.from(buf).toString('base64'); } exports.toBase64 = toBase64; function toURLSafeBase64(buf) { return toBase64(buf) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } exports.toURLSafeBase64 = toURLSafeBase64; function fromBase64(base64) { return Buffer.from(base64, 'base64'); } exports.fromBase64 = fromBase64; function fromURLSafeBase64(base64) { const source = base64.replace(/-/g, '+').replace(/_/g, '/'); // Note that `Buffer.from()` ignores padding anyway so we don't need to // restore it. return fromBase64(source); } exports.fromURLSafeBase64 = fromURLSafeBase64; function assertJsonValue(root) { const issues = []; function visit(node, path) { if (node === null || typeof node === 'boolean' || (typeof node === 'number' && Number.isFinite(node)) || typeof node === 'string') { return; } else if (Array.isArray(node)) { node.forEach((item, index) => { visit(item, path.concat(index)); }); return; } else if ((0, is_plain_obj_1.default)(node)) { Object.entries(node).forEach(([key, item]) => { // ignore undefined properties if (typeof item !== 'undefined') { visit(item, path.concat(key)); } }); } else { issues.push(`${path.join('.')}: ${node_util_1.default.inspect(node)}`); } } visit(root, ['value']); if (issues.length > 0) { throw new TypeError(`Invalid JsonValue:\n${issues.join('\n')}`); } } exports.assertJsonValue = assertJsonValue; function serviceIdKindFromQuery(query) { if (query && (query.identity === 'pni' || query.identity === 'PNI')) { return types_1.ServiceIdKind.PNI; } return types_1.ServiceIdKind.ACI; } exports.serviceIdKindFromQuery = serviceIdKindFromQuery; async function getDevicesKeysResult(serviceIdKind, devices) { const [primary] = devices; (0, assert_1.default)(primary !== undefined, 'Empty device list'); const identityKey = await primary.getIdentityKey(serviceIdKind); return { identityKey: identityKey.serialize().toString('base64'), devices: await Promise.all(devices.map(async (device) => { const { signedPreKey, preKey, pqPreKey } = await device.popSingleUseKey(serviceIdKind); return { deviceId: device.deviceId, registrationId: device.getRegistrationId(serviceIdKind), signedPreKey: { keyId: signedPreKey.keyId, publicKey: signedPreKey.publicKey.serialize().toString('base64'), signature: signedPreKey.signature.toString('base64'), }, pqPreKey: { keyId: pqPreKey.keyId, publicKey: pqPreKey.publicKey.serialize().toString('base64'), signature: pqPreKey.signature.toString('base64'), }, preKey: preKey ? { keyId: preKey.keyId, publicKey: preKey.publicKey.serialize().toString('base64'), } : null, }; })), }; } exports.getDevicesKeysResult = getDevicesKeysResult;