Answer 3.4 binds with sc_interface_version and cover TLS with tests
This commit is contained in:
@@ -166,7 +166,7 @@ await smpp.close(); // stop listening and close every live session
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `host` / `port` | all interfaces / `2775` | Where to listen. Pass `0` for any free port. |
|
| `host` / `port` | all interfaces / `2775` | Where to listen. Pass `0` for any free port. |
|
||||||
| `authenticate` | accept everything | `({ password, session, systemId, systemType }) => false \| { userData }`, sync or async. |
|
| `authenticate` | accept everything | `({ password, session, systemId, systemType }) => false \| { userData }`, sync or async. |
|
||||||
| `tls` | `false` | `true`, or a `tls.TlsOptions` object with your certificate and key. |
|
| `tls` | `false` | A `tls.TlsOptions` object with your certificate and key. |
|
||||||
| `idleTimeout` | `40000` | Drop a peer that has been silent this long. |
|
| `idleTimeout` | `40000` | Drop a peer that has been silent this long. |
|
||||||
| `maxReassembly` | `1000` | Incomplete multipart messages held per session. |
|
| `maxReassembly` | `1000` | Incomplete multipart messages held per session. |
|
||||||
| `reassemblyTimeout` | `300000` | How long an incomplete multipart message is held. |
|
| `reassemblyTimeout` | `300000` | How long an incomplete multipart message is held. |
|
||||||
|
|||||||
+45
-6
@@ -1,14 +1,15 @@
|
|||||||
import type { LogInt } from '@larvit/log';
|
import type { LogInt } from '@larvit/log';
|
||||||
import type { PduObject } from './pdu.ts';
|
import type { PduObject, TlvInput } from './pdu.ts';
|
||||||
import type { Result } from './result.ts';
|
import type { Result } from './result.ts';
|
||||||
import type { Server as NetServer, Socket } from 'node:net';
|
import type { Server as NetServer, Socket } from 'node:net';
|
||||||
import type { TlsOptions } from 'node:tls';
|
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { Session } from './session.ts';
|
import { Session } from './session.ts';
|
||||||
import { createServer as createNetServer } from 'node:net';
|
import { createServer as createNetServer } from 'node:net';
|
||||||
import { createServer as createTlsServer } from 'node:tls';
|
import { createServer as createTlsServer } from 'node:tls';
|
||||||
import { paramText } from './defs/types.ts';
|
import { paramText } from './defs/types.ts';
|
||||||
import { silentLog } from './log.ts';
|
import { silentLog } from './log.ts';
|
||||||
|
import { tlvs } from './defs/tlvs.ts';
|
||||||
|
|
||||||
export type AuthenticateResult = { userData?: unknown } | boolean;
|
export type AuthenticateResult = { userData?: unknown } | boolean;
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ const defaults = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bindCommands = ['bind_receiver', 'bind_transceiver', 'bind_transmitter'];
|
const bindCommands = ['bind_receiver', 'bind_transceiver', 'bind_transmitter'];
|
||||||
|
const scInterfaceVersion = 0x34;
|
||||||
|
|
||||||
/** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */
|
/** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */
|
||||||
export class SmppServer extends EventEmitter<ServerEvents> {
|
export class SmppServer extends EventEmitter<ServerEvents> {
|
||||||
@@ -100,6 +102,25 @@ async function authenticate(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A peer that declared below 0x34 must not be sent optional parameters at all; above it, an ESME
|
||||||
|
* reads a missing sc_interface_version as this SMSC having none.
|
||||||
|
*/
|
||||||
|
function bindRespTlvs(pduObj: PduObject): Record<string, TlvInput> | undefined {
|
||||||
|
const declared = pduObj.params.interface_version;
|
||||||
|
const definition = tlvs.sc_interface_version;
|
||||||
|
|
||||||
|
if (!definition || typeof declared !== 'number' || declared < scInterfaceVersion) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sc_interface_version: {
|
||||||
|
tagId: definition.id,
|
||||||
|
tagName: definition.tag,
|
||||||
|
tagValue: scInterfaceVersion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles everything a peer may send before it is bound. Returns true when it has answered, so the
|
* Handles everything a peer may send before it is bound. Returns true when it has answered, so the
|
||||||
* session leaves the PDU alone.
|
* session leaves the PDU alone.
|
||||||
@@ -128,7 +149,7 @@ async function onRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.loggedIn = true;
|
session.loggedIn = true;
|
||||||
await session.sendReturn(pduObj);
|
await session.sendReturn(pduObj, 'ESME_ROK', {}, bindRespTlvs(pduObj));
|
||||||
log.verbose('server - bound', { systemId: paramText(pduObj.params.system_id) });
|
log.verbose('server - bound', { systemId: paramText(pduObj.params.system_id) });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -158,15 +179,33 @@ function onConnection(sock: Socket, options: ServerOptions, server: SmppServer):
|
|||||||
server.emit('session', session);
|
server.emit('session', session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Node reports a rejected handshake as `tlsClientError`, which is never an `error` event. */
|
||||||
|
function createSecureListener(tlsOptions: TlsOptions, log: LogInt): TlsServer {
|
||||||
|
const listener = createTlsServer(tlsOptions);
|
||||||
|
|
||||||
|
listener.on('tlsClientError', err => {
|
||||||
|
log.warn('server - client handshake failed', { message: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
/** Starts listening for SMPP connections. Resolves once the socket is bound. */
|
/** Starts listening for SMPP connections. Resolves once the socket is bound. */
|
||||||
export function server(options: ServerOptions = {}): Promise<Result<{ server: SmppServer }>> {
|
export function server(options: ServerOptions = {}): Promise<Result<{ server: SmppServer }>> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const log = options.log ?? silentLog;
|
const log = options.log ?? silentLog;
|
||||||
const port = options.port ?? defaults.port;
|
const port = options.port ?? defaults.port;
|
||||||
const useTls = options.tls !== undefined && options.tls !== false;
|
const useTls = options.tls !== undefined && options.tls !== false;
|
||||||
const listener = useTls
|
const tlsOptions = typeof options.tls === 'object' ? options.tls : undefined;
|
||||||
? createTlsServer(typeof options.tls === 'object' ? options.tls : {})
|
|
||||||
: createNetServer();
|
if (useTls && !tlsOptions) {
|
||||||
|
log.warn('server - tls without a certificate', { port });
|
||||||
|
resolve({ err: new Error('Listening over TLS needs tls: { cert, key }') });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = tlsOptions ? createSecureListener(tlsOptions, log) : createNetServer();
|
||||||
const smpp = new SmppServer(listener);
|
const smpp = new SmppServer(listener);
|
||||||
|
|
||||||
listener.on(useTls ? 'secureConnection' : 'connection', (sock: Socket) => {
|
listener.on(useTls ? 'secureConnection' : 'connection', (sock: Socket) => {
|
||||||
|
|||||||
+53
-1
@@ -1,11 +1,12 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import test, { describe } from 'node:test';
|
import test, { describe } from 'node:test';
|
||||||
|
import type { PduObject } from '../src/pdu.ts';
|
||||||
import type { Session } from '../src/session.ts';
|
import type { Session } from '../src/session.ts';
|
||||||
import type { Sms } from '../src/sms.ts';
|
import type { Sms } from '../src/sms.ts';
|
||||||
import type { SmppServer } from '../src/server.ts';
|
import type { SmppServer } from '../src/server.ts';
|
||||||
import { client } from '../src/client.ts';
|
import { client } from '../src/client.ts';
|
||||||
import { isCommand } from '../src/pdu.ts';
|
import { isCommand, objToPdu, pduToObj } from '../src/pdu.ts';
|
||||||
import { server } from '../src/server.ts';
|
import { server } from '../src/server.ts';
|
||||||
|
|
||||||
async function startServer(options: Parameters<typeof server>[0] = {}): Promise<SmppServer> {
|
async function startServer(options: Parameters<typeof server>[0] = {}): Promise<SmppServer> {
|
||||||
@@ -25,6 +26,30 @@ function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
|
|||||||
return new Promise<T>(resolve => { register(resolve); });
|
return new Promise<T>(resolve => { register(resolve); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Binds off a raw socket, which is the only way to declare a version the client cannot. */
|
||||||
|
async function bindRaw(smpp: SmppServer, interfaceVersion: number): Promise<PduObject> {
|
||||||
|
const { buffer } = objToPdu({
|
||||||
|
cmdName: 'bind_transceiver',
|
||||||
|
params: { interface_version: interfaceVersion, password: 'pass', system_id: 'user' },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(buffer);
|
||||||
|
|
||||||
|
const sock = net.connect({ port: smpp.port });
|
||||||
|
const response = await once<Buffer>(resolve => {
|
||||||
|
sock.on('connect', () => { sock.write(buffer); });
|
||||||
|
sock.once('data', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
sock.destroy();
|
||||||
|
|
||||||
|
const { pduObj } = pduToObj(response);
|
||||||
|
|
||||||
|
assert.ok(pduObj);
|
||||||
|
|
||||||
|
return pduObj;
|
||||||
|
}
|
||||||
|
|
||||||
describe('bind', () => {
|
describe('bind', () => {
|
||||||
test('binds and unbinds against a server with no auth', async () => {
|
test('binds and unbinds against a server with no auth', async () => {
|
||||||
const smpp = await startServer();
|
const smpp = await startServer();
|
||||||
@@ -118,6 +143,33 @@ describe('bind', () => {
|
|||||||
await asFive.unbind();
|
await asFive.unbind();
|
||||||
await smpp.close();
|
await smpp.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tells a 3.4 peer the version it supports in the bind response', async () => {
|
||||||
|
const smpp = await startServer();
|
||||||
|
const asThreeFour = await bindRaw(smpp, 0x34);
|
||||||
|
const asFive = await bindRaw(smpp, 0x50);
|
||||||
|
|
||||||
|
assert.equal(asThreeFour.cmdName, 'bind_transceiver_resp');
|
||||||
|
assert.equal(asThreeFour.cmdStatus, 'ESME_ROK');
|
||||||
|
assert.deepEqual(asThreeFour.tlvs.sc_interface_version, {
|
||||||
|
tagId: 0x0210,
|
||||||
|
tagName: 'sc_interface_version',
|
||||||
|
tagValue: 0x34,
|
||||||
|
});
|
||||||
|
assert.equal(asFive.tlvs.sc_interface_version?.tagValue, 0x34);
|
||||||
|
|
||||||
|
await smpp.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sends no optional parameters to a peer declaring less than 3.4', async () => {
|
||||||
|
const smpp = await startServer();
|
||||||
|
const bound = await bindRaw(smpp, 0x00);
|
||||||
|
|
||||||
|
assert.equal(bound.cmdStatus, 'ESME_ROK');
|
||||||
|
assert.deepEqual(bound.tlvs, {});
|
||||||
|
|
||||||
|
await smpp.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sending', () => {
|
describe('sending', () => {
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import net from 'node:net';
|
||||||
|
import test, { describe } from 'node:test';
|
||||||
|
import type { Sms } from '../src/sms.ts';
|
||||||
|
import type { SmppServer } from '../src/server.ts';
|
||||||
|
import { Log } from '@larvit/log';
|
||||||
|
import { TLSSocket } from 'node:tls';
|
||||||
|
import { client } from '../src/client.ts';
|
||||||
|
import { generateKeyPairSync, randomBytes, sign } from 'node:crypto';
|
||||||
|
import { server } from '../src/server.ts';
|
||||||
|
|
||||||
|
const host = 'localhost';
|
||||||
|
|
||||||
|
const oids = {
|
||||||
|
basicConstraints: Buffer.from('551d13', 'hex'),
|
||||||
|
commonName: Buffer.from('550403', 'hex'),
|
||||||
|
ecdsaWithSha256: Buffer.from('2a8648ce3d040302', 'hex'),
|
||||||
|
subjectAltName: Buffer.from('551d11', 'hex'),
|
||||||
|
};
|
||||||
|
|
||||||
|
function derLength(length: number): Buffer {
|
||||||
|
if (length < 0x80) return Buffer.from([length]);
|
||||||
|
|
||||||
|
const bytes: number[] = [];
|
||||||
|
|
||||||
|
for (let rest = length; rest > 0; rest = Math.floor(rest / 0x100)) {
|
||||||
|
bytes.unshift(rest % 0x100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from([0x80 | bytes.length, ...bytes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function der(tag: number, ...parts: Buffer[]): Buffer {
|
||||||
|
const body = Buffer.concat(parts);
|
||||||
|
|
||||||
|
return Buffer.concat([Buffer.from([tag]), derLength(body.length), body]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function derBoolean(value: boolean): Buffer {
|
||||||
|
return der(0x01, Buffer.from([value ? 0xff : 0x00]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function derName(commonName: string): Buffer {
|
||||||
|
return der(0x30, der(0x31, der(0x30,
|
||||||
|
der(0x06, oids.commonName),
|
||||||
|
der(0x0c, Buffer.from(commonName, 'utf8')),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function derExtension(id: Buffer, critical: boolean, value: Buffer): Buffer {
|
||||||
|
return der(0x30, der(0x06, id), ...(critical ? [derBoolean(true)] : []), der(0x04, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function derUtcTime(date: Date): Buffer {
|
||||||
|
const text = date.toISOString().replace(/[-:T]/g, '').replace(/\.\d{3}/, '').slice(2);
|
||||||
|
|
||||||
|
return der(0x17, Buffer.from(text, 'ascii'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPem(label: string, contents: Buffer): string {
|
||||||
|
const lines = contents.toString('base64').match(/.{1,64}/g) ?? [];
|
||||||
|
|
||||||
|
return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built here rather than shelled out or committed: the image has no openssl, and a fixture key in a
|
||||||
|
// public repository leaks.
|
||||||
|
function createCertificate(): { cert: string; key: string } {
|
||||||
|
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
||||||
|
const algorithm = der(0x30, der(0x06, oids.ecdsaWithSha256));
|
||||||
|
const serial = randomBytes(8);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
serial.writeUInt8((serial.readUInt8(0) & 0x3f) | 0x40, 0);
|
||||||
|
|
||||||
|
// RFC 5280 TBSCertificate — field order is wire order, never sort it.
|
||||||
|
const tbs = der(0x30,
|
||||||
|
der(0xa0, der(0x02, Buffer.from([0x02]))),
|
||||||
|
der(0x02, serial),
|
||||||
|
algorithm,
|
||||||
|
derName(host),
|
||||||
|
der(0x30, derUtcTime(new Date(now - 60_000)), derUtcTime(new Date(now + 3_600_000))),
|
||||||
|
derName(host),
|
||||||
|
publicKey.export({ format: 'der', type: 'spki' }),
|
||||||
|
der(0xa3, der(0x30,
|
||||||
|
derExtension(oids.basicConstraints, true, der(0x30, derBoolean(true))),
|
||||||
|
derExtension(oids.subjectAltName, false, der(0x30, der(0x82, Buffer.from(host, 'ascii')))),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
const certificate = der(0x30,
|
||||||
|
tbs,
|
||||||
|
algorithm,
|
||||||
|
der(0x03, Buffer.from([0x00]), sign('sha256', tbs, privateKey)),
|
||||||
|
);
|
||||||
|
const key = privateKey.export({ format: 'pem', type: 'pkcs8' });
|
||||||
|
|
||||||
|
return {
|
||||||
|
cert: toPem('CERTIFICATE', certificate),
|
||||||
|
key: typeof key === 'string' ? key : key.toString('utf8'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificate = createCertificate();
|
||||||
|
|
||||||
|
async function startServer(): Promise<SmppServer> {
|
||||||
|
const { err, server: smpp } = await server({
|
||||||
|
port: 0,
|
||||||
|
tls: { cert: certificate.cert, key: certificate.key },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(err, undefined);
|
||||||
|
assert.ok(smpp);
|
||||||
|
|
||||||
|
return smpp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
|
||||||
|
return new Promise<T>(resolve => { register(resolve); });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('tls', () => {
|
||||||
|
test('binds over a verified handshake and delivers an SMS', async () => {
|
||||||
|
const smpp = await startServer();
|
||||||
|
const incoming = once<Sms>(resolve => {
|
||||||
|
smpp.on('session', session => session.on('sms', resolve));
|
||||||
|
});
|
||||||
|
const { err, session } = await client({
|
||||||
|
host,
|
||||||
|
port: smpp.port,
|
||||||
|
tls: { ca: certificate.cert },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(err, undefined);
|
||||||
|
assert.ok(session);
|
||||||
|
assert.ok(session.loggedIn);
|
||||||
|
|
||||||
|
const sock = session.sock;
|
||||||
|
|
||||||
|
assert.ok(sock instanceof TLSSocket);
|
||||||
|
assert.ok(sock.authorized);
|
||||||
|
assert.equal(sock.getPeerCertificate().subject.CN, host);
|
||||||
|
|
||||||
|
const [sms, sent] = await Promise.all([
|
||||||
|
incoming.then(async received => {
|
||||||
|
received.smsId = 'tls-id';
|
||||||
|
await received.sendResp();
|
||||||
|
|
||||||
|
return received;
|
||||||
|
}),
|
||||||
|
session.sendSms({ from: 'MyBrand', message: 'hello over tls', to: '46709771337' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(sms.message, 'hello over tls');
|
||||||
|
assert.equal(sms.to, '46709771337');
|
||||||
|
assert.equal(sent.err, undefined);
|
||||||
|
assert.deepEqual(sent.smsIds, ['tls-id']);
|
||||||
|
|
||||||
|
assert.deepEqual(await session.unbind(), {});
|
||||||
|
await smpp.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns an error rather than throwing when the certificate is not trusted', async () => {
|
||||||
|
const smpp = await startServer();
|
||||||
|
const { err, session } = await client({ host, port: smpp.port, tls: {} });
|
||||||
|
|
||||||
|
assert.ok(err instanceof Error);
|
||||||
|
assert.match(err.message, /self.signed certificate/);
|
||||||
|
assert.equal(session, undefined);
|
||||||
|
|
||||||
|
await smpp.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns an error when the certificate does not cover the host', async () => {
|
||||||
|
const smpp = await startServer();
|
||||||
|
const { err, session } = await client({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: smpp.port,
|
||||||
|
tls: { ca: certificate.cert },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(err instanceof Error);
|
||||||
|
assert.match(err.message, /altnames/);
|
||||||
|
assert.equal(session, undefined);
|
||||||
|
|
||||||
|
await smpp.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refuses to listen over tls without a certificate', async () => {
|
||||||
|
const { err, server: smpp } = await server({ port: 0, tls: true });
|
||||||
|
|
||||||
|
assert.ok(err instanceof Error);
|
||||||
|
assert.equal(smpp, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs a handshake the server turned away', async () => {
|
||||||
|
const warned = once<string>(resolve => {
|
||||||
|
const log = new Log({ logLevel: 'warn', stderr: resolve, stdout: resolve });
|
||||||
|
|
||||||
|
void server({ log, port: 0, tls: { cert: certificate.cert, key: certificate.key } })
|
||||||
|
.then(({ server: smpp }) => {
|
||||||
|
assert.ok(smpp);
|
||||||
|
|
||||||
|
const sock = net.connect({ port: smpp.port }, () => {
|
||||||
|
sock.end('not a client hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
sock.on('close', () => { void smpp.close(); });
|
||||||
|
sock.resume();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(await warned, /client handshake failed/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ rules there constrain every item below.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
The rewrite is **feature complete and green**: 137 tests, lint and typecheck clean, verified on Node
|
The rewrite is **feature complete and green**: 144 tests, lint and typecheck clean, verified on Node
|
||||||
18, 20, 22 and 24. What is left is release work and a few things worth adding before or after 1.0.0.
|
18, 20, 22 and 24. What is left is release work and a few things worth adding before or after 1.0.0.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -74,12 +74,6 @@ Every defect listed in the AGENTS.md table has a regression test naming the beha
|
|||||||
- [ ] **In-flight sends across a reconnect.** They currently fail with "Session closed before a
|
- [ ] **In-flight sends across a reconnect.** They currently fail with "Session closed before a
|
||||||
response arrived" and the caller retries. Re-queueing them automatically would be friendlier
|
response arrived" and the caller retries. Re-queueing them automatically would be friendlier
|
||||||
but risks duplicate delivery, so it needs a decision before it is built.
|
but risks duplicate delivery, so it needs a decision before it is built.
|
||||||
- [ ] **The server never returns the `sc_interface_version` TLV.** The 3.4 spec has an ESME read its
|
|
||||||
absence from a bind response as "this SMSC does not support optional parameters", and ours
|
|
||||||
does send DLR TLVs. Returning it has to be conditional: a peer that declared below 0x34 must
|
|
||||||
not be sent optional parameters at all.
|
|
||||||
- [ ] **TLS is untested.** The code path is right (`tls.connect` / `tls.createServer`, options
|
|
||||||
passed through) but no test exercises a handshake. Needs a self-signed certificate fixture.
|
|
||||||
- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them
|
- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them
|
||||||
end to end. The interop suite is the natural place.
|
end to end. The interop suite is the natural place.
|
||||||
- [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript
|
- [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript
|
||||||
|
|||||||
Reference in New Issue
Block a user