Enforce bind direction, cap segments per message and pin the README examples
This commit is contained in:
+4
-2
@@ -1,7 +1,10 @@
|
||||
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 { Socket } from 'node:net';
|
||||
export type { BindType };
|
||||
|
||||
import { Session } from './session.ts';
|
||||
import { checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { connect as netConnect } from 'node:net';
|
||||
@@ -9,8 +12,6 @@ import { connect as tlsConnect } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
import { silentLog } from './log.ts';
|
||||
|
||||
export type BindType = 'receiver' | 'transceiver' | 'transmitter';
|
||||
|
||||
export type ClientOptions = {
|
||||
addressRange?: string;
|
||||
addrNpi?: number;
|
||||
@@ -125,6 +126,7 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
|
||||
|
||||
const declared = sent.pduObj.tlvs.sc_interface_version?.tagValue;
|
||||
|
||||
session.boundAs = bindType;
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
|
||||
@@ -48,6 +48,16 @@ export class IncomingRequests {
|
||||
async handle(pduObj: PduObject): Promise<void> {
|
||||
if (this.onRequest && await this.onRequest(this.session, pduObj)) return;
|
||||
|
||||
if (!this.session.bindAllows(pduObj.cmdName)) {
|
||||
this.log.info('session - command the peer\'s bind direction does not carry', {
|
||||
bindType: this.session.boundAs ?? '',
|
||||
cmdName: pduObj.cmdName,
|
||||
});
|
||||
await this.session.sendReturn(pduObj, 'ESME_RINVBNDSTS');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pduObj.cmdName) {
|
||||
case 'deliver_sm':
|
||||
await this.onDeliverSm(pduObj);
|
||||
|
||||
+40
-21
@@ -15,6 +15,8 @@ export type SendSmsOptions = {
|
||||
encoding?: EncodingName;
|
||||
flash?: boolean;
|
||||
from: string;
|
||||
/** Refuse before sending anything if the message needs more than this many segments. */
|
||||
maxSegments?: number;
|
||||
message: string;
|
||||
scheduleDeliveryTime?: Date | number | string;
|
||||
sourceAddrNpi?: number;
|
||||
@@ -79,31 +81,26 @@ export function submitSmParams(
|
||||
}
|
||||
|
||||
/** Puts a message on the wire as one submit_sm per segment. */
|
||||
export async function submitSms(deps: SendSmsDeps, sms: SendSmsOptions): Promise<SendSmsResult> {
|
||||
const encoding = sms.encoding ?? detect(sms.message);
|
||||
const segments = splitMessage(sms.message, { encoding, reference: deps.reference });
|
||||
|
||||
if (segments.length === 0) {
|
||||
return {
|
||||
err: new Error(`Message needs more than ${String(maxSegments)} segments, the concatenation limit`),
|
||||
pduObjs: [],
|
||||
smsIds: [],
|
||||
};
|
||||
/** Nothing goes on the wire until the whole message fits: a half-sent message bills twice. */
|
||||
function checkSegments(allowed: number, segments: number): Error | undefined {
|
||||
if (!Number.isInteger(allowed) || allowed < 1 || allowed > maxSegments) {
|
||||
return new Error(`maxSegments must be between 1 and ${String(maxSegments)}, got ${String(allowed)}`);
|
||||
}
|
||||
|
||||
const multipart = segments.length > 1;
|
||||
if (segments === 0) {
|
||||
return new Error(`Message needs more than ${String(maxSegments)} segments, the concatenation limit`);
|
||||
}
|
||||
|
||||
if (segments > allowed) {
|
||||
return new Error(`Message needs ${String(segments)} segments, more than the ${String(allowed)} allowed`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectSent(sent: Result<{ pduObj: PduObject }>[]): SendSmsResult {
|
||||
const pduObjs: PduObject[] = [];
|
||||
const smsIds: string[] = [];
|
||||
|
||||
deps.log.debug('sendSms() - sending', { encoding, segments: segments.length, to: sms.to });
|
||||
|
||||
// Segments go out together rather than one-after-a-response: a receiver that waits for every
|
||||
// segment before answering — this library's own server does — would otherwise deadlock.
|
||||
const sent = await Promise.all(segments.map(segment => deps.send({
|
||||
cmdName: 'submit_sm',
|
||||
params: submitSmParams(sms, segment, { encoding, multipart }),
|
||||
})));
|
||||
|
||||
let failure: Error | undefined;
|
||||
|
||||
for (const one of sent) {
|
||||
@@ -121,3 +118,25 @@ export async function submitSms(deps: SendSmsDeps, sms: SendSmsOptions): Promise
|
||||
|
||||
return failure ? { err: failure, pduObjs, smsIds } : { pduObjs, smsIds };
|
||||
}
|
||||
|
||||
export async function submitSms(deps: SendSmsDeps, sms: SendSmsOptions): Promise<SendSmsResult> {
|
||||
const allowed = sms.maxSegments ?? maxSegments;
|
||||
const encoding = sms.encoding ?? detect(sms.message);
|
||||
const segments = splitMessage(sms.message, { encoding, reference: deps.reference });
|
||||
const refused = checkSegments(allowed, segments.length);
|
||||
|
||||
if (refused) return { err: refused, pduObjs: [], smsIds: [] };
|
||||
|
||||
const multipart = segments.length > 1;
|
||||
|
||||
deps.log.debug('sendSms() - sending', { encoding, segments: segments.length, to: sms.to });
|
||||
|
||||
// Segments go out together rather than one-after-a-response: a receiver that waits for every
|
||||
// segment before answering — this library's own server does — would otherwise deadlock.
|
||||
const sent = await Promise.all(segments.map(segment => deps.send({
|
||||
cmdName: 'submit_sm',
|
||||
params: submitSmParams(sms, segment, { encoding, multipart }),
|
||||
})));
|
||||
|
||||
return collectSent(sent);
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@ import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
||||
import { checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { bindTypeFromCommand, 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';
|
||||
@@ -147,6 +147,7 @@ async function acceptBind(
|
||||
): Promise<void> {
|
||||
const declared = pduObj.params.interface_version;
|
||||
|
||||
session.boundAs = bindTypeFromCommand(pduObj.cmdName);
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
|
||||
@@ -25,6 +25,28 @@ export const bindCommands: readonly string[] = [
|
||||
'bind_transmitter',
|
||||
];
|
||||
|
||||
export type BindType = 'receiver' | 'transceiver' | 'transmitter';
|
||||
|
||||
export function bindTypeFromCommand(cmdName: string): BindType | undefined {
|
||||
if (cmdName === 'bind_receiver') return 'receiver';
|
||||
if (cmdName === 'bind_transceiver') return 'transceiver';
|
||||
if (cmdName === 'bind_transmitter') return 'transmitter';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type SendOptions = { signal?: AbortSignal | undefined };
|
||||
|
||||
/**
|
||||
|
||||
+14
-2
@@ -2,7 +2,7 @@ import type { ErrorName } from './defs/errors.ts';
|
||||
import type { MessageDlr } from './dlr-merger.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject, PduObjectInput, TlvInput } from './pdu.ts';
|
||||
import type { ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { BindType, 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';
|
||||
@@ -16,7 +16,7 @@ import { PendingRequests } from './pending-requests.ts';
|
||||
import { ReconnectLoop } from './reconnect-loop.ts';
|
||||
import { SendWindow } from './send-window.ts';
|
||||
import { optionalParamsMinVersion } from './defs/constants.ts';
|
||||
import { bindCommands, defaultSystemId, defaults } from './session-options.ts';
|
||||
import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts';
|
||||
import { isResp, objToPdu, pduReturn, pduToObj } from './pdu.ts';
|
||||
import { silentLog } from './log.ts';
|
||||
import { submitSms } from './send-sms.ts';
|
||||
@@ -30,6 +30,7 @@ export type {
|
||||
SessionEvents,
|
||||
SessionOptions,
|
||||
};
|
||||
export type { BindType };
|
||||
export { bindCommands, defaultSystemId };
|
||||
|
||||
export class Session extends EventEmitter<SessionEvents> {
|
||||
@@ -37,6 +38,8 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
sock: Socket;
|
||||
readonly log: SmppLog;
|
||||
|
||||
/** The role the ESME bound with, whichever end of the link this is. Undefined before any bind. */
|
||||
boundAs: BindType | undefined = undefined;
|
||||
loggedIn = false;
|
||||
/** What the peer declared when binding: 0x00 if it declared none, undefined before any bind. */
|
||||
peerInterfaceVersion: number | undefined = undefined;
|
||||
@@ -110,6 +113,11 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
this.resetTimers();
|
||||
}
|
||||
|
||||
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
|
||||
bindAllows(cmdName: string): boolean {
|
||||
return bindCarries(this.boundAs, cmdName);
|
||||
}
|
||||
|
||||
/** SMPP 3.4 forbids sending optional parameters to a peer that declared an older version. */
|
||||
acceptsOptionalParams(): boolean {
|
||||
return this.peerInterfaceVersion === undefined
|
||||
@@ -165,6 +173,10 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
}
|
||||
|
||||
async sendSms(sms: SendSmsOptions, options: SendOptions = {}): Promise<SendSmsResult> {
|
||||
if (!this.bindAllows('submit_sm')) {
|
||||
return { err: new Error('A receiver-bound session does not carry submit_sm'), pduObjs: [], smsIds: [] };
|
||||
}
|
||||
|
||||
const sent = await submitSms({
|
||||
log: this.log,
|
||||
reference: this.nextConcatReference(),
|
||||
|
||||
@@ -126,6 +126,10 @@ async function sendDlr(
|
||||
sms: Sms,
|
||||
status: MessageState = 'DELIVERED',
|
||||
): Promise<Result<{ pduObjs: PduObject[] }>> {
|
||||
if (!sms.session.bindAllows('deliver_sm')) {
|
||||
return { err: new Error('A transmitter-bound session does not carry deliver_sm') };
|
||||
}
|
||||
|
||||
const total = sms.pduObjs.length;
|
||||
const pduObjs: PduObject[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user