Refuse a bind bound() cannot record, and document it for a 0.4.0 consumer
Mirror / push (push) Successful in 6s
Test / lint (pull_request) Successful in 23s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 45s
Test / test (22) (pull_request) Successful in 32s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 32s

This commit is contained in:
2026-09-28 04:01:51 +02:00
parent 3cc38325cc
commit 2ac72fb3f4
8 changed files with 51 additions and 19 deletions
+4 -1
View File
@@ -174,7 +174,10 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
return { err: new Error(`Remote host refused login: ${sent.pduObj.cmdStatus ?? 'unknown'}`) };
}
session.bound(bindType, sent.pduObj.tlvs.sc_interface_version?.tagValue);
const recorded = session.bound(bindType, sent.pduObj.tlvs.sc_interface_version?.tagValue);
if (recorded.err) return recorded;
session.log.info('client - bound', { bindType, systemId });
return {};
+2 -2
View File
@@ -177,9 +177,9 @@ async function acceptBind(
options: ServerOptions,
identity: Record<string, string>,
): Promise<void> {
session.bound(bindType, pduObj.params.interface_version);
const recorded = session.bound(bindType, pduObj.params.interface_version);
await session.sendReturn(pduObj, 'ESME_ROK', identity, bindRespTlvs(session, options));
await session.sendReturn(pduObj, recorded.err ? 'ESME_RBINDFAIL' : 'ESME_ROK', identity, bindRespTlvs(session, options));
}
async function onBind(
+23
View File
@@ -121,6 +121,29 @@ 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 type SessionBind = { as: BindType; peerVersion: number };
const bindTypes: readonly string[] = ['receiver', 'transceiver', 'transmitter'];
function isBindType(value: unknown): value is BindType {
return typeof value === 'string' && bindTypes.includes(value);
}
/** A bind as `Session.bound()` records it: undefined declares no version, which is pre-3.4. */
export function checkedBind(bindType: unknown, declaredVersion: unknown): Result<{ bind: SessionBind }> {
if (!isBindType(bindType)) {
return { err: new Error(`bindType must be ${bindTypes.join(', ')}, got ${namedValue(bindType)}`) };
}
if (declaredVersion === undefined) return { bind: { as: bindType, peerVersion: undeclaredInterfaceVersion } };
if (typeof declaredVersion !== 'number' || !Number.isInteger(declaredVersion) || declaredVersion < 0 || declaredVersion > 0xFF) {
return { err: new Error(`declaredVersion must be 0-255, or undefined where the peer declared none, got ${namedValue(declaredVersion)}`) };
}
return { bind: { as: bindType, peerVersion: declaredVersion } };
}
export const defaults = {
/** Receipts of a multipart message can be a working day apart, so the cap does the bounding. */
dlrMergeTimeout: 86_400_000,
+10 -9
View File
@@ -4,7 +4,7 @@ import type { MessageDlr } from './dlr-merger.ts';
import type { ParamValue } from './defs/types.ts';
import type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts';
import type { PduRefusedError } from './pdu-refusal.ts';
import type { BindType, CloseOptions, LinkEnd, OnRequest, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
import type { BindType, CloseOptions, LinkEnd, OnRequest, ReconnectOptions, SendOptions, SessionBind, 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';
@@ -19,7 +19,7 @@ import { ReconnectLoop } from './reconnect-loop.ts';
import { leftOf } from './idle-waiters.ts';
import { errorFrom } from './error-from.ts';
import { optionalParamsMinVersion } from './defs/constants.ts';
import { bindCarries, bindCommands, defaultSystemId, defaults, undeclaredInterfaceVersion } from './session-options.ts';
import { bindCarries, bindCommands, checkedBind, defaultSystemId, defaults } from './session-options.ts';
import { isResp, objToPdu, pduReturn } from './pdu.ts';
import { refusalAnswer } from './pdu-refusal.ts';
import { guardedLog } from './log.ts';
@@ -58,7 +58,7 @@ export class Session extends EventEmitter<SessionEvents> {
linkEnd: LinkEnd = 'esme';
userData: unknown = undefined;
private bind: { as: BindType; peerVersion: number } | undefined = undefined;
private bind: SessionBind | undefined = undefined;
private readonly concatReference = new ConcatReference();
private readonly dlrMerger: DlrMerger;
@@ -161,11 +161,13 @@ export class Session extends EventEmitter<SessionEvents> {
return this.bind?.peerVersion;
}
/** Records a bind this link accepted or had accepted, until the next one. A version that is not a number is none. */
bound(bindType: BindType, declaredVersion: unknown): void {
const peerVersion = typeof declaredVersion === 'number' ? declaredVersion : undeclaredInterfaceVersion;
/** Records a bind this link accepted or had accepted, until the next one. */
bound(bindType: BindType, declaredVersion: unknown): VoidResult {
const checked = checkedBind(bindType, declaredVersion);
this.bind = { as: bindType, peerVersion };
if (!checked.err) this.bind = checked.bind;
return checked.err ? { err: checked.err } : {};
}
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
@@ -175,8 +177,7 @@ export class Session extends EventEmitter<SessionEvents> {
/** SMPP 3.4 forbids sending optional parameters to a peer that declared an older version. */
acceptsOptionalParams(): boolean {
return this.peerInterfaceVersion === undefined
|| this.peerInterfaceVersion >= optionalParamsMinVersion;
return this.peerInterfaceVersion === undefined || this.peerInterfaceVersion >= optionalParamsMinVersion;
}
/** Sends a request and resolves with the peer's response. */