Decompose the codec, document the session surface and record the review decisions
This commit is contained in:
+3
-2
@@ -120,6 +120,7 @@ export function parseReceipt(message: string): Receipt {
|
||||
export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
||||
const message = pduObj.params.short_message;
|
||||
const receipt = typeof message === 'string' ? parseReceipt(message) : undefined;
|
||||
const receiptState = receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
||||
|
||||
const tlvState = pduObj.tlvs.message_state?.tagValue;
|
||||
const tlvId = pduObj.tlvs.receipted_message_id?.tagValue;
|
||||
@@ -132,13 +133,13 @@ export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
||||
|
||||
const statusMsg = typeof tlvState === 'number'
|
||||
? constsById.MESSAGE_STATE?.[tlvState]
|
||||
: receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
||||
: receiptState;
|
||||
|
||||
if (statusMsg === undefined) return undefined;
|
||||
|
||||
const statusId = typeof tlvState === 'number'
|
||||
? tlvState
|
||||
: consts.MESSAGE_STATE[receiptStates[receipt?.stat?.toUpperCase() ?? ''] ?? 'UNKNOWN'];
|
||||
: consts.MESSAGE_STATE[receiptState ?? 'UNKNOWN'];
|
||||
|
||||
return {
|
||||
doneDate: receiptDate(receipt?.doneDate),
|
||||
|
||||
+120
-68
@@ -1,7 +1,7 @@
|
||||
import type { CommandName, PduParams, PduParamsInput } from './defs/commands.ts';
|
||||
import type { CommandDefinition, CommandName, PduParams, PduParamsInput } from './defs/commands.ts';
|
||||
import type { ErrorName } from './defs/errors.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { Result } from './result.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { Tlv } from './defs/tlvs.ts';
|
||||
import { cmds, commandNameById, isCommandName } from './defs/commands.ts';
|
||||
import { consts } from './defs/constants.ts';
|
||||
@@ -79,46 +79,31 @@ function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> {
|
||||
return { tagId };
|
||||
}
|
||||
|
||||
function buildPdu(
|
||||
cmdName: CommandName,
|
||||
cmdStatus: ErrorName,
|
||||
seqNr: number,
|
||||
/** Encoding a string short_message also settles data_coding and sm_length. */
|
||||
function resolveShortMessage(
|
||||
params: Record<string, ParamValue | undefined>,
|
||||
tlvs: Record<string, TlvInput> | undefined,
|
||||
): Result<{ buffer: Buffer }> {
|
||||
const definition = cmds[cmdName];
|
||||
): Record<string, ParamValue | undefined> {
|
||||
const message = params.short_message;
|
||||
|
||||
if (!definition) {
|
||||
return { err: new Error(`Invalid cmdName: ${JSON.stringify(cmdName)}`) };
|
||||
}
|
||||
if (typeof message !== 'string') return { ...params };
|
||||
|
||||
if (!isErrorName(cmdStatus)) {
|
||||
return { err: new Error(`Invalid cmdStatus: ${JSON.stringify(cmdStatus)}`) };
|
||||
}
|
||||
const dataCoding = params.data_coding;
|
||||
const encoding = typeof dataCoding === 'number' ? encodingByDataCoding(dataCoding) : detect(message);
|
||||
const encoded = encodeMessage(message, encoding);
|
||||
|
||||
if (!Number.isInteger(seqNr) || seqNr < 0 || seqNr > maxSeqNr) {
|
||||
return { err: new Error(`Invalid seqNr: ${JSON.stringify(seqNr)}`) };
|
||||
}
|
||||
|
||||
const resolved = { ...params };
|
||||
const message = resolved.short_message;
|
||||
|
||||
// A string short_message is encoded here, which also settles data_coding and sm_length.
|
||||
if (typeof message === 'string') {
|
||||
const dataCoding = resolved.data_coding;
|
||||
const encoding = typeof dataCoding === 'number'
|
||||
? encodingByDataCoding(dataCoding)
|
||||
: detect(message);
|
||||
const encoded = encodeMessage(message, encoding);
|
||||
|
||||
resolved.short_message = encoded.buffer;
|
||||
resolved.sm_length = encoded.buffer.length;
|
||||
|
||||
if (typeof dataCoding !== 'number') {
|
||||
resolved.data_coding = consts.ENCODING[encoded.encoding];
|
||||
}
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
data_coding: typeof dataCoding === 'number' ? dataCoding : consts.ENCODING[encoded.encoding],
|
||||
short_message: encoded.buffer,
|
||||
sm_length: encoded.buffer.length,
|
||||
};
|
||||
}
|
||||
|
||||
function writeParams(
|
||||
definition: CommandDefinition,
|
||||
resolved: Record<string, ParamValue | undefined>,
|
||||
cmdName: CommandName,
|
||||
): Result<{ chunks: Buffer[] }> {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
for (const [name, type] of Object.entries(definition.params ?? {})) {
|
||||
@@ -139,6 +124,12 @@ function buildPdu(
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return { chunks };
|
||||
}
|
||||
|
||||
function writeTlvs(tlvs: Record<string, TlvInput> | undefined): Result<{ chunks: Buffer[] }> {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
for (const [name, tlv] of Object.entries(tlvs ?? {})) {
|
||||
const tag = tagIdOf(name, tlv);
|
||||
|
||||
@@ -169,7 +160,39 @@ function buildPdu(
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
const body = Buffer.concat(chunks);
|
||||
return { chunks };
|
||||
}
|
||||
|
||||
function buildPdu(
|
||||
cmdName: CommandName,
|
||||
cmdStatus: ErrorName,
|
||||
seqNr: number,
|
||||
params: Record<string, ParamValue | undefined>,
|
||||
tlvs: Record<string, TlvInput> | undefined,
|
||||
): Result<{ buffer: Buffer }> {
|
||||
const definition = cmds[cmdName];
|
||||
|
||||
if (!definition) {
|
||||
return { err: new Error(`Invalid cmdName: ${JSON.stringify(cmdName)}`) };
|
||||
}
|
||||
|
||||
if (!isErrorName(cmdStatus)) {
|
||||
return { err: new Error(`Invalid cmdStatus: ${JSON.stringify(cmdStatus)}`) };
|
||||
}
|
||||
|
||||
if (!Number.isInteger(seqNr) || seqNr < 0 || seqNr > maxSeqNr) {
|
||||
return { err: new Error(`Invalid seqNr: ${JSON.stringify(seqNr)}`) };
|
||||
}
|
||||
|
||||
const written = writeParams(definition, resolveShortMessage(params), cmdName);
|
||||
|
||||
if (written.err) return { err: written.err };
|
||||
|
||||
const writtenTlvs = writeTlvs(tlvs);
|
||||
|
||||
if (writtenTlvs.err) return { err: writtenTlvs.err };
|
||||
|
||||
const body = Buffer.concat([...written.chunks, ...writtenTlvs.chunks]);
|
||||
const header = Buffer.alloc(16);
|
||||
|
||||
header.writeUInt32BE(body.length + 16, 0);
|
||||
@@ -223,22 +246,11 @@ function parseTlvs(
|
||||
return { offset, tlvs };
|
||||
}
|
||||
|
||||
function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolean; pduObj: PduObject }> {
|
||||
const cmdLength = pdu.readUInt32BE(0);
|
||||
const cmdId = pdu.readUInt32BE(4);
|
||||
const cmdName = commandNameById(cmdId);
|
||||
|
||||
if (!cmdName) {
|
||||
return { err: new Error(`Unknown PDU command id: ${String(cmdId)}`) };
|
||||
}
|
||||
|
||||
const cmdStatusId = pdu.readUInt32BE(8);
|
||||
const seqNr = pdu.readUInt32BE(12);
|
||||
|
||||
if (seqNr > maxSeqNr) {
|
||||
return { err: new Error(`Invalid seqNr, exceeds ${String(maxSeqNr)}: ${String(seqNr)}`) };
|
||||
}
|
||||
|
||||
function readParams(
|
||||
cmdName: CommandName,
|
||||
pdu: Buffer,
|
||||
trailingNull: boolean,
|
||||
): Result<{ offset: number; params: Record<string, ParamValue> }> {
|
||||
const params: Record<string, ParamValue> = {};
|
||||
let offset = 16;
|
||||
|
||||
@@ -255,10 +267,34 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea
|
||||
if (name === 'short_message' && trailingNull) offset++;
|
||||
}
|
||||
|
||||
const parsed = parseTlvs(pdu, offset, cmdLength);
|
||||
return { offset, params };
|
||||
}
|
||||
|
||||
function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolean; pduObj: PduObject }> {
|
||||
const cmdLength = pdu.readUInt32BE(0);
|
||||
const cmdId = pdu.readUInt32BE(4);
|
||||
const cmdName = commandNameById(cmdId);
|
||||
|
||||
if (!cmdName) {
|
||||
return { err: new Error(`Unknown PDU command id: ${String(cmdId)}`) };
|
||||
}
|
||||
|
||||
const cmdStatusId = pdu.readUInt32BE(8);
|
||||
const seqNr = pdu.readUInt32BE(12);
|
||||
|
||||
if (seqNr > maxSeqNr) {
|
||||
return { err: new Error(`Invalid seqNr, exceeds ${String(maxSeqNr)}: ${String(seqNr)}`) };
|
||||
}
|
||||
|
||||
const read = readParams(cmdName, pdu, trailingNull);
|
||||
|
||||
if (read.err) return { err: read.err };
|
||||
|
||||
const parsed = parseTlvs(pdu, read.offset, cmdLength);
|
||||
|
||||
if (parsed.err) return { err: parsed.err };
|
||||
|
||||
const params = read.params;
|
||||
const message = params.short_message;
|
||||
const esmClass = numberOr(params.esm_class, 0);
|
||||
|
||||
@@ -282,7 +318,7 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea
|
||||
};
|
||||
}
|
||||
|
||||
export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
||||
function checkFraming(pdu: Buffer): VoidResult {
|
||||
if (pdu.length < 16) {
|
||||
return { err: new Error(`PDU is too short, minimum is 16 octets, got ${String(pdu.length)}`) };
|
||||
}
|
||||
@@ -297,6 +333,14 @@ export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
||||
return { err: new Error(`cmd_length ${String(cmdLength)} exceeds the ${String(pdu.length)} octets given`) };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
||||
const framing = checkFraming(pdu);
|
||||
|
||||
if (framing.err) return { err: framing.err };
|
||||
|
||||
const plain = parseOnce(pdu, false);
|
||||
|
||||
if (!plain.err && plain.aligned) return { pduObj: plain.pduObj };
|
||||
@@ -311,6 +355,25 @@ export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
||||
return { err: plain.err };
|
||||
}
|
||||
|
||||
/** Fields the response shares with the request are echoed back unless the caller overrode them. */
|
||||
function echoParams(
|
||||
respName: CommandName,
|
||||
pdu: PduObject,
|
||||
params: Record<string, ParamValue>,
|
||||
): Record<string, ParamValue> {
|
||||
const respParams: Record<string, ParamValue> = { ...params };
|
||||
|
||||
for (const name of Object.keys(cmds[respName]?.params ?? {})) {
|
||||
const value = pdu.params[name];
|
||||
|
||||
if (respParams[name] === undefined && value !== undefined) {
|
||||
respParams[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return respParams;
|
||||
}
|
||||
|
||||
export function pduReturn(
|
||||
pdu: Buffer | PduObject,
|
||||
status: ErrorName = 'ESME_ROK',
|
||||
@@ -329,16 +392,5 @@ export function pduReturn(
|
||||
return { err: new Error(`"${pdu.cmdName}" has no response command`) };
|
||||
}
|
||||
|
||||
const respParams: Record<string, ParamValue> = { ...params };
|
||||
|
||||
// Fields the response shares with the request are echoed back unless the caller overrode them.
|
||||
for (const name of Object.keys(cmds[respName]?.params ?? {})) {
|
||||
const value = pdu.params[name];
|
||||
|
||||
if (respParams[name] === undefined && value !== undefined) {
|
||||
respParams[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return buildPdu(respName, status, pdu.seqNr, respParams, tlvs);
|
||||
return buildPdu(respName, status, pdu.seqNr, echoParams(respName, pdu, params), tlvs);
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import type { Result } from './result.ts';
|
||||
import type { Server as NetServer, Socket } from 'node:net';
|
||||
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Session, bindCommands } from './session.ts';
|
||||
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
||||
import { createServer as createNetServer } from 'node:net';
|
||||
import { createServer as createTlsServer } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
@@ -45,7 +45,7 @@ const defaults = {
|
||||
idleTimeout: 40_000,
|
||||
interfaceVersion: defaultInterfaceVersion,
|
||||
port: 2775,
|
||||
systemId: '',
|
||||
systemId: defaultSystemId,
|
||||
};
|
||||
|
||||
/** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */
|
||||
|
||||
+3
-1
@@ -72,6 +72,8 @@ export type SessionOptions = {
|
||||
systemId?: string | undefined;
|
||||
};
|
||||
|
||||
export const defaultSystemId = '';
|
||||
|
||||
const defaults = {
|
||||
maxDelay: 30_000,
|
||||
maxOutstanding: 10,
|
||||
@@ -79,7 +81,7 @@ const defaults = {
|
||||
minDelay: 1000,
|
||||
reassemblyTimeout: 300_000,
|
||||
responseTimeout: 30_000,
|
||||
systemId: '',
|
||||
systemId: defaultSystemId,
|
||||
};
|
||||
|
||||
export const bindCommands: readonly string[] = [
|
||||
|
||||
Reference in New Issue
Block a user