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:
@@ -0,0 +1,40 @@
|
||||
import type { ConcatInfo } from './udh.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
import { hasUdh } from './defs/constants.ts';
|
||||
import { messageOctets } from './message-body.ts';
|
||||
import { paramNumber } from './defs/types.ts';
|
||||
|
||||
/** Where a segment sits in its message, and what ties it to the rest of that message. */
|
||||
export type Concat = ConcatInfo & {
|
||||
/** Which of the two carried the numbering: their references are counters of their own. */
|
||||
spelling: 'sar' | 'udh';
|
||||
};
|
||||
|
||||
function sarConcat(pduObj: PduObject): Concat | undefined {
|
||||
const reference = pduObj.tlvs.sar_msg_ref_num?.tagValue;
|
||||
const part = pduObj.tlvs.sar_segment_seqnum?.tagValue;
|
||||
const total = pduObj.tlvs.sar_total_segments?.tagValue;
|
||||
|
||||
if (typeof reference !== 'number' || typeof part !== 'number' || typeof total !== 'number') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { part, reference, spelling: 'sar', total };
|
||||
}
|
||||
|
||||
/**
|
||||
* How a PDU says it is one segment of a longer message: the UDH its body starts with, or the
|
||||
* sar_* TLVs SMPP 3.4 5.3.2.31-5.3.2.33 define in its place. A UDH that names the concatenation
|
||||
* wins; one carrying only a port or a language indicator leaves the TLVs to say.
|
||||
*/
|
||||
export function concatOf(pduObj: PduObject): Concat | undefined {
|
||||
const body = messageOctets(pduObj);
|
||||
const udh = body !== undefined && hasUdh(paramNumber(pduObj.params.esm_class, 0))
|
||||
? concatInfo(body)
|
||||
: undefined;
|
||||
|
||||
if (udh) return { ...udh, spelling: 'udh' };
|
||||
|
||||
return sarConcat(pduObj);
|
||||
}
|
||||
+14
-10
@@ -1,3 +1,4 @@
|
||||
import type { Concat } from './concat.ts';
|
||||
import type { DlrMerger } from './dlr-merger.ts';
|
||||
import type { ErrorName } from './defs/errors.ts';
|
||||
import type { LostGroup, Refusal } from './reassembly.ts';
|
||||
@@ -10,17 +11,22 @@ import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { HeldMessages } from './held-messages.ts';
|
||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||
import { bindCommands, defaults, standsInFor } from './session-options.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
import { hasUdh } from './defs/constants.ts';
|
||||
import { concatOf } from './concat.ts';
|
||||
import { createSms } from './sms.ts';
|
||||
import { dlrFromPdu } from './dlr.ts';
|
||||
import { messageOctets } from './message-body.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
import { paramText } from './defs/types.ts';
|
||||
import { respIdParams, segmentId } from './sms-id.ts';
|
||||
|
||||
/** SMPP 3.4 lists ESME_RMSGQFUL under submit_sm_resp only; 4.6.2's retryable code is another. */
|
||||
export function refusedSegmentStatus(carriedAs: string, refusal: Refusal): ErrorName {
|
||||
if (refusal === 'unplaceable') return 'ESME_RINVESMCLASS';
|
||||
export function refusedSegmentStatus(
|
||||
carriedAs: string,
|
||||
refusal: Refusal,
|
||||
spelling: Concat['spelling'],
|
||||
): ErrorName {
|
||||
// A sar_* segment's esm_class is 0x00 and correct: naming it would name the part the peer got right.
|
||||
if (refusal === 'unplaceable') {
|
||||
return spelling === 'sar' ? 'ESME_RINVTLVVAL' : 'ESME_RINVESMCLASS';
|
||||
}
|
||||
|
||||
return carriedAs === 'submit_sm' ? 'ESME_RMSGQFUL' : 'ESME_RX_T_APPN';
|
||||
}
|
||||
@@ -197,9 +203,7 @@ export class IncomingRequests {
|
||||
* one request at a time never sends the second segment until the first has been answered.
|
||||
*/
|
||||
private async onMessage(pduObj: PduObject): Promise<void> {
|
||||
const message = messageOctets(pduObj);
|
||||
const carriesUdh = hasUdh(paramNumber(pduObj.params.esm_class, 0));
|
||||
const concat = carriesUdh && message ? concatInfo(message) : undefined;
|
||||
const concat = concatOf(pduObj);
|
||||
|
||||
if (!concat) {
|
||||
this.emitSms([pduObj]);
|
||||
@@ -212,7 +216,7 @@ export class IncomingRequests {
|
||||
if (!collected.kept) {
|
||||
await this.session.sendReturn(
|
||||
pduObj,
|
||||
refusedSegmentStatus(this.carriedAs(pduObj), collected.refusal),
|
||||
refusedSegmentStatus(this.carriedAs(pduObj), collected.refusal, concat.spelling),
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
@@ -31,6 +31,7 @@ export {
|
||||
|
||||
export { dlrFromPdu, parseReceipt, receiptCodes } from './dlr.ts';
|
||||
export { messageOctets } from './message-body.ts';
|
||||
export { concatOf } from './concat.ts';
|
||||
export { concatInfo } from './udh.ts';
|
||||
export { PduFramer } from './pdu-framer.ts';
|
||||
export { uuidv7 } from './uuid.ts';
|
||||
@@ -38,6 +39,7 @@ export { uuidv7 } from './uuid.ts';
|
||||
export type { BindType, ClientOptions } from './client.ts';
|
||||
export type { Dlr, Receipt } from './dlr.ts';
|
||||
export type { SendDlrResult, SendRespOptions, Sms, SmsInput } from './sms.ts';
|
||||
export type { Concat } from './concat.ts';
|
||||
export type { ConcatInfo } from './udh.ts';
|
||||
export type { Result, VoidResult } from './result.ts';
|
||||
export type { SmppLog } from './log.ts';
|
||||
|
||||
+12
-10
@@ -1,4 +1,4 @@
|
||||
import type { ConcatInfo } from './udh.ts';
|
||||
import type { Concat } from './concat.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
@@ -96,12 +96,14 @@ function octetsOf(pduObj: PduObject): number {
|
||||
return octets;
|
||||
}
|
||||
|
||||
function groupKey(pduObj: PduObject, reference: number): string {
|
||||
// NUL: the one octet a C-Octet String address cannot hold, so no sender can forge another's key.
|
||||
function groupKey(pduObj: PduObject, concat: Concat): string {
|
||||
return [
|
||||
paramText(pduObj.params.source_addr),
|
||||
paramText(pduObj.params.destination_addr),
|
||||
String(reference),
|
||||
].join('_');
|
||||
concat.spelling,
|
||||
String(concat.reference),
|
||||
].join('\u0000');
|
||||
}
|
||||
|
||||
/** The text of a message, joining its segments in the order they were reassembled. */
|
||||
@@ -152,10 +154,10 @@ export class Reassembler {
|
||||
}
|
||||
|
||||
/** The group the segment joined, and always an answer for it: an unanswered one stalls a peer. */
|
||||
collect(pduObj: PduObject, concat: ConcatInfo): Collected {
|
||||
collect(pduObj: PduObject, concat: Concat): Collected {
|
||||
this.sweep();
|
||||
|
||||
const key = groupKey(pduObj, concat.reference);
|
||||
const key = groupKey(pduObj, concat);
|
||||
const existing = this.groups.get(key);
|
||||
|
||||
if (!this.placeable(concat, existing)) return { kept: false, refusal: 'unplaceable' };
|
||||
@@ -204,10 +206,10 @@ export class Reassembler {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a segment's UDH can join a group at all: its own numbering, and the group's total. */
|
||||
private placeable(concat: ConcatInfo, existing: Group | undefined): boolean {
|
||||
/** Whether a segment can join a group at all: its own numbering, and the group's total. */
|
||||
private placeable(concat: Concat, existing: Group | undefined): boolean {
|
||||
if (concat.part < 1 || concat.total < 1 || concat.part > concat.total) {
|
||||
this.log.warn('reassembler - dropping a segment the UDH numbers impossibly', {
|
||||
this.log.warn('reassembler - dropping an impossibly numbered segment', {
|
||||
part: concat.part,
|
||||
total: concat.total,
|
||||
});
|
||||
@@ -217,7 +219,7 @@ export class Reassembler {
|
||||
|
||||
// Parts 1/2 and 2/3 would otherwise complete the stored two-part group as a truncated message.
|
||||
if (existing && existing.total !== concat.total) {
|
||||
this.log.warn('reassembler - dropping a segment with an inconsistent UDH total', {
|
||||
this.log.warn('reassembler - dropping a segment with an inconsistent total', {
|
||||
existingTotal: existing.total,
|
||||
part: concat.part,
|
||||
total: concat.total,
|
||||
|
||||
Reference in New Issue
Block a user