Address the CodeRabbit review: inbound SMS, binary payloads and untrusted input
This commit is contained in:
+10
-6
@@ -3,7 +3,7 @@ import type { LogInt } from '@larvit/log';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { Socket } from 'node:net';
|
||||
import { Session } from './session.ts';
|
||||
import { checkSessionOptions } from './session-options.ts';
|
||||
import { checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { connect as netConnect } from 'node:net';
|
||||
import { connect as tlsConnect } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
@@ -107,11 +107,10 @@ function bindParams(options: ClientOptions, systemId: string) {
|
||||
async function bind(session: Session, options: ClientOptions): Promise<VoidResult> {
|
||||
const bindType = options.bindType ?? defaults.bindType;
|
||||
const systemId = options.username ?? defaults.username;
|
||||
const sent = await session.send({
|
||||
cmdName: `bind_${bindType}`,
|
||||
params: bindParams(options, systemId),
|
||||
...(options.signal ? { signal: options.signal } : {}),
|
||||
});
|
||||
const sent = await session.send(
|
||||
{ cmdName: `bind_${bindType}`, params: bindParams(options, systemId) },
|
||||
options.signal ? { signal: options.signal } : {},
|
||||
);
|
||||
|
||||
if (sent.err) return { err: sent.err };
|
||||
|
||||
@@ -124,7 +123,12 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
|
||||
return { err: new Error(`Remote host refused login: ${sent.pduObj.cmdStatus ?? 'unknown'}`) };
|
||||
}
|
||||
|
||||
const declared = sent.pduObj.tlvs.sc_interface_version?.tagValue;
|
||||
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
: undeclaredInterfaceVersion;
|
||||
session.log.info('client - bound', { bindType, systemId });
|
||||
|
||||
return {};
|
||||
|
||||
+27
-11
@@ -87,7 +87,10 @@ const latin1: Encoding = {
|
||||
|
||||
const ucs2: Encoding = {
|
||||
decode(buffer) {
|
||||
return Buffer.from(buffer).swap16().toString('utf16le');
|
||||
// A peer-controlled sm_length can cut a character in half, and swap16() refuses odd lengths.
|
||||
const whole = buffer.length - (buffer.length % 2);
|
||||
|
||||
return Buffer.from(buffer.subarray(0, whole)).swap16().toString('utf16le');
|
||||
},
|
||||
|
||||
encode(value) {
|
||||
@@ -113,22 +116,35 @@ export function detect(value: string): EncodingName {
|
||||
return 'UCS2';
|
||||
}
|
||||
|
||||
/**
|
||||
* SMPP data_coding is a flat table for 0x00-0x0E, but the 0x1X and 0xFX ranges carry a GSM message
|
||||
* class and encode the alphabet in bits 3-2 (or bit 2) instead — which is how a flash UCS2 message
|
||||
* arrives as 0x18. Alphabets with no codec here fall back to ASCII.
|
||||
*/
|
||||
export function encodingByDataCoding(dataCoding: number): EncodingName {
|
||||
/** The 0x1X and 0xFX ranges carry a GSM message class and put the alphabet in bits 3-2 or bit 2. */
|
||||
function messageClassEncoding(dataCoding: number): EncodingName | undefined {
|
||||
if ((dataCoding & 0xF0) === 0x10) {
|
||||
return ((dataCoding >> 2) & 0x03) === 0x02 ? 'UCS2' : 'ASCII';
|
||||
const alphabet = (dataCoding >> 2) & 0x03;
|
||||
|
||||
if (alphabet === 0x01) return 'LATIN1';
|
||||
|
||||
return alphabet === 0x02 ? 'UCS2' : 'ASCII';
|
||||
}
|
||||
|
||||
if ((dataCoding & 0xF0) === 0xF0) {
|
||||
return 'ASCII';
|
||||
return (dataCoding & 0x04) === 0x04 ? 'LATIN1' : 'ASCII';
|
||||
}
|
||||
|
||||
if (dataCoding === 0x03) return 'LATIN1';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* SMPP data_coding is a flat table for 0x00-0x0E, and the message class ranges are how a flash UCS2
|
||||
* message arrives as 0x18. The 8-bit binary codings resolve to LATIN1, the one codec here that maps
|
||||
* every octet to a code point and back unchanged, so a binary payload survives; alphabets with no
|
||||
* codec fall back to ASCII.
|
||||
*/
|
||||
export function encodingByDataCoding(dataCoding: number): EncodingName {
|
||||
const messageClass = messageClassEncoding(dataCoding);
|
||||
|
||||
if (messageClass) return messageClass;
|
||||
if (dataCoding === 0x08) return 'UCS2';
|
||||
|
||||
return 'ASCII';
|
||||
// 0x02 and 0x04 are 8-bit binary, 0x03 is Latin-1.
|
||||
return dataCoding >= 0x02 && dataCoding <= 0x04 ? 'LATIN1' : 'ASCII';
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ const specs = tlvSpecs({
|
||||
source_addr_subunit: { id: 0x000D, tag: 'source_addr_subunit', type: tlv.int8 },
|
||||
source_network_type: { id: 0x000E, tag: 'source_network_type', type: tlv.int8 },
|
||||
source_bearer_type: { id: 0x000F, tag: 'source_bearer_type', type: tlv.int8 },
|
||||
source_telematics_id: { id: 0x0010, tag: 'source_telematics_id', type: tlv.int16 },
|
||||
source_telematics_id: { id: 0x0010, tag: 'source_telematics_id', type: tlv.int8 },
|
||||
qos_time_to_live: { id: 0x0017, tag: 'qos_time_to_live', type: tlv.int32 },
|
||||
payload_type: { id: 0x0019, tag: 'payload_type', type: tlv.int8 },
|
||||
additional_status_info_text: { id: 0x001D, tag: 'additional_status_info_text', type: tlv.cstring },
|
||||
|
||||
+35
-3
@@ -142,6 +142,15 @@ function wantUnsuccessSmes(value: ParamValue): Result<{ smes: UnsuccessSme[] }>
|
||||
}
|
||||
|
||||
function readCstring(buffer: Buffer, offset: number): Result<{ bytesRead: number; value: string }> {
|
||||
// An offset at the end exactly is an absent trailing field, which real peers do send.
|
||||
if (outOfRange(buffer, offset, 0)) {
|
||||
return {
|
||||
err: new Error(
|
||||
`C-Octet String starts at offset ${String(offset)}, past a ${String(buffer.length)} octet buffer`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
|
||||
while (buffer[offset + length]) {
|
||||
@@ -197,6 +206,29 @@ export const int8 = intType(1, 0xFF, (b, o) => b.readUInt8(o), (b, v, o) => b.wr
|
||||
export const int16 = intType(2, 0xFFFF, (b, o) => b.readUInt16BE(o), (b, v, o) => b.writeUInt16BE(v, o));
|
||||
export const int32 = intType(4, 0xFFFFFFFF, (b, o) => b.readUInt32BE(o), (b, v, o) => b.writeUInt32BE(v, o));
|
||||
|
||||
const intByOctets: Record<number, WireType<number>> = { 1: int8, 2: int16, 4: int32 };
|
||||
|
||||
/**
|
||||
* The TLV header's length is what the parser skips past, so it is also the width the value is read
|
||||
* at — a peer that types a tag one octet wider than the table says still gets the value it meant.
|
||||
*/
|
||||
function tlvInt(declared: WireType<number>): WireType<number> {
|
||||
return {
|
||||
...declared,
|
||||
read(buffer, offset, length) {
|
||||
if (length === undefined) return declared.read(buffer, offset);
|
||||
|
||||
const width = intByOctets[length];
|
||||
|
||||
if (!width) {
|
||||
return { err: new Error(`Integer TLV declares ${String(length)} octets, expected 1, 2 or 4`) };
|
||||
}
|
||||
|
||||
return width.read(buffer, offset);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Octet String: a length octet followed by that many octets. */
|
||||
export const string: WireType<string> = {
|
||||
default: '',
|
||||
@@ -513,9 +545,9 @@ export const tlv = {
|
||||
return err ? { err } : writeCstring(text, buf, offset);
|
||||
},
|
||||
} satisfies WireType<string>,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
int8: tlvInt(int8),
|
||||
int16: tlvInt(int16),
|
||||
int32: tlvInt(int32),
|
||||
string: {
|
||||
default: '',
|
||||
read(buf: Buffer, offset: number, length = 0) {
|
||||
|
||||
+10
-2
@@ -82,8 +82,7 @@ function receiptDate(value: string | undefined): Date | undefined {
|
||||
|
||||
const [, years, months, days, hours, minutes, seconds] = match;
|
||||
const century = Math.floor(new Date().getUTCFullYear() / 100) * 100;
|
||||
|
||||
return new Date(Date.UTC(
|
||||
const date = new Date(Date.UTC(
|
||||
century + Number(years),
|
||||
Number(months) - 1,
|
||||
Number(days),
|
||||
@@ -91,6 +90,15 @@ function receiptDate(value: string | undefined): Date | undefined {
|
||||
Number(minutes),
|
||||
Number(seconds ?? 0),
|
||||
));
|
||||
|
||||
// Date.UTC rolls 31 February over into March rather than refusing it.
|
||||
const rolled = date.getUTCMonth() !== Number(months) - 1
|
||||
|| date.getUTCDate() !== Number(days)
|
||||
|| date.getUTCHours() !== Number(hours)
|
||||
|| date.getUTCMinutes() !== Number(minutes)
|
||||
|| date.getUTCSeconds() !== Number(seconds ?? 0);
|
||||
|
||||
return rolled ? undefined : date;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { DlrMerger } from './dlr-merger.ts';
|
||||
import type { LogInt } from '@larvit/log';
|
||||
import type { OnRequest } from './session-options.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import type { Session } from './session.ts';
|
||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||
import { bindCommands, defaults } from './session-options.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
import { consts } from './defs/constants.ts';
|
||||
import { createSms } from './sms.ts';
|
||||
import { dlrFromPdu } from './dlr.ts';
|
||||
import { paramText } from './defs/types.ts';
|
||||
|
||||
export type IncomingRequestsOptions = {
|
||||
dlrMerger: DlrMerger;
|
||||
log: LogInt;
|
||||
maxOctets?: number | undefined;
|
||||
maxReassembly?: number | undefined;
|
||||
onRequest?: OnRequest | undefined;
|
||||
reassemblyTimeout?: number | undefined;
|
||||
session: Session;
|
||||
systemId?: string | undefined;
|
||||
};
|
||||
|
||||
/** Everything the peer asks of a session: messages, receipts, links and the answers to them. */
|
||||
export class IncomingRequests {
|
||||
private readonly dlrMerger: DlrMerger;
|
||||
private readonly log: LogInt;
|
||||
private readonly onRequest: OnRequest | undefined;
|
||||
private readonly reassembler: Reassembler;
|
||||
private readonly session: Session;
|
||||
private readonly systemId: string;
|
||||
|
||||
constructor(options: IncomingRequestsOptions) {
|
||||
this.dlrMerger = options.dlrMerger;
|
||||
this.log = options.log;
|
||||
this.onRequest = options.onRequest;
|
||||
this.reassembler = new Reassembler({
|
||||
log: options.log,
|
||||
max: options.maxReassembly ?? defaults.maxReassembly,
|
||||
maxOctets: options.maxOctets,
|
||||
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
|
||||
});
|
||||
this.session = options.session;
|
||||
this.systemId = options.systemId ?? defaults.systemId;
|
||||
}
|
||||
|
||||
async handle(pduObj: PduObject): Promise<void> {
|
||||
if (this.onRequest && await this.onRequest(this.session, pduObj)) return;
|
||||
|
||||
switch (pduObj.cmdName) {
|
||||
case 'deliver_sm':
|
||||
await this.onDeliverSm(pduObj);
|
||||
break;
|
||||
case 'enquire_link':
|
||||
await this.session.sendReturn(pduObj);
|
||||
break;
|
||||
case 'submit_sm':
|
||||
this.onMessage(pduObj);
|
||||
break;
|
||||
case 'unbind':
|
||||
await this.session.sendReturn(pduObj);
|
||||
this.session.close();
|
||||
break;
|
||||
default:
|
||||
await this.unhandled(pduObj);
|
||||
}
|
||||
}
|
||||
|
||||
/** Drops the segments of every message that never became whole. */
|
||||
clear(): void {
|
||||
this.reassembler.clear();
|
||||
}
|
||||
|
||||
private async unhandled(pduObj: PduObject): Promise<void> {
|
||||
if (bindCommands.includes(pduObj.cmdName)) {
|
||||
this.log.info('session - bind on an already bound session', { cmdName: pduObj.cmdName });
|
||||
await this.session.sendReturn(pduObj, 'ESME_RALYBND', { system_id: this.systemId });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info('session - no handler for command', { cmdName: pduObj.cmdName });
|
||||
await this.session.sendReturn(pduObj, 'ESME_RINVCMDID');
|
||||
}
|
||||
|
||||
/** SMPP carries a mobile-originated message and a delivery receipt on the same command. */
|
||||
private async onDeliverSm(pduObj: PduObject): Promise<void> {
|
||||
const dlr = dlrFromPdu(pduObj);
|
||||
|
||||
if (!dlr) {
|
||||
this.onMessage(pduObj);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.emit('dlr', dlr, pduObj);
|
||||
|
||||
const merged = this.dlrMerger.collect(dlr);
|
||||
|
||||
if (merged) this.session.emit('messageDlr', merged);
|
||||
|
||||
await this.session.sendReturn(pduObj);
|
||||
}
|
||||
|
||||
private onMessage(pduObj: PduObject): void {
|
||||
const message = pduObj.params.short_message;
|
||||
const esmClass = pduObj.params.esm_class;
|
||||
const hasUdh = typeof esmClass === 'number'
|
||||
&& (esmClass & consts.ESM_CLASS.UDH_INDICATOR) === consts.ESM_CLASS.UDH_INDICATOR;
|
||||
const concat = hasUdh && Buffer.isBuffer(message) ? concatInfo(message) : undefined;
|
||||
|
||||
if (!concat) {
|
||||
this.emitSms([pduObj]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const whole = this.reassembler.collect(pduObj, concat);
|
||||
|
||||
if (whole) this.emitSms(whole);
|
||||
}
|
||||
|
||||
private emitSms(pduObjs: PduObject[]): void {
|
||||
const first = pduObjs[0];
|
||||
|
||||
if (!first) return;
|
||||
|
||||
this.session.emit('sms', createSms({
|
||||
from: paramText(first.params.source_addr),
|
||||
message: decodeSegments(pduObjs),
|
||||
pduObjs,
|
||||
session: this.session,
|
||||
to: paramText(first.params.destination_addr),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,15 @@ export class Reassembler {
|
||||
collect(pduObj: PduObject, concat: ConcatInfo): PduObject[] | undefined {
|
||||
this.sweep();
|
||||
|
||||
if (concat.part < 1 || concat.total < 1 || concat.part > concat.total) {
|
||||
this.log.warn('reassembler - dropping a segment the UDH numbers impossibly', {
|
||||
part: concat.part,
|
||||
total: concat.total,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = groupKey(pduObj, concat.reference);
|
||||
const group = this.groups.get(key) ?? this.open(key, concat.total);
|
||||
const replaced = group.parts.get(concat.part);
|
||||
|
||||
+16
-1
@@ -90,7 +90,7 @@ export class ReconnectLoop {
|
||||
return false;
|
||||
}
|
||||
|
||||
const up = await this.options.onConnected(opened.sock);
|
||||
const up = await this.bringUp(opened.sock);
|
||||
|
||||
if (up.err) {
|
||||
this.options.log.warn('reconnect - could not come back up', { message: up.err.message });
|
||||
@@ -102,4 +102,19 @@ export class ReconnectLoop {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The loop owns the socket until the owner is up on it, so a failed handover must not leak it. */
|
||||
private async bringUp(sock: Socket): Promise<VoidResult> {
|
||||
try {
|
||||
const up = await this.options.onConnected(sock);
|
||||
|
||||
if (up.err) sock.destroy();
|
||||
|
||||
return up;
|
||||
} catch (thrown: unknown) {
|
||||
sock.destroy();
|
||||
|
||||
return { err: thrown instanceof Error ? thrown : new Error(String(thrown)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -5,7 +5,7 @@ import type { Server as NetServer, Socket } from 'node:net';
|
||||
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
||||
import { checkSessionOptions } from './session-options.ts';
|
||||
import { checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { createServer as createNetServer } from 'node:net';
|
||||
import { createServer as createTlsServer } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
@@ -148,7 +148,9 @@ async function acceptBind(
|
||||
const declared = pduObj.params.interface_version;
|
||||
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number' ? declared : undefined;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
: undeclaredInterfaceVersion;
|
||||
|
||||
await session.sendReturn(pduObj, 'ESME_ROK', identity, bindRespTlvs(session, options));
|
||||
}
|
||||
|
||||
+11
-6
@@ -27,6 +27,13 @@ export const bindCommands: readonly string[] = [
|
||||
|
||||
export type SendOptions = { signal?: AbortSignal | undefined };
|
||||
|
||||
/**
|
||||
* First refusal on every incoming request. Returning true means the hook answered it and the
|
||||
* built-in handling is skipped — this is how the server owns bind without the session also
|
||||
* replying "invalid command".
|
||||
*/
|
||||
export type OnRequest = (session: Session, pduObj: PduObject) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* How to come back after an unexpected disconnect. The session owns the retry loop; the caller
|
||||
* supplies how to open a socket and what to do once it is open (bind, for a client).
|
||||
@@ -45,12 +52,7 @@ export type SessionOptions = {
|
||||
maxOctets?: number | undefined;
|
||||
maxOutstanding?: number | undefined;
|
||||
maxReassembly?: number | undefined;
|
||||
/**
|
||||
* First refusal on every incoming request. Returning true means the hook answered it and the
|
||||
* built-in handling is skipped — this is how the server owns bind without the session also
|
||||
* replying "invalid command".
|
||||
*/
|
||||
onRequest?: ((session: Session, pduObj: PduObject) => Promise<boolean>) | undefined;
|
||||
onRequest?: OnRequest | undefined;
|
||||
reassemblyTimeout?: number | undefined;
|
||||
reconnect?: ReconnectOptions | undefined;
|
||||
responseTimeout?: number | undefined;
|
||||
@@ -61,6 +63,9 @@ export type SessionOptions = {
|
||||
|
||||
export const defaultSystemId = '';
|
||||
|
||||
/** SMPP 3.4: a peer that declares no version at all is one from before optional parameters. */
|
||||
export const undeclaredInterfaceVersion = 0x00;
|
||||
|
||||
export const defaults = {
|
||||
/** Receipts of a multipart message can be a working day apart, so the cap does the bounding. */
|
||||
dlrMergeTimeout: 86_400_000,
|
||||
|
||||
+19
-103
@@ -9,19 +9,15 @@ import type { SendSmsOptions, SendSmsResult } from './send-sms.ts';
|
||||
import type { Socket } from 'node:net';
|
||||
import { DlrMerger } from './dlr-merger.ts';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { IncomingRequests } from './incoming-requests.ts';
|
||||
import { LinkTimers } from './link-timers.ts';
|
||||
import { PduFramer } from './pdu-framer.ts';
|
||||
import { PendingRequests } from './pending-requests.ts';
|
||||
import { ReconnectLoop } from './reconnect-loop.ts';
|
||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||
import { SendWindow } from './send-window.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
import { consts, optionalParamsMinVersion } from './defs/constants.ts';
|
||||
import { createSms } from './sms.ts';
|
||||
import { optionalParamsMinVersion } from './defs/constants.ts';
|
||||
import { bindCommands, defaultSystemId, defaults } from './session-options.ts';
|
||||
import { dlrFromPdu } from './dlr.ts';
|
||||
import { isResp, objToPdu, pduReturn, pduToObj } from './pdu.ts';
|
||||
import { paramText } from './defs/types.ts';
|
||||
import { silentLog } from './log.ts';
|
||||
import { submitSms } from './send-sms.ts';
|
||||
|
||||
@@ -42,14 +38,14 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
readonly log: LogInt;
|
||||
|
||||
loggedIn = false;
|
||||
/** The interface_version the peer declared when binding; undefined until a bind is accepted. */
|
||||
/** What the peer declared when binding: 0x00 if it declared none, undefined before any bind. */
|
||||
peerInterfaceVersion: number | undefined = undefined;
|
||||
userData: unknown = undefined;
|
||||
|
||||
private readonly dlrMerger: DlrMerger;
|
||||
private readonly incoming: IncomingRequests;
|
||||
private readonly options: SessionOptions;
|
||||
private readonly pending: PendingRequests;
|
||||
private readonly reassembler: Reassembler;
|
||||
private readonly reconnectLoop: ReconnectLoop | undefined;
|
||||
private readonly timers: LinkTimers;
|
||||
private readonly window: SendWindow;
|
||||
@@ -87,13 +83,17 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
max: defaults.maxDlrMerges,
|
||||
timeout: defaults.dlrMergeTimeout,
|
||||
});
|
||||
this.pending = new PendingRequests(this.log);
|
||||
this.reassembler = new Reassembler({
|
||||
this.incoming = new IncomingRequests({
|
||||
dlrMerger: this.dlrMerger,
|
||||
log: this.log,
|
||||
max: options.maxReassembly ?? defaults.maxReassembly,
|
||||
maxOctets: options.maxOctets,
|
||||
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
|
||||
maxReassembly: options.maxReassembly,
|
||||
onRequest: options.onRequest,
|
||||
reassemblyTimeout: options.reassemblyTimeout,
|
||||
session: this,
|
||||
systemId: options.systemId,
|
||||
});
|
||||
this.pending = new PendingRequests(this.log);
|
||||
this.reconnectLoop = this.loopFor(options.reconnect);
|
||||
this.sock = options.sock;
|
||||
this.timers = new LinkTimers({
|
||||
@@ -243,6 +243,11 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
input: PduObjectInput,
|
||||
options: SendOptions,
|
||||
): Promise<Result<{ pduObj: PduObject }>> {
|
||||
// pending.wait() alone settles the caller while the request still goes out to the peer.
|
||||
if (options.signal?.aborted === true) {
|
||||
return { err: new Error('Aborted before the request was sent') };
|
||||
}
|
||||
|
||||
const seqNr = this.pending.nextSeqNr();
|
||||
const built = objToPdu({ ...input, seqNr });
|
||||
|
||||
@@ -270,7 +275,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
this.timers.clear();
|
||||
this.pending.settleAll(new Error('Session closed before a response arrived'));
|
||||
this.dlrMerger.clear();
|
||||
this.reassembler.clear();
|
||||
this.incoming.clear();
|
||||
this.sock.destroy();
|
||||
this.emit('close');
|
||||
}
|
||||
@@ -343,7 +348,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
|
||||
this.emit('incomingPduObj', pduObj);
|
||||
// Every application hook and listener reached from an incoming PDU funnels through here.
|
||||
void this.handle(pduObj).catch((thrown: unknown) => {
|
||||
void this.incoming.handle(pduObj).catch((thrown: unknown) => {
|
||||
const err = thrown instanceof Error ? thrown : new Error(String(thrown));
|
||||
|
||||
this.log.error('session - a handler threw', { message: err.message });
|
||||
@@ -351,95 +356,6 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
});
|
||||
}
|
||||
|
||||
private async handle(pduObj: PduObject): Promise<void> {
|
||||
const onRequest = this.options.onRequest;
|
||||
|
||||
if (onRequest && await onRequest(this, pduObj)) return;
|
||||
|
||||
switch (pduObj.cmdName) {
|
||||
case 'deliver_sm':
|
||||
await this.onDeliverSm(pduObj);
|
||||
break;
|
||||
case 'enquire_link':
|
||||
await this.sendReturn(pduObj);
|
||||
break;
|
||||
case 'submit_sm':
|
||||
this.onSubmitSm(pduObj);
|
||||
break;
|
||||
case 'unbind':
|
||||
await this.sendReturn(pduObj);
|
||||
this.close();
|
||||
break;
|
||||
default:
|
||||
await this.unhandled(pduObj);
|
||||
}
|
||||
}
|
||||
|
||||
private async unhandled(pduObj: PduObject): Promise<void> {
|
||||
if (bindCommands.includes(pduObj.cmdName)) {
|
||||
this.log.info('session - bind on an already bound session', { cmdName: pduObj.cmdName });
|
||||
await this.sendReturn(pduObj, 'ESME_RALYBND', {
|
||||
system_id: this.options.systemId ?? defaults.systemId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info('session - no handler for command', { cmdName: pduObj.cmdName });
|
||||
await this.sendReturn(pduObj, 'ESME_RINVCMDID');
|
||||
}
|
||||
|
||||
private onSubmitSm(pduObj: PduObject): void {
|
||||
const message = pduObj.params.short_message;
|
||||
const esmClass = pduObj.params.esm_class;
|
||||
const hasUdh = typeof esmClass === 'number'
|
||||
&& (esmClass & consts.ESM_CLASS.UDH_INDICATOR) === consts.ESM_CLASS.UDH_INDICATOR;
|
||||
const concat = hasUdh && Buffer.isBuffer(message) ? concatInfo(message) : undefined;
|
||||
|
||||
if (!concat) {
|
||||
this.emitSms([pduObj]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const whole = this.reassembler.collect(pduObj, concat);
|
||||
|
||||
if (whole) this.emitSms(whole);
|
||||
}
|
||||
|
||||
private emitSms(pduObjs: PduObject[]): void {
|
||||
const first = pduObjs[0];
|
||||
|
||||
if (!first) return;
|
||||
|
||||
this.emit('sms', createSms({
|
||||
from: paramText(first.params.source_addr),
|
||||
message: decodeSegments(pduObjs),
|
||||
pduObjs,
|
||||
session: this,
|
||||
to: paramText(first.params.destination_addr),
|
||||
}));
|
||||
}
|
||||
|
||||
private async onDeliverSm(pduObj: PduObject): Promise<void> {
|
||||
const dlr = dlrFromPdu(pduObj);
|
||||
|
||||
if (!dlr) {
|
||||
this.log.info('session - deliver_sm carries no delivery report', { seqNr: pduObj.seqNr });
|
||||
await this.sendReturn(pduObj, 'ESME_RINVTLVSTREAM');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('dlr', dlr, pduObj);
|
||||
|
||||
const merged = this.dlrMerger.collect(dlr);
|
||||
|
||||
if (merged) this.emit('messageDlr', merged);
|
||||
|
||||
await this.sendReturn(pduObj);
|
||||
}
|
||||
|
||||
private resetTimers(): void {
|
||||
if (this.closed) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user