Read a body from message_payload and accept data_sm (#84)

* Regression tests for a body in message_payload and for data_sm

* Read a body from message_payload and accept data_sm

* Assert the fixed message_payload and data_sm behaviour against Jasmin

* Record the message_payload and data_sm fixes in the Jasmin findings

* Read an inbound data_sm as the direction it travelled, and export messageOctets

* Refuse a segment with the code its stand-in command defines

* Name the stand-in the refusal status is read from
This commit is contained in:
2026-09-06 05:59:27 +02:00
committed by GitHub
parent 5b7b563dc2
commit 184d1dc7af
17 changed files with 552 additions and 41 deletions
+18
View File
@@ -99,6 +99,13 @@ send it over the connector's session, and inspect the `sms` event at a client bo
**Severity.** Medium: silent data loss, not a wire error - the message is fully present on the wire
and Jasmin forwards it faithfully; only this library's read of it is incomplete.
**Fixed** in [#84](https://github.com/larvit/larvitsmpp/pull/84): the body is read from
`message_payload` where `short_message` carries none. The relay is confirmed on the wire - the
capture's one `sm_length` 0 `deliver_sm` is Jasmin's own, out of `smpps` to our client, carrying the
`0x0424` TLV intact. The reproducer's own fixture was wrong as well as the library: it built the TLV
as Latin-1 while the PDU declared `data_coding` 0, so `_` (GSM 03.38 0x11, Latin-1 0x5F) came back
as `§` even once the body was read. It now encodes the payload the way the PDU says it is written.
### `sar_*`/UDH segmentation from an upstream SMSC reassembles fine (target 3, MO direction) - not reproduced as a defect here
C8's SAR and UDH MO pushes both reassembled into one whole `sms` at our real client. This does not
@@ -121,6 +128,12 @@ Our client's `incomingPduObj` sees it arrive; no `dlr` event ever fires. The rec
**Severity.** Medium: a real, documented Jasmin configuration (`dlr_pdu = data_sm`) that a receipt
depends on silently drops delivery reports, with no error surfaced to the application either.
**Fixed** in [#84](https://github.com/larvit/larvitsmpp/pull/84): `data_sm` is classified and
answered exactly as `deliver_sm` is, and the receipt reaches `dlr` naming the id the `submit_sm_resp`
carried, `DELIVERED`. The wire histogram still shows no `data_sm`: `capture` runs
`network_mode: service:jasmin`, so it only ever sees the main instance's namespace, never
`jasmin-datasm`'s.
### A multi-part MT send deadlocks against Jasmin's serialized per-connector relay - a library/peer interaction, not a wire defect
**What happened.** A 2-, 3-, 10-segment GSM or UCS-2 message sent through Jasmin's `smpps` (our real
@@ -215,6 +228,11 @@ arrives, with `<base>-<n>` off an id the group is opened with. All four multi-se
- Whether the deadlock above is specific to a message requesting a receipt (`registered_delivery`
set) or would also occur for a plain multi-segment send with no `dlr` - not isolated separately,
since every `C3+C7` case here requests one.
- Whether a real peer accepts a `data_sm_resp` carrying a `message_id`. SMPP 3.4 4.7.2 defines the
field, unlike `deliver_sm_resp`'s, and this library fills it when a `data_sm` carried a message -
but Jasmin only ever sends one as a receipt, which is answered with the field empty, so the filled
case has met no peer. Jasmin FINing over a `deliver_sm_resp` that carried one is the nearest
precedent there is.
- Whether Jasmin, given an *upstream* connector that itself defaults to SAR (rather than our
library's own UDH), would relay an MT message using SAR instead of preserving our UDH bytes - the
120s-timeout deadlock always intervened before a second segment could be observed on the wire in
+16 -11
View File
@@ -11,7 +11,7 @@ import { client } from '../src/client.ts';
import { closeAfter } from '../test/teardown.ts';
import { paramText } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import { splitMessage } from '../src/message.ts';
import { encodeMessage, splitMessage } from '../src/message.ts';
import { submitSmParams } from '../src/send-sms.ts';
const PEER_HOST = process.env.PEER_HOST ?? 'jasmin';
@@ -246,7 +246,8 @@ async function sendMessagePayloadMo(session: Session, opts: { from: string; mess
source_addr: opts.from,
},
tlvs: {
message_payload: { tagValue: Buffer.from(opts.message, 'latin1') },
// The body is octets under the PDU's own data_coding wherever it is carried, and 0 is GSM.
message_payload: { tagValue: encodeMessage(opts.message, 'ASCII').buffer },
},
});
}
@@ -565,20 +566,20 @@ describe('C8 (target 2) - message_payload with sm_length 0', () => {
assert.equal(pushed.err, undefined, 'expected Jasmin to accept a message_payload-only deliver_sm from its connector');
const arrived = await waitFor(() => sms.find(s => s.message === ''), 5000);
const arrived = await waitFor(() => sms.find(s => s.message === text), 5000);
await session.close({ signal: AbortSignal.abort() });
// Confirmed (target 2): Jasmin relays message_payload faithfully (sm_length 0, the real text
// in the TLV) - our own incoming-requests.ts reads short_message only, so the sms that arrives
// here has the right envelope (from/to) but an empty message, never the text carried in the
// TLV. See findings/03-jasmin.md for the reproducer.
assert.ok(arrived, 'expected an sms to arrive (with an empty message, per target 2) for the message_payload push');
// Jasmin relays message_payload faithfully (sm_length 0, the real text in the TLV), so the
// whole body has to reach the application from there.
assert.ok(arrived, 'expected the message_payload body to arrive as the sms text');
assert.equal(arrived.from, TO);
assert.equal(arrived.to, FROM);
});
});
describe('C9 (target 4) - DLR as data_sm against the jasmin-datasm instance', () => {
test('a receipt thrown as data_sm is not read as a dlr; the raw PDU still arrives', async t => {
test('a receipt thrown as data_sm reaches the dlr event', async t => {
await waitForUpstreamSession('datasm');
const { err, session } = await client({ host: DATASM_HOST, password: PASSWORD, port: PEER_PORT, username: USERNAME });
@@ -597,12 +598,16 @@ describe('C9 (target 4) - DLR as data_sm against the jasmin-datasm instance', ()
assert.equal(sent.err, undefined);
const [smsId] = sent.smsIds;
const arrived = await waitFor(() => incomingDataSm[0], 15_000);
assert.ok(smsId);
assert.ok(arrived, 'expected Jasmin to throw the receipt as data_sm (dlr_pdu = data_sm)');
await delay(2000);
assert.deepEqual(dlrs, [], 'data_sm is answered ESME_RINVCMDID and never reaches the dlr event (target 4)');
const dlr = await waitFor(() => dlrs.find(one => one.smsId === smsId), 10_000);
assert.ok(dlr, `no dlr for the receipt Jasmin threw as data_sm, id ${smsId}`);
assert.equal(dlr.statusMsg, 'DELIVERED');
});
});