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:
+38
-14
@@ -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 });
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export type CloseOptions = { signal?: AbortSignal | undefined };
|
||||
* built-in handling is skipped — this is how the server owns bind without the session also
|
||||
* replying "invalid command".
|
||||
*/
|
||||
export type OnRequest = (session: Session, pduObj: PduObject) => Promise<boolean>;
|
||||
export type OnRequest = (session: Session, pduObj: PduObject) => Promise<boolean> | boolean;
|
||||
|
||||
/**
|
||||
* How to come back after an unexpected disconnect. The session owns the retry loop; the caller
|
||||
|
||||
+5
-1
@@ -397,7 +397,11 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
void this.incoming.handle(pduObj).catch((thrown: unknown) => {
|
||||
const err = errorFrom(thrown);
|
||||
|
||||
this.log.error('session - a handler threw', { message: err.message });
|
||||
this.log.error('session - a handler threw', {
|
||||
cmdName: pduObj.cmdName,
|
||||
message: err.message,
|
||||
seqNr: pduObj.seqNr,
|
||||
});
|
||||
this.emit('sessionError', err);
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ function answeredOnArrival(
|
||||
|
||||
if (options.status !== undefined && options.status !== 'ESME_ROK') {
|
||||
return Promise.resolve({
|
||||
err: new Error('Its segments were answered as they arrived, so there is nothing left to refuse; refuse a segment from onRequest instead'),
|
||||
err: new Error('Its segments were answered as they arrived, so there is nothing left to refuse; refuse a segment from the onRequest option instead'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user