fix: honor Node all-address DNS lookup for HTTP webhooks
CSS quality / css-quality (push) Has been cancelled
CSS quality / css-quality (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
// Real CMPP 2 TCP test peers. No real supplier forwarding exists in this module.
|
||||
import net from 'node:net';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const z = (n) => Buffer.alloc(n);
|
||||
const fixed = (s, n) => {
|
||||
const b = z(n);
|
||||
b.write(s ?? '', 'ascii');
|
||||
return b;
|
||||
};
|
||||
const u32 = (n) => {
|
||||
const b = z(4);
|
||||
b.writeUInt32BE(n >>> 0);
|
||||
return b;
|
||||
};
|
||||
const u64 = (n) => {
|
||||
const b = z(8);
|
||||
b.writeBigUInt64BE(BigInt(n));
|
||||
return b;
|
||||
};
|
||||
export function packet(type, sequence, body = z(0)) {
|
||||
return Buffer.concat([u32(body.length + 12), u32(type), u32(sequence), body]);
|
||||
}
|
||||
export function parseSubmit(body) {
|
||||
const count = body[116];
|
||||
const end = 117 + 21 * count;
|
||||
assert(body.length >= end + 9);
|
||||
return {
|
||||
total: body[8],
|
||||
number: body[9],
|
||||
receipt: body[10],
|
||||
udhi: body[45],
|
||||
format: body[46],
|
||||
source: body.subarray(95, 116).toString('ascii').replace(/\0/g, ''),
|
||||
phones: Array.from({ length: count }, (_, i) =>
|
||||
body
|
||||
.subarray(117 + i * 21, 138 + i * 21)
|
||||
.toString('ascii')
|
||||
.replace(/\0/g, ''),
|
||||
),
|
||||
content: body.subarray(end + 1, end + 1 + body[end]),
|
||||
};
|
||||
}
|
||||
export function decodeUcs2(buffer) {
|
||||
return Buffer.from(buffer).swap16().toString('utf16le');
|
||||
}
|
||||
function framed(socket, onPacket, onError) {
|
||||
let accumulated = z(0);
|
||||
socket.on('error', onError);
|
||||
socket.on('data', (chunk) => {
|
||||
try {
|
||||
accumulated = Buffer.concat([accumulated, chunk]);
|
||||
while (accumulated.length >= 12) {
|
||||
const length = accumulated.readUInt32BE(0);
|
||||
assert(length >= 12 && length <= 65536, 'Invalid CMPP frame length');
|
||||
if (accumulated.length < length) return;
|
||||
const value = accumulated.subarray(0, length);
|
||||
accumulated = accumulated.subarray(length);
|
||||
onPacket(value.readUInt32BE(4), value.readUInt32BE(8), value.subarray(12));
|
||||
}
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function startSupplier({ host, port, accounts, phoneAllowed, events, receiptStatus = () => 'DELIVRD' }) {
|
||||
assert.equal(host, '127.0.0.1');
|
||||
assert.equal(port, 17900);
|
||||
const sockets = new Set();
|
||||
const connected = new Set();
|
||||
const peers = new Map();
|
||||
let id = BigInt(Date.now()) * 1000n;
|
||||
let sequence = 80000;
|
||||
const server = net.createServer((socket) => {
|
||||
if (socket.remoteAddress?.replace('::ffff:', '') !== '127.0.0.1') {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
sockets.add(socket);
|
||||
let account;
|
||||
socket.on('close', () => {
|
||||
sockets.delete(socket);
|
||||
if (account) connected.delete(account);
|
||||
});
|
||||
framed(
|
||||
socket,
|
||||
(type, seq, body) => {
|
||||
if (type === 1) {
|
||||
account = body.subarray(0, 6).toString('ascii').replace(/\0/g, '');
|
||||
assert(accounts.has(account), 'Unexpected simulator account');
|
||||
assert.equal(body[22], 0x20);
|
||||
socket.write(packet(0x80000001, seq, Buffer.concat([z(17), Buffer.from([0x20])])));
|
||||
connected.add(account);
|
||||
peers.set(account, socket);
|
||||
events.push({ type: 'connect', account, time: Date.now() });
|
||||
} else if (type === 8) socket.write(packet(0x80000008, seq, z(1)));
|
||||
else if (type === 2) {
|
||||
socket.write(packet(0x80000002, seq));
|
||||
socket.end();
|
||||
} else if (type === 4) {
|
||||
assert(account);
|
||||
const submit = parseSubmit(body);
|
||||
assert(submit.phones.every(phoneAllowed), 'Unexpected recipient: simulator stop condition');
|
||||
assert(events.filter((x) => x.type === 'submit').length < 160, 'Wire submit cap reached');
|
||||
const messageId = ++id;
|
||||
events.push({
|
||||
type: 'submit',
|
||||
account,
|
||||
time: Date.now(),
|
||||
id: String(messageId),
|
||||
...submit,
|
||||
content: submit.content.toString('base64'),
|
||||
});
|
||||
socket.write(packet(0x80000004, seq, Buffer.concat([u64(messageId), z(1)])));
|
||||
if (submit.receipt) {
|
||||
setTimeout(() => {
|
||||
if (socket.destroyed) return;
|
||||
const now = new Date();
|
||||
const stamp = [
|
||||
now.getFullYear() % 100,
|
||||
now.getMonth() + 1,
|
||||
now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes(),
|
||||
]
|
||||
.map((n) => String(n).padStart(2, '0'))
|
||||
.join('');
|
||||
const report = Buffer.concat([
|
||||
u64(messageId),
|
||||
fixed(receiptStatus(submit), 7),
|
||||
fixed(stamp, 10),
|
||||
fixed(stamp, 10),
|
||||
fixed(submit.phones[0], 21),
|
||||
u32(sequence),
|
||||
]);
|
||||
const deliver = Buffer.concat([
|
||||
u64(messageId),
|
||||
fixed(submit.source, 21),
|
||||
z(10),
|
||||
z(3),
|
||||
fixed(submit.phones[0], 21),
|
||||
Buffer.from([1, report.length]),
|
||||
report,
|
||||
z(8),
|
||||
]);
|
||||
socket.write(packet(5, ++sequence, deliver));
|
||||
}, 150);
|
||||
}
|
||||
} else if (type === 0x80000005)
|
||||
events.push({ type: 'supplierReceiptAck', account, result: body[8], time: Date.now() });
|
||||
},
|
||||
(error) => {
|
||||
events.push({ type: 'error', message: error.message });
|
||||
},
|
||||
);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, host, resolve);
|
||||
});
|
||||
return {
|
||||
connected,
|
||||
sendUplink(account, phone, dest, text) {
|
||||
const socket = peers.get(account);
|
||||
assert(socket && !socket.destroyed);
|
||||
assert(phoneAllowed(phone));
|
||||
const content = Buffer.from(text, 'utf16le').swap16();
|
||||
assert(content.length <= 140);
|
||||
const messageId = ++id;
|
||||
const body = Buffer.concat([
|
||||
u64(messageId),
|
||||
fixed(dest, 21),
|
||||
z(10),
|
||||
z(2),
|
||||
Buffer.from([8]),
|
||||
fixed(phone, 21),
|
||||
Buffer.from([0, content.length]),
|
||||
content,
|
||||
z(8),
|
||||
]);
|
||||
socket.write(packet(5, ++sequence, body));
|
||||
events.push({ type: 'supplierUplink', account, phone, dest, text, id: String(messageId), time: Date.now() });
|
||||
},
|
||||
close: async () => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user