Read a peer's submit and receipt message ids into one notation before comparing them
This commit is contained in:
@@ -2,6 +2,7 @@ import type { ConnectionOptions } from 'node:tls';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { BindType } from './session-options.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import type { SmsIdFormats } from './sms-id.ts';
|
||||
import type { Socket } from 'node:net';
|
||||
export type { BindType };
|
||||
|
||||
@@ -29,6 +30,7 @@ export type ClientOptions = {
|
||||
responseTimeout?: number;
|
||||
shutdownTimeout?: number;
|
||||
signal?: AbortSignal;
|
||||
smsIdFormat?: SmsIdFormats;
|
||||
systemType?: string;
|
||||
tls?: ConnectionOptions | boolean;
|
||||
username?: string;
|
||||
@@ -147,6 +149,7 @@ function createSession(options: ClientOptions, log: SmppLog, sock: Socket): Sess
|
||||
maxOutstanding: options.maxOutstanding,
|
||||
responseTimeout: options.responseTimeout,
|
||||
shutdownTimeout: options.shutdownTimeout,
|
||||
smsIdFormat: options.smsIdFormat,
|
||||
sock,
|
||||
...(options.reconnect
|
||||
? {
|
||||
|
||||
+12
-4
@@ -1,8 +1,10 @@
|
||||
import type { MessageState } from './defs/constants.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { consts, constsById, messageTypeOf } from './defs/constants.ts';
|
||||
import { decodeMessage } from './message.ts';
|
||||
import { normaliseSmsId } from './sms-id.ts';
|
||||
import { paramNumber, paramText } from './defs/types.ts';
|
||||
|
||||
/**
|
||||
@@ -159,8 +161,14 @@ function receiptBody(pduObj: PduObject): string {
|
||||
).message;
|
||||
}
|
||||
|
||||
function receiptId(tlvId: ParamValue | undefined, receipt: Receipt | undefined): string | undefined {
|
||||
return nonEmptyText(tlvId) ?? nonEmptyText(receipt?.id);
|
||||
function receiptId(
|
||||
tlvId: ParamValue | undefined,
|
||||
receipt: Receipt | undefined,
|
||||
format: SmsIdFormat | undefined,
|
||||
): string | undefined {
|
||||
const id = nonEmptyText(tlvId) ?? nonEmptyText(receipt?.id);
|
||||
|
||||
return id === undefined ? undefined : normaliseSmsId(id, format);
|
||||
}
|
||||
|
||||
function isMessageState(name: string | undefined): name is MessageState {
|
||||
@@ -184,14 +192,14 @@ function receiptStatus(
|
||||
}
|
||||
|
||||
/** The delivery report a deliver_sm carries, or nothing when it carries a message instead. */
|
||||
export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
||||
export function dlrFromPdu(pduObj: PduObject, format?: SmsIdFormat): Dlr | undefined {
|
||||
const type = messageType(pduObj);
|
||||
|
||||
if (type === 'other') return undefined;
|
||||
|
||||
const body = receiptBody(pduObj);
|
||||
const receipt = body === '' ? undefined : parseReceipt(body);
|
||||
const smsId = receiptId(pduObj.tlvs.receipted_message_id?.tagValue, receipt);
|
||||
const smsId = receiptId(pduObj.tlvs.receipted_message_id?.tagValue, receipt, format);
|
||||
const { statusId, statusMsg } = receiptStatus(pduObj.tlvs.message_state?.tagValue, receipt);
|
||||
|
||||
if (type === 'unmarked' && (smsId === undefined || statusMsg === undefined)) return undefined;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OnRequest } from './session-options.ts';
|
||||
import type { PduObject } from './pdu.ts';
|
||||
import type { Session } from './session.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { Reassembler, decodeSegments } from './reassembly.ts';
|
||||
import { bindCommands, defaults } from './session-options.ts';
|
||||
import { concatInfo } from './udh.ts';
|
||||
@@ -18,6 +19,7 @@ export type IncomingRequestsOptions = {
|
||||
maxReassembly?: number | undefined;
|
||||
onRequest?: OnRequest | undefined;
|
||||
reassemblyTimeout?: number | undefined;
|
||||
receiptIdFormat?: SmsIdFormat | undefined;
|
||||
session: Session;
|
||||
systemId?: string | undefined;
|
||||
};
|
||||
@@ -28,6 +30,7 @@ export class IncomingRequests {
|
||||
private readonly log: SmppLog;
|
||||
private readonly onRequest: OnRequest | undefined;
|
||||
private readonly reassembler: Reassembler;
|
||||
private readonly receiptIdFormat: SmsIdFormat | undefined;
|
||||
private readonly session: Session;
|
||||
private readonly systemId: string;
|
||||
|
||||
@@ -41,6 +44,7 @@ export class IncomingRequests {
|
||||
maxOctets: options.maxOctets,
|
||||
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
|
||||
});
|
||||
this.receiptIdFormat = options.receiptIdFormat;
|
||||
this.session = options.session;
|
||||
this.systemId = options.systemId ?? defaults.systemId;
|
||||
}
|
||||
@@ -97,7 +101,7 @@ export class IncomingRequests {
|
||||
|
||||
/** 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);
|
||||
const dlr = dlrFromPdu(pduObj, this.receiptIdFormat);
|
||||
|
||||
if (!dlr) {
|
||||
this.onMessage(pduObj);
|
||||
|
||||
@@ -39,6 +39,7 @@ export type { SendRespOptions, Sms, SmsInput } from './sms.ts';
|
||||
export type { ConcatInfo } from './udh.ts';
|
||||
export type { Result, VoidResult } from './result.ts';
|
||||
export type { SmppLog } from './log.ts';
|
||||
export type { SmsIdFormat, SmsIdFormats } from './sms-id.ts';
|
||||
export type {
|
||||
AuthenticateInput,
|
||||
AuthenticateResult,
|
||||
|
||||
+9
-3
@@ -3,8 +3,10 @@ import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject, PduObjectInput } from './pdu.ts';
|
||||
import type { Result } from './result.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import type { SmsIdFormat } from './sms-id.ts';
|
||||
import { consts } from './defs/constants.ts';
|
||||
import { detect } from './defs/encodings.ts';
|
||||
import { normaliseSmsId } from './sms-id.ts';
|
||||
import { paramText } from './defs/types.ts';
|
||||
import { maxSegments, smppTime, splitMessage } from './message.ts';
|
||||
|
||||
@@ -32,6 +34,7 @@ export type SendSmsResult = { err?: Error; pduObjs: PduObject[]; smsIds: string[
|
||||
export type SendSmsDeps = {
|
||||
log: SmppLog;
|
||||
reference: number;
|
||||
respIdFormat?: SmsIdFormat | undefined;
|
||||
send: (input: PduObjectInput) => Promise<Result<{ pduObj: PduObject }>>;
|
||||
};
|
||||
|
||||
@@ -98,7 +101,10 @@ function checkSegments(allowed: number, segments: number): Error | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectSent(sent: Result<{ pduObj: PduObject }>[]): SendSmsResult {
|
||||
function collectSent(
|
||||
sent: Result<{ pduObj: PduObject }>[],
|
||||
format: SmsIdFormat | undefined,
|
||||
): SendSmsResult {
|
||||
const pduObjs: PduObject[] = [];
|
||||
const smsIds: string[] = [];
|
||||
let failure: Error | undefined;
|
||||
@@ -108,7 +114,7 @@ function collectSent(sent: Result<{ pduObj: PduObject }>[]): SendSmsResult {
|
||||
failure ??= one.err;
|
||||
} else if (one.pduObj.cmdStatus === 'ESME_ROK') {
|
||||
pduObjs.push(one.pduObj);
|
||||
smsIds.push(paramText(one.pduObj.params.message_id));
|
||||
smsIds.push(normaliseSmsId(paramText(one.pduObj.params.message_id), format));
|
||||
} else {
|
||||
const refusal = one.pduObj.cmdStatus ?? String(one.pduObj.cmdStatusId);
|
||||
|
||||
@@ -138,5 +144,5 @@ export async function submitSms(deps: SendSmsDeps, sms: SendSmsOptions): Promise
|
||||
params: submitSmParams(sms, segment, { encoding, multipart }),
|
||||
})));
|
||||
|
||||
return collectSent(sent);
|
||||
return collectSent(sent, deps.respIdFormat);
|
||||
}
|
||||
|
||||
+23
-2
@@ -4,8 +4,10 @@ import type { PduObject } from './pdu.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { Session } from './session.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import type { SmsIdFormats } from './sms-id.ts';
|
||||
import type { Sms } from './sms.ts';
|
||||
import type { Socket } from 'node:net';
|
||||
import { isSmsIdFormat } from './sms-id.ts';
|
||||
|
||||
export type SessionEvents = {
|
||||
close: [];
|
||||
@@ -83,6 +85,8 @@ export type SessionOptions = {
|
||||
responseTimeout?: number | undefined;
|
||||
/** How long a drain waits for the requests already on the wire. 0 waits forever. */
|
||||
shutdownTimeout?: number | undefined;
|
||||
/** The notation the peer writes message ids in, where it is not the one they are compared in. */
|
||||
smsIdFormat?: SmsIdFormats | undefined;
|
||||
sock: Socket;
|
||||
/** This end's own identity, answered to the peer in place of the one it sent. */
|
||||
systemId?: string | undefined;
|
||||
@@ -111,7 +115,7 @@ export const defaults = {
|
||||
* A count below 1 does not fail loudly anywhere downstream: `maxOutstanding: 0` leaves every send
|
||||
* queued behind a slot that is never freed, so the call never settles at all.
|
||||
*/
|
||||
export function checkSessionOptions(options: SessionCounts): VoidResult {
|
||||
export function checkSessionOptions(options: CheckableOptions): VoidResult {
|
||||
const limits: [string, number, number][] = [
|
||||
['idleTimeout', options.idleTimeout ?? 0, 0],
|
||||
['maxOutstanding', options.maxOutstanding ?? defaults.maxOutstanding, 1],
|
||||
@@ -127,14 +131,31 @@ export function checkSessionOptions(options: SessionCounts): VoidResult {
|
||||
}
|
||||
}
|
||||
|
||||
return checkSmsIdFormats(options.smsIdFormat);
|
||||
}
|
||||
|
||||
function checkSmsIdFormats(smsIdFormat: CheckableOptions['smsIdFormat']): VoidResult {
|
||||
const formats: [string, string | undefined][] = [
|
||||
['receipt', smsIdFormat?.receipt],
|
||||
['submitResp', smsIdFormat?.submitResp],
|
||||
];
|
||||
|
||||
for (const [place, format] of formats) {
|
||||
if (format !== undefined && !isSmsIdFormat(format)) {
|
||||
return { err: new Error(`smsIdFormat.${place} must be decimal or hex, got ${format}`) };
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export type SessionCounts = {
|
||||
/** What the checker reads, as it arrives: a caller without types can put anything in it. */
|
||||
export type CheckableOptions = {
|
||||
idleTimeout?: number | undefined;
|
||||
maxOutstanding?: number | undefined;
|
||||
maxReassembly?: number | undefined;
|
||||
reassemblyTimeout?: number | undefined;
|
||||
responseTimeout?: number | undefined;
|
||||
shutdownTimeout?: number | undefined;
|
||||
smsIdFormat?: { receipt?: string | undefined; submitResp?: string | undefined } | undefined;
|
||||
};
|
||||
|
||||
@@ -120,6 +120,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
maxReassembly: options.maxReassembly,
|
||||
onRequest: options.onRequest,
|
||||
reassemblyTimeout: options.reassemblyTimeout,
|
||||
receiptIdFormat: options.smsIdFormat?.receipt,
|
||||
session: this,
|
||||
systemId: options.systemId,
|
||||
});
|
||||
@@ -209,6 +210,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
const sent = await submitSms({
|
||||
log: this.log,
|
||||
reference: this.nextConcatReference(),
|
||||
respIdFormat: this.options.smsIdFormat?.submitResp,
|
||||
send: input => this.send(input, options),
|
||||
}, sms);
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/** The notation a peer writes message ids in. */
|
||||
export type SmsIdFormat = 'decimal' | 'hex';
|
||||
|
||||
/** The notation per place the peer writes an id. An omitted place is left as it arrived. */
|
||||
export type SmsIdFormats = {
|
||||
receipt?: SmsIdFormat | undefined;
|
||||
submitResp?: SmsIdFormat | undefined;
|
||||
};
|
||||
|
||||
export function isSmsIdFormat(value: unknown): value is SmsIdFormat {
|
||||
return value === 'decimal' || value === 'hex';
|
||||
}
|
||||
|
||||
// SMPP 3.4 caps message_id at 64 octets, and BigInt on a longer string is a peer-controlled cost.
|
||||
const maxIdLength = 64;
|
||||
|
||||
const notations = {
|
||||
decimal: { digits: /^[0-9]+$/, prefix: '' },
|
||||
hex: { digits: /^[0-9a-f]+$/i, prefix: '0x' },
|
||||
};
|
||||
|
||||
/**
|
||||
* The id as a plain decimal value, so an SMSC that answers a submit in one notation and writes the
|
||||
* receipt in another still correlates. An id the notation cannot read is left as it arrived.
|
||||
*/
|
||||
export function normaliseSmsId(id: string, format: SmsIdFormat | undefined): string {
|
||||
if (format === undefined || id.length > maxIdLength) return id;
|
||||
|
||||
const { digits, prefix } = notations[format];
|
||||
|
||||
return digits.test(id) ? BigInt(`${prefix}${id}`).toString(10) : id;
|
||||
}
|
||||
Reference in New Issue
Block a user