diff --git a/README.md b/README.md index 617f140..b117504 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,9 @@ most handsets and SMSCs stop well short of 255, and refusing beats a message onl ### Receiving 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 session.on('sms', async sms => { @@ -207,11 +209,12 @@ if (err) throw err; smpp.on('session', session => { session.on('sms', async sms => { - // Responding is part of the protocol, not optional. Without arguments it answers - // ESME_ROK with a generated id. On a message sendResp() still answers itself — see - // below — you may pass your own id, and a status to refuse the message: - // await sms.sendResp({ smsId: yourOwnId, status: 'ESME_RMSGQFUL' }); - await sms.sendResp(); + if (sms.answeredOnArrival) { + await sms.sendResp(); // multipart: already answered per segment; this only releases the shutdown drain + } else { + // no args: ESME_ROK + generated id; or sendResp({ smsId, status: 'ESME_RMSGQFUL' }) + await sms.sendResp(); + } if (sms.dlr) { await sms.sendDlr(); // same as sms.sendDlr('DELIVERED') @@ -235,6 +238,40 @@ answers itself. `sms.smsId` is the base either way, and `sendDlr()` names ` { + 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`, `ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. `SCHEDULED` and `ENROUTE` go out as intermediate delivery notifications (`esm_class` 0x20), the rest as delivery receipts (0x04). diff --git a/src/sms.ts b/src/sms.ts index e83f036..a82aa88 100644 --- a/src/sms.ts +++ b/src/sms.ts @@ -87,7 +87,7 @@ export function createSms(input: SmsInput, handlers: SmsHandlers): Sms { sendDlr: status => sendDlr(sms, handlers.send, status), sendResp: options => (input.answeredAs === undefined ? sendResp(sms, answered, options ?? {}, handlers) - : alreadyAnswered(options ?? {}, handlers)), + : answeredOnArrival(options ?? {}, handlers)), session: input.session, get smsId(): string { 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. */ -function alreadyAnswered( +function answeredOnArrival( options: SendRespOptions, handlers: Pick, ): Promise { diff --git a/test/readme.test.ts b/test/readme.test.ts index 0895180..e9362f1 100644 --- a/test/readme.test.ts +++ b/test/readme.test.ts @@ -1,15 +1,18 @@ import assert from 'node:assert/strict'; +import net from 'node:net'; import test, { describe } from 'node:test'; 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 { SmppLog } from '../src/log.ts'; import type { SmppServer } from '../src/server.ts'; import type { TestContext } from 'node:test'; +import { PduFramer } from '../src/pdu-framer.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 { closeAfter } from './teardown.ts'; +import { closeAfter, closeListenerAfter } from './teardown.ts'; +import { isCommand, objToPdu, pduToObj } from '../src/pdu.ts'; import { server } from '../src/server.ts'; function once(register: (resolve: (value: T) => void) => void): Promise { @@ -218,9 +221,17 @@ describe('README: Server', () => { closeAfter(t, smpp); + let answeredOnArrival: boolean | undefined; + smpp.on('session', session => { 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) { await sms.sendDlr(); @@ -250,6 +261,71 @@ describe('README: Server', () => { assert.equal(sent.err, undefined); 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(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(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(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'); }); });