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:
+3
-2
@@ -4,6 +4,7 @@ import type { PduObject } from './pdu.ts';
|
||||
import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { consts, constsById, hasUdh, messageTypeOf } from './defs/constants.ts';
|
||||
import { encodings } from './defs/encodings.ts';
|
||||
import { messageOctets } from './message-body.ts';
|
||||
import { normaliseSmsId } from './sms-id.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
import { udhLength } from './udh.ts';
|
||||
@@ -157,7 +158,7 @@ function messageType(pduObj: PduObject): MessageType {
|
||||
|
||||
/** SMPP 3.4 Appendix B makes a receipt fixed text, so its octets are read as octets, not decoded. */
|
||||
function receiptBody(pduObj: PduObject): string {
|
||||
const octets = pduObj.shortMessageOctets;
|
||||
const octets = messageOctets(pduObj);
|
||||
|
||||
if (octets === undefined) return paramText(pduObj.params.short_message);
|
||||
|
||||
@@ -203,7 +204,7 @@ function receiptStatus(
|
||||
return { statusId: tlvState, statusMsg: isMessageState(named) ? named : scraped };
|
||||
}
|
||||
|
||||
/** The delivery report a deliver_sm carries, or nothing when it carries a message instead. */
|
||||
/** The delivery report a deliver_sm or data_sm carries, or nothing where it carries a message. */
|
||||
export function dlrFromPdu(pduObj: PduObject, format: SmsIdFormat = {}): Dlr | undefined {
|
||||
const type = messageType(pduObj);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { CommandName } from './defs/commands.ts';
|
||||
import type { DlrMerger } from './dlr-merger.ts';
|
||||
import type { ErrorName } from './defs/errors.ts';
|
||||
import type { LostGroup, Refusal } from './reassembly.ts';
|
||||
@@ -10,19 +9,20 @@ import type { SmppLog } from './log.ts';
|
||||
import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { HeldMessages } from './held-messages.ts';
|
||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||
import { bindCommands, defaults } from './session-options.ts';
|
||||
import { bindCommands, defaults, standsInFor } from './session-options.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
import { hasUdh } from './defs/constants.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 { 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(cmdName: CommandName, refusal: Refusal): ErrorName {
|
||||
export function refusedSegmentStatus(carriedAs: string, refusal: Refusal): ErrorName {
|
||||
if (refusal === 'unplaceable') return 'ESME_RINVESMCLASS';
|
||||
|
||||
return cmdName === 'deliver_sm' ? 'ESME_RX_T_APPN' : 'ESME_RMSGQFUL';
|
||||
return carriedAs === 'submit_sm' ? 'ESME_RMSGQFUL' : 'ESME_RX_T_APPN';
|
||||
}
|
||||
|
||||
const lostReasons: Record<LostGroup['reason'], string> = {
|
||||
@@ -104,9 +104,17 @@ export class IncomingRequests {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.route(pduObj);
|
||||
}
|
||||
|
||||
private async route(pduObj: PduObject): Promise<void> {
|
||||
switch (pduObj.cmdName) {
|
||||
case 'data_sm':
|
||||
case 'deliver_sm':
|
||||
await this.onDeliverSm(pduObj);
|
||||
// A data_sm at the SMSC end is a submission, and a submission is never a report.
|
||||
await (this.carriedAs(pduObj) === 'submit_sm'
|
||||
? this.onMessage(pduObj)
|
||||
: this.onDelivery(pduObj));
|
||||
break;
|
||||
case 'enquire_link':
|
||||
await this.session.sendReturn(pduObj);
|
||||
@@ -161,8 +169,12 @@ export class IncomingRequests {
|
||||
await this.session.sendReturn(pduObj, 'ESME_RINVCMDID');
|
||||
}
|
||||
|
||||
private carriedAs(pduObj: PduObject): string {
|
||||
return standsInFor(pduObj.cmdName, this.session.linkEnd);
|
||||
}
|
||||
|
||||
/** SMPP carries a mobile-originated message and a delivery receipt on the same command. */
|
||||
private async onDeliverSm(pduObj: PduObject): Promise<void> {
|
||||
private async onDelivery(pduObj: PduObject): Promise<void> {
|
||||
const dlr = dlrFromPdu(pduObj, this.smsIdFormat);
|
||||
|
||||
if (!dlr) {
|
||||
@@ -185,9 +197,9 @@ 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 = pduObj.params.short_message;
|
||||
const message = messageOctets(pduObj);
|
||||
const carriesUdh = hasUdh(paramNumber(pduObj.params.esm_class, 0));
|
||||
const concat = carriesUdh && Buffer.isBuffer(message) ? concatInfo(message) : undefined;
|
||||
const concat = carriesUdh && message ? concatInfo(message) : undefined;
|
||||
|
||||
if (!concat) {
|
||||
this.emitSms([pduObj]);
|
||||
@@ -198,7 +210,10 @@ export class IncomingRequests {
|
||||
const collected = this.reassembler.collect(pduObj, concat);
|
||||
|
||||
if (!collected.kept) {
|
||||
await this.session.sendReturn(pduObj, refusedSegmentStatus(pduObj.cmdName, collected.refusal));
|
||||
await this.session.sendReturn(
|
||||
pduObj,
|
||||
refusedSegmentStatus(this.carriedAs(pduObj), collected.refusal),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export {
|
||||
} from './message.ts';
|
||||
|
||||
export { dlrFromPdu, parseReceipt, receiptCodes } from './dlr.ts';
|
||||
export { messageOctets } from './message-body.ts';
|
||||
export { concatInfo } from './udh.ts';
|
||||
export { PduFramer } from './pdu-framer.ts';
|
||||
export { uuidv7 } from './uuid.ts';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { PduObject } from './pdu.ts';
|
||||
|
||||
/**
|
||||
* The user data, wherever the peer put it. SMPP 3.4 5.3.2.32 carries up to 64 KB in
|
||||
* `message_payload` with `sm_length` 0, and a `data_sm` has nowhere else to put a body at all.
|
||||
* A filled `short_message` is the peer using the mandatory field, so it wins over a TLV the spec
|
||||
* only allows in its place.
|
||||
*/
|
||||
export function messageOctets(pduObj: PduObject): Buffer | undefined {
|
||||
const shortMessage = pduObj.shortMessageOctets;
|
||||
|
||||
if (shortMessage !== undefined && shortMessage.length > 0) return shortMessage;
|
||||
|
||||
const payload = pduObj.tlvs.message_payload?.tagValue;
|
||||
|
||||
return Buffer.isBuffer(payload) ? payload : shortMessage;
|
||||
}
|
||||
+4
-1
@@ -45,7 +45,10 @@ export type PduObject = {
|
||||
cmdStatusId: number;
|
||||
params: Record<string, ParamValue>;
|
||||
seqNr: number;
|
||||
/** short_message as it arrived, whatever data_coding turned `params.short_message` into. */
|
||||
/**
|
||||
* short_message as it arrived, whatever data_coding turned `params.short_message` into. A body
|
||||
* the peer put in `message_payload` is not here; `messageOctets()` is what reads either.
|
||||
*/
|
||||
shortMessageOctets: Buffer | undefined;
|
||||
tlvs: Record<string, Tlv>;
|
||||
};
|
||||
|
||||
+9
-8
@@ -5,6 +5,7 @@ import type { SmppLog } from './log.ts';
|
||||
import type { Tlv } from './defs/tlvs.ts';
|
||||
import { ExpiringGroups } from './expiring-groups.ts';
|
||||
import { decodeMessage } from './message.ts';
|
||||
import { messageOctets } from './message-body.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
import { uuidv7 } from './uuid.ts';
|
||||
|
||||
@@ -108,15 +109,15 @@ export function decodeSegments(pduObjs: PduObject[]): string {
|
||||
let message = '';
|
||||
|
||||
for (const pduObj of pduObjs) {
|
||||
const part = pduObj.params.short_message;
|
||||
const part = messageOctets(pduObj);
|
||||
|
||||
message += Buffer.isBuffer(part)
|
||||
? decodeMessage(
|
||||
part,
|
||||
paramNumber(pduObj.params.data_coding, 0),
|
||||
paramNumber(pduObj.params.esm_class, 0),
|
||||
).message
|
||||
: paramText(part);
|
||||
if (part === undefined) continue;
|
||||
|
||||
message += decodeMessage(
|
||||
part,
|
||||
paramNumber(pduObj.params.data_coding, 0),
|
||||
paramNumber(pduObj.params.esm_class, 0),
|
||||
).message;
|
||||
}
|
||||
|
||||
return message;
|
||||
|
||||
@@ -241,6 +241,7 @@ function onConnection(sock: Socket, options: ServerOptions, server: SmppServer):
|
||||
systemId: options.systemId ?? defaults.systemId,
|
||||
});
|
||||
|
||||
session.linkEnd = 'smsc';
|
||||
server.sessions.add(session);
|
||||
session.on('close', () => server.sessions.delete(session));
|
||||
|
||||
|
||||
+22
-3
@@ -32,6 +32,9 @@ export const bindCommands: readonly string[] = [
|
||||
|
||||
export type BindType = 'receiver' | 'transceiver' | 'transmitter';
|
||||
|
||||
/** Which end of the link a session is. Only `server()` is the SMSC; everything else is the ESME. */
|
||||
export type LinkEnd = 'esme' | 'smsc';
|
||||
|
||||
export function bindTypeFromCommand(cmdName: string): BindType | undefined {
|
||||
if (cmdName === 'bind_receiver') return 'receiver';
|
||||
if (cmdName === 'bind_transceiver') return 'transceiver';
|
||||
@@ -40,14 +43,30 @@ export function bindTypeFromCommand(cmdName: string): BindType | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which message-carrying command an inbound one stands in for. Every command but `data_sm` names
|
||||
* its own direction; that one travels either way, so the end it arrived at is what says.
|
||||
*/
|
||||
export function standsInFor(cmdName: string, linkEnd: LinkEnd): string {
|
||||
if (cmdName !== 'data_sm') return cmdName;
|
||||
|
||||
return linkEnd === 'smsc' ? 'submit_sm' : 'deliver_sm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a bind direction carries a command at all. A receiver-bound ESME submits nothing and a
|
||||
* transmitter-bound one is delivered nothing, whichever end of the link is looking. A session that
|
||||
* has not bound carries everything, since nothing has declared a direction yet.
|
||||
*/
|
||||
export function bindCarries(bindType: BindType | undefined, cmdName: string): boolean {
|
||||
if (bindType === 'receiver') return cmdName !== 'submit_sm';
|
||||
if (bindType === 'transmitter') return cmdName !== 'deliver_sm';
|
||||
export function bindCarries(
|
||||
bindType: BindType | undefined,
|
||||
cmdName: string,
|
||||
linkEnd: LinkEnd,
|
||||
): boolean {
|
||||
const carried = standsInFor(cmdName, linkEnd);
|
||||
|
||||
if (bindType === 'receiver') return carried !== 'submit_sm';
|
||||
if (bindType === 'transmitter') return carried !== 'deliver_sm';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+4
-2
@@ -3,7 +3,7 @@ import type { MessageDlr } from './dlr-merger.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject, PduObjectInput, TlvInput } from './pdu.ts';
|
||||
import type { PduRefusedError } from './pdu-refusal.ts';
|
||||
import type { BindType, CloseOptions, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { BindType, CloseOptions, LinkEnd, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { SendSmsOptions, SendSmsResult } from './send-sms.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
@@ -54,6 +54,8 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
|
||||
/** The role the ESME bound with, whichever end of the link this is. Undefined before any bind. */
|
||||
boundAs: BindType | undefined = undefined;
|
||||
/** Which end of the link this is. `server()` sets it; a hand-wired SMSC must set it too. */
|
||||
linkEnd: LinkEnd = 'esme';
|
||||
loggedIn = false;
|
||||
/** What the peer declared when binding: 0x00 if it declared none, undefined before any bind. */
|
||||
peerInterfaceVersion: number | undefined = undefined;
|
||||
@@ -154,7 +156,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
|
||||
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
|
||||
bindAllows(cmdName: string): boolean {
|
||||
return bindCarries(this.boundAs, cmdName);
|
||||
return bindCarries(this.boundAs, cmdName, this.linkEnd);
|
||||
}
|
||||
|
||||
/** SMPP 3.4 forbids sending optional parameters to a peer that declared an older version. */
|
||||
|
||||
Reference in New Issue
Block a user