Document answeredOnArrival for client receivers and onRequest refusal (#85)
* Document answeredOnArrival for client receivers and onRequest refusal * Apply stability review: correct the onRequest bind claim and teardown order
This commit is contained in:
@@ -132,7 +132,9 @@ most handsets and SMSCs stop well short of 255, and refusing beats a message onl
|
|||||||
### Receiving
|
### Receiving
|
||||||
|
|
||||||
A `receiver` or `transceiver` client gets mobile-originated messages as `sms` events — the same
|
A `receiver` or `transceiver` client gets mobile-originated messages as `sms` events — the same
|
||||||
handle the server side gets, answered the same way:
|
handle the server side gets, answered the same way. That includes a multipart message: it was
|
||||||
|
already answered segment by segment before you see it, which changes what `sendResp()` does there —
|
||||||
|
see [Server](#server).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
session.on('sms', async sms => {
|
session.on('sms', async sms => {
|
||||||
@@ -207,11 +209,12 @@ if (err) throw err;
|
|||||||
|
|
||||||
smpp.on('session', session => {
|
smpp.on('session', session => {
|
||||||
session.on('sms', async sms => {
|
session.on('sms', async sms => {
|
||||||
// Responding is part of the protocol, not optional. Without arguments it answers
|
if (sms.answeredOnArrival) {
|
||||||
// ESME_ROK with a generated id. On a message sendResp() still answers itself — see
|
await sms.sendResp(); // multipart: already answered per segment; this only releases the shutdown drain
|
||||||
// below — you may pass your own id, and a status to refuse the message:
|
} else {
|
||||||
// await sms.sendResp({ smsId: yourOwnId, status: 'ESME_RMSGQFUL' });
|
// no args: ESME_ROK + generated id; or sendResp({ smsId, status: 'ESME_RMSGQFUL' })
|
||||||
await sms.sendResp();
|
await sms.sendResp();
|
||||||
|
}
|
||||||
|
|
||||||
if (sms.dlr) {
|
if (sms.dlr) {
|
||||||
await sms.sendDlr(); // same as sms.sendDlr('DELIVERED')
|
await sms.sendDlr(); // same as sms.sendDlr('DELIVERED')
|
||||||
@@ -235,6 +238,40 @@ answers itself. `sms.smsId` is the base either way, and `sendDlr()` names `<smsI
|
|||||||
and so on — the ids a `submit_sm`'s responses carried. A `deliver_sm` is answered with no id at all,
|
and so on — the ids a `submit_sm`'s responses carried. A `deliver_sm` is answered with no id at all,
|
||||||
since SMPP marks that field unused, so an inbound message's base is a handle of your own only.
|
since SMPP marks that field unused, so an inbound message's base is a handle of your own only.
|
||||||
|
|
||||||
|
A refusal that depends on the request rather than the reassembled message — a full queue, an unknown
|
||||||
|
recipient, an unauthorised sender — needs to land before a segment is answered, which `sendResp()`
|
||||||
|
can no longer do once it has. A hand-wired `Session` (see [Bind direction](#bind-direction)) decides
|
||||||
|
there instead, at `onRequest`, which runs before any of the library's own handling:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import net from 'node:net';
|
||||||
|
import { isCommand, Session } from '@larvit/smpp';
|
||||||
|
|
||||||
|
const knownRecipients = new Set(['46709771337']);
|
||||||
|
|
||||||
|
net.createServer(sock => {
|
||||||
|
const session = new Session({
|
||||||
|
onRequest: async (bound, pduObj) => {
|
||||||
|
if (!isCommand(pduObj, 'submit_sm') || knownRecipients.has(pduObj.params.destination_addr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bound.sendReturn(pduObj, 'ESME_RINVDSTADR');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
sock,
|
||||||
|
});
|
||||||
|
|
||||||
|
session.linkEnd = 'smsc';
|
||||||
|
}).listen(2775);
|
||||||
|
```
|
||||||
|
|
||||||
|
Returning `true` means the hook has answered the PDU and the library leaves it alone; `false` lets
|
||||||
|
the built-in handling — reassembly, the `sms` event — run as usual. A hand-wired `Session` has no
|
||||||
|
bind handling of its own, though: `onRequest` is where a peer's bind gets accepted too, the way
|
||||||
|
`server()` does it.
|
||||||
|
|
||||||
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
||||||
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. `SCHEDULED` and `ENROUTE` go out as intermediate
|
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. `SCHEDULED` and `ENROUTE` go out as intermediate
|
||||||
delivery notifications (`esm_class` 0x20), the rest as delivery receipts (0x04).
|
delivery notifications (`esm_class` 0x20), the rest as delivery receipts (0x04).
|
||||||
|
|||||||
+2
-2
@@ -87,7 +87,7 @@ export function createSms(input: SmsInput, handlers: SmsHandlers): Sms {
|
|||||||
sendDlr: status => sendDlr(sms, handlers.send, status),
|
sendDlr: status => sendDlr(sms, handlers.send, status),
|
||||||
sendResp: options => (input.answeredAs === undefined
|
sendResp: options => (input.answeredAs === undefined
|
||||||
? sendResp(sms, answered, options ?? {}, handlers)
|
? sendResp(sms, answered, options ?? {}, handlers)
|
||||||
: alreadyAnswered(options ?? {}, handlers)),
|
: answeredOnArrival(options ?? {}, handlers)),
|
||||||
session: input.session,
|
session: input.session,
|
||||||
get smsId(): string {
|
get smsId(): string {
|
||||||
return answered.smsId;
|
return answered.smsId;
|
||||||
@@ -100,7 +100,7 @@ export function createSms(input: SmsInput, handlers: SmsHandlers): Sms {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Every segment went out answered, so the call is what the shutdown waits for and nothing else. */
|
/** Every segment went out answered, so the call is what the shutdown waits for and nothing else. */
|
||||||
function alreadyAnswered(
|
function answeredOnArrival(
|
||||||
options: SendRespOptions,
|
options: SendRespOptions,
|
||||||
handlers: Pick<SmsHandlers, 'onAnswered'>,
|
handlers: Pick<SmsHandlers, 'onAnswered'>,
|
||||||
): Promise<VoidResult> {
|
): Promise<VoidResult> {
|
||||||
|
|||||||
+80
-4
@@ -1,15 +1,18 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import net from 'node:net';
|
||||||
import test, { describe } from 'node:test';
|
import test, { describe } from 'node:test';
|
||||||
import type { Dlr } from '../src/dlr.ts';
|
import type { Dlr } from '../src/dlr.ts';
|
||||||
import type { Session } from '../src/session.ts';
|
import type { PduObject } from '../src/pdu.ts';
|
||||||
import type { Sms } from '../src/sms.ts';
|
import type { Sms } from '../src/sms.ts';
|
||||||
import type { SmppLog } from '../src/log.ts';
|
import type { SmppLog } from '../src/log.ts';
|
||||||
import type { SmppServer } from '../src/server.ts';
|
import type { SmppServer } from '../src/server.ts';
|
||||||
import type { TestContext } from 'node:test';
|
import type { TestContext } from 'node:test';
|
||||||
|
import { PduFramer } from '../src/pdu-framer.ts';
|
||||||
import { PduRefusedError } from '../src/pdu-refusal.ts';
|
import { PduRefusedError } from '../src/pdu-refusal.ts';
|
||||||
import { objToPdu } from '../src/pdu.ts';
|
import { Session } from '../src/session.ts';
|
||||||
import { client } from '../src/client.ts';
|
import { client } from '../src/client.ts';
|
||||||
import { closeAfter } from './teardown.ts';
|
import { closeAfter, closeListenerAfter } from './teardown.ts';
|
||||||
|
import { isCommand, objToPdu, pduToObj } from '../src/pdu.ts';
|
||||||
import { server } from '../src/server.ts';
|
import { server } from '../src/server.ts';
|
||||||
|
|
||||||
function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
|
function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
|
||||||
@@ -218,9 +221,17 @@ describe('README: Server', () => {
|
|||||||
|
|
||||||
closeAfter(t, smpp);
|
closeAfter(t, smpp);
|
||||||
|
|
||||||
|
let answeredOnArrival: boolean | undefined;
|
||||||
|
|
||||||
smpp.on('session', session => {
|
smpp.on('session', session => {
|
||||||
session.on('sms', async sms => {
|
session.on('sms', async sms => {
|
||||||
await sms.sendResp();
|
answeredOnArrival = sms.answeredOnArrival;
|
||||||
|
|
||||||
|
if (sms.answeredOnArrival) {
|
||||||
|
await sms.sendResp(); // multipart: only releases the shutdown drain
|
||||||
|
} else {
|
||||||
|
await sms.sendResp(); // ESME_ROK with a generated id
|
||||||
|
}
|
||||||
|
|
||||||
if (sms.dlr) {
|
if (sms.dlr) {
|
||||||
await sms.sendDlr();
|
await sms.sendDlr();
|
||||||
@@ -250,6 +261,71 @@ describe('README: Server', () => {
|
|||||||
|
|
||||||
assert.equal(sent.err, undefined);
|
assert.equal(sent.err, undefined);
|
||||||
assert.equal((await reported).statusMsg, 'DELIVERED');
|
assert.equal((await reported).statusMsg, 'DELIVERED');
|
||||||
|
assert.equal(answeredOnArrival, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refusing a submission at onRequest, before this library would answer it', async t => {
|
||||||
|
const knownRecipients = new Set(['46709771337']);
|
||||||
|
const accepted: net.Socket[] = [];
|
||||||
|
const listener = net.createServer(sock => {
|
||||||
|
accepted.push(sock);
|
||||||
|
|
||||||
|
const session = new Session({
|
||||||
|
onRequest: async (bound, pduObj) => {
|
||||||
|
if (!isCommand(pduObj, 'submit_sm') || knownRecipients.has(pduObj.params.destination_addr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await bound.sendReturn(pduObj, 'ESME_RINVDSTADR');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
sock,
|
||||||
|
});
|
||||||
|
|
||||||
|
session.linkEnd = 'smsc';
|
||||||
|
closeAfter(t, session);
|
||||||
|
});
|
||||||
|
|
||||||
|
closeListenerAfter(t, listener, accepted);
|
||||||
|
|
||||||
|
await new Promise<void>(resolve => { listener.listen(0, resolve); });
|
||||||
|
|
||||||
|
const address = listener.address();
|
||||||
|
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
||||||
|
const peer = net.connect({ port });
|
||||||
|
|
||||||
|
t.after(() => { peer.destroy(); });
|
||||||
|
|
||||||
|
const framer = new PduFramer();
|
||||||
|
const refused = once<PduObject>(resolve => {
|
||||||
|
peer.on('data', chunk => {
|
||||||
|
framer.push(chunk);
|
||||||
|
|
||||||
|
for (const pdu of framer.next().pdus ?? []) {
|
||||||
|
const { pduObj } = pduToObj(pdu);
|
||||||
|
|
||||||
|
if (pduObj) resolve(pduObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await once<true>(resolve => { peer.once('connect', () => { resolve(true); }); });
|
||||||
|
|
||||||
|
const { buffer } = objToPdu({
|
||||||
|
cmdName: 'submit_sm',
|
||||||
|
params: {
|
||||||
|
destination_addr: '46700000000',
|
||||||
|
short_message: 'Hello world',
|
||||||
|
source_addr: '46701113311',
|
||||||
|
},
|
||||||
|
seqNr: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(buffer);
|
||||||
|
peer.write(buffer);
|
||||||
|
|
||||||
|
assert.equal((await refused).cmdStatus, 'ESME_RINVDSTADR');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user