Address the CodeRabbit review: inbound SMS, binary payloads and untrusted input

This commit is contained in:
2026-08-27 11:52:43 +02:00
parent 884afdb87b
commit 8a182604b7
23 changed files with 603 additions and 152 deletions
+178
View File
@@ -5,15 +5,19 @@ import type { Dlr } from '../src/dlr.ts';
import type { PduObject, PduObjectInput } from '../src/pdu.ts';
import type { Sms } from '../src/sms.ts';
import type { SmppServer } from '../src/server.ts';
import type { TestContext } from 'node:test';
import type { VoidResult } from '../src/result.ts';
import { DlrMerger } from '../src/dlr-merger.ts';
import { PduFramer } from '../src/pdu-framer.ts';
import { ReconnectLoop } from '../src/reconnect-loop.ts';
import { Session, bindCommands } from '../src/session.ts';
import { client } from '../src/client.ts';
import { consts } from '../src/defs/constants.ts';
import { isCommand, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
import { paramText } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import { silentLog } from '../src/log.ts';
import { splitMessage } from '../src/message.ts';
async function startServer(options: Parameters<typeof server>[0] = {}): Promise<SmppServer> {
const { err, server: smpp } = await server({ ...options, port: 0 });
@@ -391,6 +395,33 @@ describe('bind', () => {
peer.close();
await smpp.close();
});
test('records the version the SMSC declared in its bind response', async t => {
const smpp = await startServer({ interfaceVersion: 0x50 });
t.after(() => smpp.close());
const { session } = await connect(smpp);
assert.ok(session);
t.after(() => { session.close(); });
assert.equal(session.peerInterfaceVersion, 0x50);
assert.ok(session.acceptsOptionalParams());
});
// The spec: an absent sc_interface_version means the SMSC supports no optional parameters.
test('takes an SMSC that declares no version as older than 3.4', async t => {
const peer = await bindOnlyPeer();
t.after(() => peer.close());
const { session } = await client({ port: peer.port });
assert.ok(session);
t.after(() => { session.close(); });
assert.equal(session.peerInterfaceVersion, 0x00);
assert.equal(session.acceptsOptionalParams(), false);
});
});
describe('sending', () => {
@@ -517,6 +548,84 @@ describe('sending', () => {
});
});
describe('receiving', () => {
async function inbound(t: TestContext): Promise<{ peer: Session; session: Session }> {
const smpp = await startServer();
t.after(() => smpp.close());
const bound = once<Session>(resolve => { smpp.on('session', resolve); });
const { session } = await connect(smpp);
assert.ok(session);
t.after(() => { session.close(); });
return { peer: await bound, session };
}
test('hands a client a deliver_sm that is not a delivery receipt', async t => {
const { peer, session } = await inbound(t);
const incoming = once<Sms>(resolve => { session.on('sms', resolve); });
const delivered = peer.send({
cmdName: 'deliver_sm',
params: {
destination_addr: '46709771337',
short_message: 'inbound hello',
source_addr: '46701113311',
},
});
const sms = await raceWithin(2000, incoming);
assert.ok(sms, 'a deliver_sm that carries no receipt is an inbound SMS');
assert.equal(sms.from, '46701113311');
assert.equal(sms.to, '46709771337');
assert.equal(sms.message, 'inbound hello');
await sms.sendResp({ smsId: 'inbound-id' });
const answered = await delivered;
assert.ok(answered.pduObj);
assert.equal(answered.pduObj.cmdName, 'deliver_sm_resp');
assert.equal(answered.pduObj.params.message_id, 'inbound-id');
});
test('reassembles a multipart inbound SMS before the sms event', async t => {
const message = 'Inbound lorem ipsum dolor sit amet consectetur, '.repeat(6);
const { peer, session } = await inbound(t);
const incoming = once<Sms>(resolve => { session.on('sms', resolve); });
const segments = splitMessage(message, { reference: 42 });
assert.equal(segments.length, 2);
const delivered = Promise.all(segments.map(segment => peer.send({
cmdName: 'deliver_sm',
params: {
destination_addr: '46709771337',
esm_class: consts.ESM_CLASS.UDH_INDICATOR,
short_message: segment,
source_addr: '46701113311',
},
})));
const sms = await raceWithin(2000, incoming);
assert.ok(sms, 'both segments should reassemble into one message');
assert.equal(sms.message, message);
assert.equal(sms.pduObjs.length, 2);
await sms.sendResp({ smsId: 'inbound-long' });
const ids: string[] = [];
for (const answered of await delivered) {
assert.ok(answered.pduObj);
ids.push(paramText(answered.pduObj.params.message_id));
}
assert.deepEqual(ids, ['inbound-long-1', 'inbound-long-2']);
});
});
describe('delivery reports', () => {
test('reaches the sender as a dlr event', async () => {
const smpp = await startServer();
@@ -860,6 +969,75 @@ describe('robustness', () => {
assert.ok(await closed);
await smpp.close();
});
// An aborted send that still reaches the SMSC bills a message the caller believes never went out.
test('puts nothing on the wire for a signal that is already aborted', async t => {
const smpp = await startServer();
t.after(() => smpp.close());
const bound = once<Session>(resolve => { smpp.on('session', resolve); });
const { session } = await connect(smpp);
assert.ok(session);
t.after(() => { session.close(); });
const peer = await bound;
const controller = new AbortController();
const seen: string[] = [];
peer.on('incomingPduObj', pduObj => { seen.push(pduObj.cmdName); });
controller.abort();
const sent = await session.sendSms({
from: '46701113311',
message: 'must never reach the peer',
to: '46709771337',
}, { signal: controller.signal });
assert.ok(sent.err instanceof Error);
await delay(50);
assert.deepEqual(seen, []);
});
// A socket the loop opened and never handed over is one leaked per retry, forever.
test('leaves no socket open when coming back up fails', async () => {
const opened: net.Socket[] = [];
function onConnected(): Promise<VoidResult> {
if (opened.length === 1) return Promise.resolve({ err: new Error('bind refused') });
throw new Error('bind exploded');
}
const loop = new ReconnectLoop({
connect: () => {
const sock = new net.Socket();
opened.push(sock);
return Promise.resolve({ sock });
},
log: silentLog,
maxDelay: 10,
minDelay: 1,
onConnected,
});
loop.schedule();
const destroyed = await waitFor(() => opened.length >= 2
&& opened[0]?.destroyed === true
&& opened[1]?.destroyed === true);
loop.stop();
for (const sock of opened) {
sock.destroy();
}
assert.ok(destroyed, 'a failed setup should leave no socket open');
});
});
describe('application hooks that throw', () => {