Reassemble a concatenated message segmented with the sar_* TLVs (#91)

* Regression tests for inbound sar_* concatenation

* Reassemble a concatenated message segmented with the sar_* TLVs

* Record the sar_* fix in the jasmin and Java-client findings

* Name the sar_* TLVs in the refusal, and separate group keys the addresses cannot forge

* Name the field a refused segment got wrong, and key groups on a separator no address holds

* Bound the new session tests, and derive Concat from the UDH fields
This commit is contained in:
2026-09-06 21:03:14 +02:00
committed by GitHub
parent 061c1871bd
commit 18de565aad
12 changed files with 595 additions and 89 deletions
+14 -3
View File
@@ -159,6 +159,12 @@ client reads one as a delivery, so a message on it arrives as `sms` and a receip
`server()` session reads one as the submission it is and always hands it to you as `sms`. Either
way it is answered `data_sm_resp`.
Nor does the way a peer ties a long message's segments together. A user data header at the start of
the body and the `sar_msg_ref_num`, `sar_total_segments` and `sar_segment_seqnum` TLVs are the two
spellings of the same thing, and either reassembles into one `sms`. A header that numbers the
segment is what a PDU carrying both is read from, and the two reference numbers are counters of
their own — the same number in each is two different messages.
Matching a receipt to a send means comparing `dlr.smsId` against the `smsIds` that `sendSms()`
returned. Some SMSCs write the two in different notations — a hex `message_id` on the
`submit_sm_resp` and a decimal `id:` in the receipt, or one of them zero-padded — and the comparison
@@ -228,8 +234,8 @@ await smpp.close(); // stop listening, then drain and close every live sess
A message that arrived in several segments was already answered when you see it: each segment is
answered as it lands, because a relaying SMSC will not send the next one until the last is answered.
That answer is `ESME_ROK` unless the segment's header names no message this session can join, which
refuses it, or the reassembly buffer is full, which asks the SMSC to keep it and try again.
That answer is `ESME_ROK` unless the segment numbers itself into no message this session can join,
which refuses it, or the reassembly buffer is full, which asks the SMSC to keep it and try again.
`sms.answeredOnArrival` says whether the message you are holding was answered that way — a segment count cannot, since a peer
may number a concatenated message one part of one. The id was fixed with the first segment, so
`sendResp()` there only says you are done with the message, and returns an `err` for an `smsId` or a
@@ -507,7 +513,9 @@ if (isCommand(pduObj, 'submit_sm')) {
`params.short_message` is decoded with the PDU's own `data_coding`; `shortMessageOctets` is that
same field exactly as it arrived. Neither holds the body of a PDU that carried it in the
`message_payload` TLV instead, which a `data_sm` always does — `messageOctets(pduObj)` is the one
answer to which of the two the peer used.
answer to which of the two the peer used. `concatOf(pduObj)` is the same for concatenation: the
`part`, `total` and `reference` a PDU declares, and the `spelling``'udh'` or `'sar'` — that
carried them, or `undefined` where the PDU is a whole message.
The spec tables are exported both individually (`cmds`, `consts`, `encodings`, `errors`, `tlvs`,
`types`, and the matching `*ById` maps) and grouped as `defs`.
@@ -576,6 +584,9 @@ have worked around any of these, remove the workaround:
`data_sm` was answered `ESME_RINVCMDID`, so a receipt thrown on one was lost with nothing said.
Both reach the application now — a receipt as `dlr`, answered for you, and a message as `sms` for
you to answer like any other.
- A long message whose segments were tied together by the `sar_msg_ref_num`, `sar_total_segments`
and `sar_segment_seqnum` TLVs rather than by a user data header was never reassembled, so each
segment arrived as its own message. Both spellings reassemble now.
- Short or malformed PDUs threw out of the codec instead of being reported as a parse failure.
- Binds now declare `interface_version` 0x34. 0.4.0 declared 0x00, which tells the SMSC the ESME
speaks SMPP 3.3 or earlier — and a spec-following SMSC then withholds every optional parameter,