Bind as SMPP 3.4 by default, with interfaceVersion to override it

This commit is contained in:
2026-08-26 15:41:44 +02:00
parent 54b73205ac
commit 694c3a506e
5 changed files with 39 additions and 12 deletions
+5 -3
View File
@@ -77,7 +77,8 @@ Every one is optional.
| `host` / `port` | `localhost` / `2775` | Where to connect. | | `host` / `port` | `localhost` / `2775` | Where to connect. |
| `username` / `password` | `user` / `pass` | Bind credentials (`system_id` and `password`). | | `username` / `password` | `user` / `pass` | Bind credentials (`system_id` and `password`). |
| `bindType` | `transceiver` | `transceiver`, `transmitter` or `receiver`. | | `bindType` | `transceiver` | `transceiver`, `transmitter` or `receiver`. |
| `systemType`, `addressRange`, `addrTon`, `addrNpi`, `interfaceVersion` | `''`, `''`, `0`, `0`, `0x50` | The remaining bind fields, for operators that require them. | | `interfaceVersion` | `0x34` | The SMPP version declared at bind. `0x50` for an SMSC that requires SMPP 5.0. |
| `systemType`, `addressRange`, `addrTon`, `addrNpi` | `''`, `''`, `0`, `0` | The remaining bind fields, for operators that require them. |
| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. | | `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. |
| `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. | | `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. |
| `responseTimeout` | `30000` | How long to wait for a response before giving up on it. | | `responseTimeout` | `30000` | How long to wait for a response before giving up on it. |
@@ -275,8 +276,9 @@ have worked around any of these, remove the workaround:
reported the full length, so it went out corrupt. In UCS2 that is any message ending in a reported the full length, so it went out corrupt. In UCS2 that is any message ending in a
character like 一 (U+4E00), which made the bug routine for CJK text. character like 一 (U+4E00), which made the bug routine for CJK text.
- Short or malformed PDUs threw out of the codec instead of being reported as a parse failure. - Short or malformed PDUs threw out of the codec instead of being reported as a parse failure.
- Binds now declare `interface_version` 0x50. 0.4.0 declared 0x00, because the default in its own - Binds now declare `interface_version` 0x34. 0.4.0 declared 0x00, which tells the SMSC the ESME
command table was never applied. Pass `interfaceVersion: 0x34` to bind as SMPP 3.4. speaks SMPP 3.3 or earlier — and a spec-following SMSC then withholds every optional parameter,
including the TLVs delivery receipts are carried in.
- `submit_multi` was missing its `sm_length` field, so its `short_message` never round-tripped. - `submit_multi` was missing its `sm_length` field, so its `short_message` never round-tripped.
The corrected framing is cross-checked against [node-smpp](https://github.com/farhadi/node-smpp), an The corrected framing is cross-checked against [node-smpp](https://github.com/farhadi/node-smpp), an
+2 -2
View File
@@ -33,8 +33,8 @@ const defaults = {
bindType: 'transceiver', bindType: 'transceiver',
enquireLinkInterval: 20_000, enquireLinkInterval: 20_000,
host: 'localhost', host: 'localhost',
/** SMPP 5.0. 0.4.0 declared 0x00 because its per-parameter default was never applied. */ /** SMPP 3.4. The 3.4 spec reserves every value above it, so 0x50 is undefined to a 3.4 SMSC. */
interfaceVersion: 0x50, interfaceVersion: 0x34,
password: 'pass', password: 'pass',
port: 2775, port: 2775,
username: 'user', username: 'user',
+2 -2
View File
@@ -69,7 +69,7 @@ describe('our encoder against the reference parser', () => {
const parsed = referenceParse(ourBuffer({ const parsed = referenceParse(ourBuffer({
cmdName: 'bind_transceiver', cmdName: 'bind_transceiver',
params: { params: {
interface_version: 0x50, interface_version: 0x34,
password: 'bar', password: 'bar',
system_id: 'foo', system_id: 'foo',
system_type: 'smpp', system_type: 'smpp',
@@ -80,7 +80,7 @@ describe('our encoder against the reference parser', () => {
assert.equal(parsed.command, 'bind_transceiver'); assert.equal(parsed.command, 'bind_transceiver');
assert.equal(parsed.system_id, 'foo'); assert.equal(parsed.system_id, 'foo');
assert.equal(parsed.password, 'bar'); assert.equal(parsed.password, 'bar');
assert.equal(parsed.interface_version, 0x50); assert.equal(parsed.interface_version, 0x34);
}); });
test('a deliver_sm carrying delivery receipt TLVs', () => { test('a deliver_sm carrying delivery receipt TLVs', () => {
+25
View File
@@ -5,6 +5,7 @@ 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 { 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> {
@@ -93,6 +94,30 @@ describe('bind', () => {
assert.equal(await responded, '00000010800000150000000400000001'); assert.equal(await responded, '00000010800000150000000400000001');
await smpp.close(); await smpp.close();
}); });
test('declares SMPP 3.4 by default and the version the caller asks for', async () => {
const smpp = await startServer();
const declared: (number | undefined)[] = [];
smpp.on('session', session => {
session.on('incomingPduObj', pduObj => {
if (isCommand(pduObj, 'bind_transceiver')) {
declared.push(pduObj.params.interface_version);
}
});
});
const { session: byDefault } = await connect(smpp);
const { session: asFive } = await connect(smpp, { interfaceVersion: 0x50 });
assert.ok(byDefault);
assert.ok(asFive);
assert.deepEqual(declared, [0x34, 0x50]);
await byDefault.unbind();
await asFive.unbind();
await smpp.close();
});
}); });
describe('sending', () => { describe('sending', () => {
+5 -5
View File
@@ -5,7 +5,7 @@ rules there constrain every item below.
## Status ## Status
The rewrite is **feature complete and green**: 136 tests, lint and typecheck clean, verified on Node The rewrite is **feature complete and green**: 137 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
@@ -62,10 +62,6 @@ Every defect listed in the AGENTS.md table has a regression test naming the beha
## Before publishing 1.0.0 ## Before publishing 1.0.0
- [ ] **Decide the `interface_version` default.** Binds now declare 0x50, which is what 0.4.0's own
command table intended but never applied — it actually sent 0x00. 0x34 (SMPP 3.4) is the more
conservative choice and is what most SMSCs implement. Flagged to the maintainer; the README
documents 0x50 and the `interfaceVersion` option overrides it either way.
- [ ] Create the `@larvit/smpp` package on npm and add `NPM_TOKEN` to the repository secrets, which - [ ] Create the `@larvit/smpp` package on npm and add `NPM_TOKEN` to the repository secrets, which
`.github/workflows/release.yaml` needs. `.github/workflows/release.yaml` needs.
- [ ] Tag `v1.0.0` to publish. - [ ] Tag `v1.0.0` to publish.
@@ -78,6 +74,10 @@ 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 - [ ] **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. 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