import type { LogInt } from '@larvit/log'; import type { PduObject } from './pdu.ts'; import type { Result } from './result.ts'; import type { Server as NetServer, Socket } from 'node:net'; import type { TlsOptions } from 'node:tls'; import { EventEmitter } from 'node:events'; import { Session } from './session.ts'; import { createServer as createNetServer } from 'node:net'; import { createServer as createTlsServer } from 'node:tls'; import { paramText } from './defs/types.ts'; import { silentLog } from './log.ts'; export type AuthenticateResult = { userData?: unknown } | boolean; export type AuthenticateInput = { password: string; session: Session; systemId: string; systemType: string; }; export type ServerOptions = { authenticate?: (input: AuthenticateInput) => Promise | AuthenticateResult; host?: string; idleTimeout?: number; log?: LogInt; maxOutstanding?: number; maxReassembly?: number; port?: number; reassemblyTimeout?: number; responseTimeout?: number; signal?: AbortSignal; tls?: TlsOptions | boolean; }; export type ServerEvents = { serverError: [Error]; session: [Session]; }; const defaults = { idleTimeout: 40_000, port: 2775, }; const bindCommands = ['bind_receiver', 'bind_transceiver', 'bind_transmitter']; /** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */ export class SmppServer extends EventEmitter { readonly sessions = new Set(); private readonly server: NetServer; constructor(server: NetServer) { super(); this.server = server; } /** The port actually bound, which matters when 0 was requested. */ get port(): number { const address = this.server.address(); return typeof address === 'object' && address !== null ? address.port : 0; } /** Stops listening and closes every live session. */ close(): Promise { return new Promise(resolve => { for (const session of this.sessions) { session.close(); } this.sessions.clear(); this.server.close(() => { resolve(); }); }); } } async function authenticate( session: Session, pduObj: PduObject, options: ServerOptions, ): Promise { if (!options.authenticate) return true; const params = pduObj.params; const result = await options.authenticate({ password: typeof params.password === 'string' ? params.password : '', session, systemId: typeof params.system_id === 'string' ? params.system_id : '', systemType: typeof params.system_type === 'string' ? params.system_type : '', }); if (result === false) return false; if (result !== true && typeof result === 'object') { session.userData = result.userData; } return true; } /** * Handles everything a peer may send before it is bound. Returns true when it has answered, so the * session leaves the PDU alone. */ async function onRequest( session: Session, pduObj: PduObject, options: ServerOptions, ): Promise { const log = options.log ?? silentLog; if (session.loggedIn || pduObj.cmdName === 'unbind') return false; if (!bindCommands.includes(pduObj.cmdName)) { log.debug('server - command before bind', { cmdName: pduObj.cmdName }); await session.sendReturn(pduObj, 'ESME_RINVBNDSTS'); return true; } if (!await authenticate(session, pduObj, options)) { log.info('server - bind refused', { systemId: paramText(pduObj.params.system_id) }); await session.sendReturn(pduObj, 'ESME_RBINDFAIL'); return true; } session.loggedIn = true; await session.sendReturn(pduObj); log.verbose('server - bound', { systemId: paramText(pduObj.params.system_id) }); return true; } function onConnection(sock: Socket, options: ServerOptions, server: SmppServer): void { const log = options.log ?? silentLog; const session = new Session({ idleTimeout: options.idleTimeout ?? defaults.idleTimeout, log, maxOutstanding: options.maxOutstanding, maxReassembly: options.maxReassembly, onRequest: (bound, pduObj) => onRequest(bound, pduObj, options), reassemblyTimeout: options.reassemblyTimeout, responseTimeout: options.responseTimeout, sock, }); server.sessions.add(session); session.on('close', () => server.sessions.delete(session)); log.verbose('server - incoming connection', { remoteAddress: sock.remoteAddress ?? '', remotePort: sock.remotePort ?? 0, }); server.emit('session', session); } /** Starts listening for SMPP connections. Resolves once the socket is bound. */ export function server(options: ServerOptions = {}): Promise> { return new Promise(resolve => { const log = options.log ?? silentLog; const port = options.port ?? defaults.port; const useTls = options.tls !== undefined && options.tls !== false; const listener = useTls ? createTlsServer(typeof options.tls === 'object' ? options.tls : {}) : createNetServer(); const smpp = new SmppServer(listener); listener.on(useTls ? 'secureConnection' : 'connection', (sock: Socket) => { onConnection(sock, options, smpp); }); const onStartupError = (err: Error): void => { listener.removeListener('error', onStartupError); log.warn('server - could not listen', { message: err.message, port }); resolve({ err }); }; listener.once('error', onStartupError); listener.listen(port, options.host, () => { listener.removeListener('error', onStartupError); // Past startup, a listener error is a runtime event, not a failed start. listener.on('error', (err: Error) => { log.warn('server - error', { message: err.message }); smpp.emit('serverError', err); }); log.info('server - listening', { host: options.host ?? '*', port: smpp.port }); if (options.signal) { options.signal.addEventListener('abort', () => { void smpp.close(); }, { once: true }); } resolve({ server: smpp }); }); }); }