Stop a thrown listener escaping, bound what a peer can pin, and drop the body from failure responses
This commit is contained in:
+11
-3
@@ -24,7 +24,8 @@ describe('header', () => {
|
||||
test('writes command length, id, status and sequence number', () => {
|
||||
const pdu = encode({ cmdName: 'bind_transceiver_resp', cmdStatus: 'ESME_RALYBND', seqNr: 1 });
|
||||
|
||||
assert.equal(pdu.readUInt32BE(0), 17);
|
||||
// A failure response is header-only, so 16 rather than 17 with an empty system_id.
|
||||
assert.equal(pdu.readUInt32BE(0), 16);
|
||||
assert.equal(pdu.readUInt32BE(4).toString(16), '80000009');
|
||||
assert.equal(pdu.readUInt32BE(8), 5);
|
||||
assert.equal(pdu.readUInt32BE(12), 1);
|
||||
@@ -397,7 +398,7 @@ describe('pduReturn()', () => {
|
||||
params: { destination_addr: '46709771337', short_message: 'hi', source_addr: 'foo' },
|
||||
seqNr: 9,
|
||||
}));
|
||||
const { buffer, err } = pduReturn(request, 'ESME_RINVDSTADR', { message_id: 'abc123' });
|
||||
const { buffer, err } = pduReturn(request, 'ESME_ROK', { message_id: 'abc123' });
|
||||
|
||||
assert.equal(err, undefined);
|
||||
assert.ok(buffer);
|
||||
@@ -405,9 +406,16 @@ describe('pduReturn()', () => {
|
||||
const pduObj = decode(buffer);
|
||||
|
||||
assert.equal(pduObj.cmdName, 'submit_sm_resp');
|
||||
assert.equal(pduObj.cmdStatus, 'ESME_RINVDSTADR');
|
||||
assert.equal(pduObj.cmdStatus, 'ESME_ROK');
|
||||
assert.equal(pduObj.params.message_id, 'abc123');
|
||||
assert.equal(pduObj.seqNr, 9);
|
||||
|
||||
// The spec drops the body of a failure response, so the id a caller passes is not sent.
|
||||
const refused = pduReturn(request, 'ESME_RINVDSTADR', { message_id: 'abc123' });
|
||||
|
||||
assert.ok(refused.buffer);
|
||||
assert.equal(refused.buffer.length, 16);
|
||||
assert.equal(decode(refused.buffer).params.message_id, undefined);
|
||||
});
|
||||
|
||||
test('refuses a command that has no response', () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import test, { describe } from 'node:test';
|
||||
import type { Dlr } from '../src/dlr.ts';
|
||||
import type { ErrorName } from '../src/defs/errors.ts';
|
||||
import type { MessageState } from '../src/defs/constants.ts';
|
||||
import type { MessageDlr } from '../src/session.ts';
|
||||
import type { PduObject, PduObjectInput } from '../src/pdu.ts';
|
||||
import type { Result } from '../src/result.ts';
|
||||
@@ -9,7 +11,9 @@ import type { SendSmsResult } from '../src/send-sms.ts';
|
||||
import type { Sms } from '../src/sms.ts';
|
||||
import type { SmppServer } from '../src/server.ts';
|
||||
import { Reassembler, decodeSegments } from '../src/reassembly.ts';
|
||||
import { DlrMerger } from '../src/dlr-merger.ts';
|
||||
import { client } from '../src/client.ts';
|
||||
import { consts } from '../src/defs/constants.ts';
|
||||
import { errors } from '../src/defs/errors.ts';
|
||||
import { server } from '../src/server.ts';
|
||||
import { silentLog } from '../src/log.ts';
|
||||
@@ -108,6 +112,34 @@ describe('merged delivery reports', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('merging segment statuses', () => {
|
||||
function receipt(smsId: string, statusMsg: MessageState): Dlr {
|
||||
return {
|
||||
doneDate: undefined,
|
||||
errorCode: undefined,
|
||||
receipt: undefined,
|
||||
smsId,
|
||||
statusId: consts.MESSAGE_STATE[statusMsg],
|
||||
statusMsg,
|
||||
};
|
||||
}
|
||||
|
||||
// MESSAGE_STATE is a flat enum: ACCEPTED is 6 where UNDELIVERABLE is 5, so reducing on the
|
||||
// wire value called a part-failed message delivered.
|
||||
test('reports the worse of two states the wire numbers the other way round', () => {
|
||||
const merger = new DlrMerger({ log: silentLog, max: 10, now: () => 0, timeout: 60_000 });
|
||||
|
||||
merger.expect(['msg-1', 'msg-2']);
|
||||
|
||||
assert.equal(merger.collect(receipt('msg-1', 'UNDELIVERABLE')), undefined);
|
||||
|
||||
const merged = merger.collect(receipt('msg-2', 'ACCEPTED'));
|
||||
|
||||
assert.ok(merged);
|
||||
assert.equal(merged.statusMsg, 'UNDELIVERABLE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendSms()', () => {
|
||||
function submitResp(seqNr: number, messageId: string, status: ErrorName = 'ESME_ROK'): PduObject {
|
||||
return {
|
||||
@@ -332,7 +364,8 @@ describe('reassembly bounds', () => {
|
||||
const reassembler = new Reassembler({
|
||||
log: silentLog,
|
||||
max: 10,
|
||||
maxOctets: 30,
|
||||
// One segment is 36 octets: 14 of short_message plus the two 11-octet addresses.
|
||||
maxOctets: 80,
|
||||
now: () => 0,
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
+90
-4
@@ -353,12 +353,14 @@ describe('bind', () => {
|
||||
await named.close();
|
||||
});
|
||||
|
||||
test('answers a refused bind with its own system_id too', async () => {
|
||||
// The echo leak cannot reach a refusal at all: the spec gives a failure response no body.
|
||||
test('answers a refused bind with no body to leak', async () => {
|
||||
const smpp = await startServer({ authenticate: () => false, systemId: 'the-smsc' });
|
||||
const refused = await bindRaw(smpp, 0x34);
|
||||
|
||||
assert.equal(refused.cmdStatus, 'ESME_RBINDFAIL');
|
||||
assert.equal(refused.params.system_id, 'the-smsc');
|
||||
assert.equal(refused.cmdLength, 16);
|
||||
assert.deepEqual(refused.params, {});
|
||||
|
||||
await smpp.close();
|
||||
});
|
||||
@@ -373,7 +375,7 @@ describe('bind', () => {
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('answers a second bind with ESME_RALYBND and its own system_id', async () => {
|
||||
test('answers a second bind with ESME_RALYBND and no body', async () => {
|
||||
const smpp = await startServer({ systemId: 'the-smsc' });
|
||||
const peer = rawPeer(smpp.port);
|
||||
|
||||
@@ -384,7 +386,7 @@ describe('bind', () => {
|
||||
const again = await peer.next();
|
||||
|
||||
assert.equal(again.cmdStatus, 'ESME_RALYBND');
|
||||
assert.equal(again.params.system_id, 'the-smsc');
|
||||
assert.deepEqual(again.params, {});
|
||||
|
||||
peer.close();
|
||||
await smpp.close();
|
||||
@@ -905,6 +907,90 @@ describe('application hooks that throw', () => {
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
// The guard for a throwing sms listener used to emit sessionError from inside its own catch.
|
||||
test('survives a sessionError listener that throws as well', async () => {
|
||||
const smpp = await startServer();
|
||||
|
||||
smpp.on('session', session => {
|
||||
session.on('sessionError', () => { throw new Error('the reporter exploded too'); });
|
||||
session.on('sms', () => { throw new Error('listener exploded'); });
|
||||
});
|
||||
|
||||
const { session } = await connect(smpp, { responseTimeout: 200 });
|
||||
|
||||
assert.ok(session);
|
||||
|
||||
const sent = await session.sendSms({
|
||||
from: '46701113311',
|
||||
message: 'blows up both listeners',
|
||||
to: '46709771337',
|
||||
});
|
||||
|
||||
assert.ok(sent.err instanceof Error);
|
||||
|
||||
session.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('closes even when an application close listener throws', async () => {
|
||||
const smpp = await startServer();
|
||||
|
||||
smpp.on('session', session => {
|
||||
session.on('close', () => { throw new Error('close listener exploded'); });
|
||||
});
|
||||
|
||||
const { session } = await connect(smpp);
|
||||
|
||||
assert.ok(session);
|
||||
await smpp.close();
|
||||
assert.equal(smpp.sessions.size, 0);
|
||||
|
||||
session.close();
|
||||
});
|
||||
|
||||
test('refuses a send window that can never free a slot', async () => {
|
||||
const smpp = await startServer();
|
||||
const { err, session } = await connect(smpp, { maxOutstanding: 0 });
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
assert.match(err.message, /maxOutstanding/);
|
||||
assert.equal(session, undefined);
|
||||
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('keeps the message id off a submit_sm_resp that refuses the message', async () => {
|
||||
const smpp = await startServer();
|
||||
|
||||
smpp.on('session', bound => {
|
||||
bound.on('sms', sms => { void sms.sendResp({ status: 'ESME_RMSGQFUL' }); });
|
||||
});
|
||||
|
||||
const peer = rawPeer(smpp.port);
|
||||
|
||||
peer.write(bindOf(0x34));
|
||||
await peer.next();
|
||||
peer.write({
|
||||
cmdName: 'submit_sm',
|
||||
params: {
|
||||
destination_addr: '46709771337',
|
||||
short_message: 'full queue',
|
||||
source_addr: '46701113311',
|
||||
},
|
||||
seqNr: 2,
|
||||
});
|
||||
|
||||
const refused = await peer.next();
|
||||
|
||||
assert.equal(refused.cmdName, 'submit_sm_resp');
|
||||
assert.equal(refused.cmdStatus, 'ESME_RMSGQFUL');
|
||||
assert.equal(refused.cmdLength, 16);
|
||||
assert.deepEqual(refused.params, {});
|
||||
|
||||
peer.close();
|
||||
await smpp.close();
|
||||
});
|
||||
|
||||
test('keeps the reconnect loop alive when connect throws', async () => {
|
||||
let attempts = 0;
|
||||
const loop = new ReconnectLoop({
|
||||
|
||||
@@ -49,6 +49,14 @@ describe('string (Octet String)', () => {
|
||||
assert.deepEqual(types.string.read(encoded, 0), { bytesRead: 9, value: expected });
|
||||
});
|
||||
|
||||
// The length is one octet, so a longer value has nowhere to say how long it is.
|
||||
test('refuses a value longer than the length octet can count', () => {
|
||||
const tooLong = 'x'.repeat(256);
|
||||
|
||||
assert.ok(types.string.size(tooLong).err instanceof Error);
|
||||
assert.ok(types.string.write(tooLong, Buffer.alloc(300), 0).err instanceof Error);
|
||||
});
|
||||
|
||||
test('sizes as the string plus its length octet', () => {
|
||||
assert.deepEqual(types.string.size(expected), { size: 9 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user