UNPKG

@signalapp/mock-server

Version:
296 lines (295 loc) 10 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.PromiseQueue = void 0; exports.generateRandomE164 = generateRandomE164; exports.parseAuthHeader = parseAuthHeader; exports.addressToString = addressToString; exports.getTodayInSeconds = getTodayInSeconds; exports.daysToSeconds = daysToSeconds; exports.generateRegistrationId = generateRegistrationId; exports.generateDevicePassword = generateDevicePassword; exports.toBase64 = toBase64; exports.toURLSafeBase64 = toURLSafeBase64; exports.fromBase64 = fromBase64; exports.fromURLSafeBase64 = fromURLSafeBase64; exports.assertJsonValue = assertJsonValue; exports.serviceIdKindFromQuery = serviceIdKindFromQuery; exports.getDevicesKeysResult = getDevicesKeysResult; 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; } function splitOnce(input, splitter) { const index = input.indexOf(splitter); if (index === -1) { return null; } return [input.slice(0, index), input.slice(index + 1)]; } 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 decoded; try { (0, assert_1.default)(base64 != null, 'Missing base64 for basic authorization'); decoded = Buffer.from(base64, 'base64').toString(); } catch (error) { (0, assert_1.default)(error instanceof Error); return { error: error.message }; } const parts = splitOnce(decoded, ':'); if (parts == null) { return { error: 'Invalid basic auth' }; } const [username, password] = parts; if (!username) { return { error: 'Missing username' }; } if (!password && !options?.allowEmptyPassword) { return { error: 'Missing password' }; } return { username, password }; } class PromiseQueue { defaultTimeout; entries = []; resolvers = []; name; constructor(config) { this.defaultTimeout = config.timeout; this.name = config.name; } get size() { return this.entries.length; } stop() { while (this.entries.length > 0) { const entry = this.entries[0]; if (entry) { entry.cancel(); this.entries.shift(); } else { break; } } while (this.resolvers.length > 0) { const entry = this.resolvers.shift(); if (entry) { entry.cancel(); } else { break; } } } pushAndWait(value, timeout = this.defaultTimeout) { // We were waiting for `.shift()` already const resolveEntry = this.resolvers.shift(); if (resolveEntry) { resolveEntry.resolve(value); return { promise: Promise.resolve(), cancel: () => undefined }; } // Not waiting for `.shift()` - queue. const { promise, resolve, reject } = Promise.withResolvers(); let timer; const cancel = () => { if (timer) { clearTimeout(timer); timer = undefined; } const index = this.entries.indexOf(entry); if (index === -1) { return; } this.entries.splice(index, 1); reject(new Error(`PromiseQueue(${this.name}) pushAndWait timeout`)); }; if (timeout !== undefined) { timer = setTimeout(cancel, timeout); } const entry = { value, cancel, resolvePush() { if (timer !== undefined) { clearTimeout(timer); } timer = undefined; resolve(); }, }; this.entries.push(entry); return { promise, cancel }; } push(value) { // We were waiting for `.shift()` already const resolveEntry = this.resolvers.shift(); if (resolveEntry) { resolveEntry.resolve(value); return; } this.entries.push({ value, cancel: () => undefined }); } 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 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(entry); if (index === -1) { throw new Error(`PromiseQueue(${this.name}) resolvers bookkeeping error`); } this.resolvers.splice(index, 1); reject(new Error(`PromiseQueue(${this.name}) shift timeout`)); }; if (timeout !== undefined) { timer = setTimeout(cancel, timeout); } const entry = { cancel, resolve: resolveEntry, }; this.resolvers.push(entry); }); } } exports.PromiseQueue = PromiseQueue; function addressToString(address) { return `${address.name()}.${address.deviceId()}`; } function getTodayInSeconds() { const now = Date.now() / 1000; return now - (now % constants_1.DAY_IN_SECONDS); } function daysToSeconds(days) { return days * constants_1.DAY_IN_SECONDS; } function generateRegistrationId() { return Math.max(1, (Math.random() * 0x4000) | 0); } function generateDevicePassword() { return node_crypto_1.default.randomBytes(10).toString('hex'); } function toBase64(buf) { return Buffer.from(buf).toString('base64'); } function toURLSafeBase64(buf) { return toBase64(buf) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } function fromBase64(base64) { return Buffer.from(base64, 'base64'); } 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); } 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')}`); } } function serviceIdKindFromQuery(query) { if (query && (query.identity === 'pni' || query.identity === 'PNI')) { return types_1.ServiceIdKind.PNI; } return types_1.ServiceIdKind.ACI; } 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: Buffer.from(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: Buffer.from(signedPreKey.publicKey.serialize()).toString('base64'), signature: signedPreKey.signature.toString('base64'), }, pqPreKey: { keyId: pqPreKey.keyId, publicKey: Buffer.from(pqPreKey.publicKey.serialize()).toString('base64'), signature: pqPreKey.signature.toString('base64'), }, preKey: preKey ? { keyId: preKey.keyId, publicKey: Buffer.from(preKey.publicKey.serialize()).toString('base64'), } : null, }; })), }; }