@signalapp/mock-server
Version:
Mock Signal Server for writing tests
296 lines (295 loc) • 10 kB
JavaScript
;
// 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,
};
})),
};
}