Give server() consumers the onRequest hook the docs promise (#86)

* Regression tests for an onRequest hook on server()

* Compose the application's onRequest hook into server() past bind

* Document the server's onRequest hook and record the composition decision

* Pin that a re-bind and a pre-bind unbind never reach the hook, and that a non-function hook is refused at startup

* Keep every bind and all pre-bind traffic out of the application's hook

* Make the hook's published promises exact, and record the src layout deferral

* Trim the record to what the tests do not already forbid

* Pin that nothing answers a request whose hook failed

* Answer nothing for a request whose hook failed, on both surfaces alike

* State the hook's failure policy as it now behaves

* Name the request a failed handler left unanswered, and say what a wedged hook reports
This commit is contained in:
2026-09-06 17:20:45 +02:00
committed by GitHub
parent 0d1da1dc87
commit bcbb042c1b
8 changed files with 375 additions and 100 deletions
+38 -14
View File
@@ -1,4 +1,4 @@
import type { CloseOptions } from './session-options.ts';
import type { CloseOptions, OnRequest } from './session-options.ts';
import type { PduObject, TlvInput } from './pdu.ts';
import type { Result, VoidResult } from './result.ts';
import type { Server as NetServer, Socket } from 'node:net';
@@ -32,6 +32,8 @@ export type ServerOptions = {
maxOutstanding?: number;
maxOctets?: number;
maxReassembly?: number;
/** First refusal on every request a bound peer sends. */
onRequest?: OnRequest;
port?: number;
reassemblyTimeout?: number;
responseTimeout?: number;
@@ -185,42 +187,47 @@ async function acceptBind(
}
async function onBind(session: Session, pduObj: PduObject, options: ServerOptions): Promise<void> {
const log = guardedLog(options.log);
const identity = { system_id: options.systemId ?? defaults.systemId };
const systemId = paramText(pduObj.params.system_id);
if (!await authenticate(session, pduObj, options)) {
log.info('server - bind refused', { systemId });
session.log.info('server - bind refused', { systemId });
await session.sendReturn(pduObj, 'ESME_RBINDFAIL', identity);
return;
}
await acceptBind(session, pduObj, options, identity);
log.verbose('server - bound', { systemId });
session.log.verbose('server - bound', { systemId });
}
/**
* Handles everything a peer may send before it is bound. Returns true when it has answered, so the
* session leaves the PDU alone.
* Bind is the server's; a bound peer's other requests are offered to the application. Returns true
* when it has answered, so the session leaves the PDU alone.
*/
async function onRequest(
async function handleRequest(
session: Session,
pduObj: PduObject,
options: ServerOptions,
): Promise<boolean> {
if (session.loggedIn || pduObj.cmdName === 'unbind') return false;
const isBind = bindCommands.includes(pduObj.cmdName);
if (!bindCommands.includes(pduObj.cmdName)) {
const log = guardedLog(options.log);
if (session.loggedIn) {
if (isBind || !options.onRequest) return false;
log.debug('server - command before bind', { cmdName: pduObj.cmdName });
await session.sendReturn(pduObj, 'ESME_RINVBNDSTS');
return options.onRequest(session, pduObj);
}
if (isBind) {
await onBind(session, pduObj, options);
return true;
}
await onBind(session, pduObj, options);
if (pduObj.cmdName === 'unbind') return false;
session.log.debug('server - command before bind', { cmdName: pduObj.cmdName });
await session.sendReturn(pduObj, 'ESME_RINVBNDSTS');
return true;
}
@@ -233,7 +240,7 @@ function onConnection(sock: Socket, options: ServerOptions, server: SmppServer):
maxOutstanding: options.maxOutstanding,
maxOctets: options.maxOctets,
maxReassembly: options.maxReassembly,
onRequest: (bound, pduObj) => onRequest(bound, pduObj, options),
onRequest: (bound, pduObj) => handleRequest(bound, pduObj, options),
reassemblyTimeout: options.reassemblyTimeout,
responseTimeout: options.responseTimeout,
shutdownTimeout: options.shutdownTimeout,
@@ -264,11 +271,28 @@ function createSecureListener(tlsOptions: TlsOptions, log: SmppLog): TlsServer {
return listener;
}
/** A caller without types would otherwise reach a TypeError once per PDU rather than once here. */
function checkHooks(options: ServerOptions): VoidResult {
for (const name of ['authenticate', 'onRequest'] as const) {
const hook: unknown = options[name];
if (hook !== undefined && typeof hook !== 'function') {
return { err: new Error(`${name} must be a function, got ${typeof hook}`) };
}
}
return {};
}
function checkOptions(options: ServerOptions, log: SmppLog, port: number): VoidResult {
const checked = checkSessionOptions(options);
if (checked.err) return { err: checked.err };
const hooks = checkHooks(options);
if (hooks.err) return hooks;
if (options.tls === true) {
log.warn('server - tls without a certificate', { port });