diff --git a/CHANGELOG.md b/CHANGELOG.md index 208bcaf..c77c870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,8 +76,8 @@ - `cmds.broadcast_sm_resp.tlvMap` is removed; nothing read it. - `session.boundAs` and `session.peerInterfaceVersion` are read-only, and `session.loggedIn` is removed: read `session.boundAs !== undefined`. A session you wire yourself records the bind it - accepted or had accepted with `session.bound(bindType, declaredVersion)`; an assignment to either - field now throws a `TypeError`. + accepted or had accepted with `session.bound(bindType, declaredVersion)`, which returns `err` for a + bind type or version it cannot record; an assignment to either field now throws a `TypeError`. ## 0.5.0 diff --git a/MIGRATION.md b/MIGRATION.md index 2759f16..a3a4579 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -36,6 +36,9 @@ shape is the same, connect, send, listen for delivery reports, with callbacks re Write `alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back under, for `alert_on_msg_delivery` and `failed_broadcast_area_identifier`, which are gone from `tlvs` too. +- **`session.loggedIn` is gone**: read `session.boundAs !== undefined`. `boundAs` and + `peerInterfaceVersion` are read-only; a session you construct yourself records a bind with + `session.bound()`. - **The `error` event is `sessionError`**, and `serverError` on the server handle. - **`log`** takes any object with `debug`, `error`, `info`, `verbose` and `warn` methods instead of a `larvitutils` one, and is silent by default: [README](README.md#logging). diff --git a/README.md b/README.md index 4eaed8a..f0af027 100644 --- a/README.md +++ b/README.md @@ -423,10 +423,11 @@ const { err, pduObj } = await session.send({ a `send()` you build is passed through as written, so check it yourself. - `peerInterfaceVersion`: the version the peer declared, `0x00` if none, `undefined` before any bind. - `bindAllows(cmdName)` and `boundAs`: what the bind direction carries: [Bind direction](#bind-direction). -- Both hold through a reconnect's gap: the loop binds again as before. +- `boundAs` and `peerInterfaceVersion` are read-only, and hold through a reconnect's gap until the + next `bound()`. - `bound(bindType, declaredVersion)`: how a session you construct yourself records a bind, whichever - end accepted it. A `declaredVersion` that is not a number is none declared. Both fields are - read-only. + end accepted it, on every link it binds. `declaredVersion` is 0-255, or `undefined` where the peer + declared none; anything else, or an unknown `bindType`, returns `err` and records nothing. ## Receiving in depth @@ -544,7 +545,8 @@ if (err) throw err; the link. Guard on the command name, as above, and a failing hook costs only its own request. - A `Session` you construct yourself takes the same hook as a session option, and that is where a peer's bind gets accepted, since a hand-wired session has no bind handling of its own: call - `session.bound(bindType, pduObj.params.interface_version)` before answering it. + `session.bound(bindType, pduObj.params.interface_version)` before answering it, and refuse the bind + with `ESME_RBINDFAIL` where that returns `err`. **`sendDlr()`** takes `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`, `ACCEPTED`, `UNKNOWN`, `REJECTED` or `SKIPPED`. The first two go out as intermediate delivery diff --git a/src/client.ts b/src/client.ts index 8f727eb..6b7299c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -174,7 +174,10 @@ async function bind(session: Session, options: ClientOptions): Promise, ): Promise { - 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( diff --git a/src/session-options.ts b/src/session-options.ts index 0e2ad3b..dfb3325 100644 --- a/src/session-options.ts +++ b/src/session-options.ts @@ -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, diff --git a/src/session.ts b/src/session.ts index 64c90a2..e92b7d4 100644 --- a/src/session.ts +++ b/src/session.ts @@ -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 { 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 { 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 { /** 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. */ diff --git a/test/session.test.ts b/test/session.test.ts index 30a1e4e..1c8a5c8 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -465,7 +465,7 @@ describe('bind', () => { // The mistakes an untyped caller makes: the TLV object for its value, a version past int8, a typo. for (const [bindType, declared] of [['receiver', { tagValue: 0x34 }], ['receiver', 0x100], ['receiver', 3.4], ['tx', 0x34]]) { - const refused: unknown = Reflect.apply(session.bound, session, [bindType, declared]); + const refused: unknown = Reflect.apply(Reflect.get(session, 'bound'), session, [bindType, declared]); assert.ok(refused && typeof refused === 'object' && 'err' in refused && refused.err instanceof Error); }