Run Jasmin in both directions: message_payload, data_sm and a multipart deadlock confirmed against a production gateway

This commit is contained in:
2026-09-06 02:24:32 +02:00
parent db02f0058d
commit 66b49ebfb3
7 changed files with 1858 additions and 1 deletions
+1 -1
View File
@@ -194,7 +194,7 @@ document, library changes, and the AGENTS.md decision record each one needs.
| 0 | done | [01-smscsim.md](findings/01-smscsim.md) | | 0 | done | [01-smscsim.md](findings/01-smscsim.md) |
| 1 | done | [01-smscsim.md](findings/01-smscsim.md) | | 1 | done | [01-smscsim.md](findings/01-smscsim.md) |
| 2 | done | [02-smppsim.md](findings/02-smppsim.md) | | 2 | done | [02-smppsim.md](findings/02-smppsim.md) |
| 3 | not started | — | | 3 | done | [03-jasmin.md](findings/03-jasmin.md) |
| 4 | done | [04-kannel.md](findings/04-kannel.md) | | 4 | done | [04-kannel.md](findings/04-kannel.md) |
| 510 | not started | — | | 510 | not started | — |
+139
View File
@@ -0,0 +1,139 @@
x-log-limits: &log-limits
logging:
driver: json-file
options:
max-file: "3"
max-size: 20m
# Bare TCP connect, no bytes written - Kannel's own healthcheck pattern (interop-tests/compose.kannel.yaml).
# Jasmin's SMPP codec, like SMPPSim's, stack-traces on a probe that writes anything to 2775, so this
# probes the HTTP API port instead, which answers (or ignores) a bare connect harmlessly.
x-jasmin-healthcheck: &jasmin-healthcheck
test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/127.0.0.1/1401'"]
interval: 1s
retries: 60
timeout: 2s
x-rabbit-healthcheck: &rabbit-healthcheck
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 2s
retries: 60
timeout: 5s
x-jasmin-bootstrap-image: &jasmin-bootstrap-image
image: python:3.13.7-slim-bookworm
<<: *log-limits
volumes:
- ./interop-tests/peers/jasmin:/bootstrap:ro
command: ["python3", "-u", "/bootstrap/bootstrap.py"]
services:
jasmin-redis:
image: redis:8.8.2-alpine
<<: *log-limits
jasmin-rabbit:
# 0.11.0's txamqp client declares transient/non-exclusive queues, a feature RabbitMQ 4.x
# refuses by default ("transient_nonexcl_queues... not permitted anymore"), so jasmind's
# RouterPB/DLRThrower services fail to start against rabbitmq:4.3.5. Falling back to the 3.13
# series (confirmed against 3.13.7) starts clean - see findings/03-jasmin.md.
image: rabbitmq:3.13.7-management-alpine
<<: *log-limits
environment:
RABBITMQ_DEFAULT_PASS: guest
RABBITMQ_DEFAULT_USER: guest
healthcheck: *rabbit-healthcheck
jasmin:
image: jookies/jasmin:0.11.0
<<: *log-limits
environment:
AMQP_BROKER_HOST: jasmin-rabbit
REDIS_CLIENT_HOST: jasmin-redis
depends_on:
jasmin-redis:
condition: service_started
jasmin-rabbit:
condition: service_healthy
healthcheck: *jasmin-healthcheck
# group, users, the smppc connector to our own server() and the MT/MO routes - see
# interop-tests/peers/jasmin/bootstrap.py. Runs once and exits; node waits for it.
jasmin-bootstrap:
<<: *jasmin-bootstrap-image
environment:
JCLI_HOST: jasmin
depends_on:
jasmin:
condition: service_healthy
# dlr-thrower's dlr_pdu is a [dlr-thrower] config-file setting, not a jcli/connector key (target
# 4 / C9) - a second instance with its own config is the only way to flip it without touching the
# main instance's receipts. Its own redis/rabbit rather than sharing the main pair: two unrelated
# Jasmin instances sharing a broker is exactly the entanglement CLAUDE.md's compose rule bans.
jasmin-datasm-redis:
image: redis:8.8.2-alpine
<<: *log-limits
jasmin-datasm-rabbit:
image: rabbitmq:3.13.7-management-alpine
<<: *log-limits
environment:
RABBITMQ_DEFAULT_PASS: guest
RABBITMQ_DEFAULT_USER: guest
healthcheck: *rabbit-healthcheck
jasmin-datasm:
image: jookies/jasmin:0.11.0
<<: *log-limits
environment:
AMQP_BROKER_HOST: jasmin-datasm-rabbit
REDIS_CLIENT_HOST: jasmin-datasm-redis
volumes:
# Not bind-mounted straight over /etc/jasmin/jasmin.cfg: the entrypoint's `sed -i` renames a
# temp file over its target, which the kernel refuses for a bind-mounted path ("Device or
# resource busy"). Copying it into place first, over a normal writable file, sidesteps that.
- ./interop-tests/peers/jasmin/jasmin-datasm.cfg:/custom-cfg/jasmin.cfg:ro
entrypoint: ["bash", "-c"]
command:
- >-
cp /custom-cfg/jasmin.cfg /etc/jasmin/jasmin.cfg &&
exec /docker-entrypoint.sh jasmind.py --enable-interceptor-client --enable-dlr-thrower --enable-dlr-lookup -u jcliadmin -p jclipwd
depends_on:
jasmin-datasm-redis:
condition: service_started
jasmin-datasm-rabbit:
condition: service_healthy
healthcheck: *jasmin-healthcheck
jasmin-datasm-bootstrap:
<<: *jasmin-bootstrap-image
environment:
CONNECTOR_CID: upstreamds
CONNECTOR_USERNAME: upstreamdsesme
JCLI_HOST: jasmin-datasm
depends_on:
jasmin-datasm:
condition: service_healthy
capture:
image: nicolaka/netshoot:v0.16
network_mode: "service:jasmin"
cap_add:
- NET_ADMIN
- NET_RAW
depends_on:
jasmin:
condition: service_started
command: ["dumpcap", "-i", "any", "-f", "tcp port 2775", "-w", "/captures/jasmin.pcapng"]
volumes:
- ./interop-tests/captures:/captures
node:
depends_on:
capture:
condition: service_started
jasmin-bootstrap:
condition: service_completed_successfully
jasmin-datasm-bootstrap:
condition: service_completed_successfully
+212
View File
@@ -0,0 +1,212 @@
# 03 jasmin
Date: 2026-09-06. Repo commit: `db02f00`. Host Docker: 29.6.2. Images: `jookies/jasmin:0.11.0`
(both directions), `redis:8.8.2-alpine`, `rabbitmq:3.13.7-management-alpine` (fallback - see
below), `python:3.13.7-slim-bookworm` (jcli bootstrap), `nicolaka/netshoot:v0.16` (capture),
`node:24.18.0-bookworm-slim` (test runner, from the root `compose.yaml`).
## Setup
Two full Jasmin instances (`jasmin`, `jasmin-datasm`), each with its own `redis`/`rabbitmq` pair -
kept separate rather than shared, since two unrelated Jasmin instances sharing a broker is the
kind of proximity-only coupling the project avoids. `jasmin-datasm` mounts a custom `jasmin.cfg`
(`[dlr-thrower] dlr_pdu = data_sm`) for C9/target 4. Both run a `jcli` bootstrap
(`interop-tests/peers/jasmin/bootstrap.py`, a plain socket client - no telnet negotiation reply is
needed, the server proceeds regardless) that creates a group, two users (`esme1` for most
scenarios, `esme2` with a throttled `smpps_throughput` quota for C12), an `smppccm` connector
("upstream"/"upstreamds") pointing at our own `node` container, and `mtrouter`/`morouter`
`DefaultRoute`s wiring MT to that connector and MO back to `smpps(esme1)`. `interop-tests/jasmin.test.ts`
runs one persistent `server()` on `node:2777` as the fake real-world SMSC both connectors bind out
to, plus a tiny HTTP listener for Jasmin's DLR-thrower webhook (S7).
**RabbitMQ 4.3.5 does not work with Jasmin 0.11.0**: its `txamqp` client declares
transient/non-exclusive queues, a feature RabbitMQ 4.x refuses by default ("transient_nonexcl_queues
... not permitted anymore"), so `RouterPB`/`DLRThrower` fail to start. Fell back to the 3.13 series;
`rabbitmq:3.13.7-management-alpine` starts clean.
Snags fixed while building the harness, roughly in the order found:
- **jcli commands after `smppccm -a` need loud failure detection, not keyword-sniffing.** A failed
`ok` ("Failed adding connector, check log for details") doesn't contain any of "error"/"unknown"/
"invalid" - it left the bootstrap's own session stuck at the `>` sub-prompt, and every later
command was misread as a key inside it. Fixed by also checking the last reply lands back on the
top-level `jcli :` prompt.
- **The connector's password has an 8-character wire maximum.** SMPP's `password` is a C-octet-string
with an 8-char + NUL limit; Jasmin's own `smpp.pdu` encoder enforces it strictly when it builds the
connector's own bind PDU (our library's encoder is permissive and doesn't). A 10-char password
made every single connector bind throw mid-encode (`ValueError: COctetString is longer than
allowed maximum size (9)`), logged only in the connector's own per-CID log file
(`/var/log/jasmin/default-<cid>.log`), not in the main Jasmin process log.
- **`session.userData` is unset when the `session` event fires** (it fires on raw connect, before
`authenticate()` runs) - populating a map off it there silently never worked. Fixed like
`kannel.test.ts`'s `bindPdus`: read the bind PDU's own `system_id` from `incomingPduObj` instead,
and only read `userData` later, from `sms`, once authenticate() has long since run.
- **Jasmin's own MT dispatch to one connector is strictly serialized** (see the defect below) -
moving `C13`/`S7`'s HTTP-send test ahead of `C3+C7`'s multi-segment sends in file order (both need
the same connector's queue to be unstuck) turned three tests that always failed into two that
always pass.
- The `jasmin`/`jasmin-datasm` healthcheck probes the HTTP API port (1401) with a bare TCP connect,
no bytes written - Jasmin's SMPP codec, like SMPPSim's, is not something to probe with an empty
PDU. `rabbitmq` uses `rabbitmq-diagnostics -q ping`.
- Bootstrap and the whole `docker compose up` sequence are slow and variable on this host - jcli's
own connector/router commands (which round-trip through AMQP/redis, unlike the plain in-memory
group/user commands) sometimes took most of a minute each under contention; the harness itself
budgets for it (`run.py` runs past its own foreground tool timeout and is watched to completion),
not something to read as a Jasmin defect.
Three runs of `./interop-tests/run.py jasmin`, all after the fixes above: all exit non-zero (the
4 failing tests below), all `malformed: 0`, `expert errors: 0`. Wire commands, from the last run:
```
bind_transceiver: 14, bind_transmitter: 1, bind_receiver: 6 (+ their _resp)
submit_sm: 45, submit_sm_resp: 43
deliver_sm: 15, deliver_sm_resp: 1
enquire_link: 7, enquire_link_resp: 7
unbind: 1, unbind_resp: 1
```
## Scenarios
| Id | Result | Evidence |
| --- | --- | --- |
| C1 | pass | `binds transceiver, sees Jasmin's own enquire_link, unbinds clean`; `binds transmitter`; `binds receiver` |
| C3+C7 (single-segment) | pass | `single-segment GSM with extension chars`: UUID id, `receipted_message_id`/`message_state` TLVs present, `DELIVERED` |
| C3+C7 (multi-segment) | **fail (peer/library interaction, see Defects)** | `2-segment`/`3-segment`/`10-segment GSM`, `2-segment UCS2`: every attempt across 3 runs times out waiting for a receipt |
| C8 target 3 (SAR) | pass | `SAR-segmented deliver_sm from the fake upstream`: reassembles into one whole `sms` |
| C8 target 3 (UDH) | pass | `UDH-segmented deliver_sm from the fake upstream`: reassembles into one whole `sms` |
| C8 target 2 (`message_payload`) | pass (defect confirmed) | `a deliver_sm carrying message_payload instead of short_message`: Jasmin accepts and relays it; our `sms` arrives with an empty message - see Defects |
| C9 target 4 (`data_sm` DLR) | pass (defect confirmed) | `a receipt thrown as data_sm is not read as a dlr`: the raw `data_sm` PDU is seen, no `dlr` event ever fires - see Defects |
| C11 | pass | `wrong password: one attempt, no retry` (`ESME_RINVPASWD`); `a rebind refused after a live link drops: backs off, never floods` (2-8 attempts, ≥150ms apart) |
| C12 | pass | `flooding submits past the quota`: `ESME_RTHROTTLED` from `esme2`'s `smpps_throughput 0.1` quota on some of 15 parallel sends; session stays bound; a later send succeeds once the next slot opens |
| C13 | pass | `every send is answered, none lost, order preserved`: 10 distinct ids, arrival order matches send order |
| S7 | pass | `a message pushed through /send arrives as submit_sm`: HTTP `/send` → Jasmin → connector → our `server()`; our `sendResp()`+`sendDlr('DELIVERED')` fires Jasmin's own DLR webhook, both the SMSC-ack (`ESME_ROK`) and terminal (`DELIVRD`) callbacks at `dlr-level=3`; long GSM and UCS-2 MO pushes from our server reassemble whole at Jasmin's connector |
## Defects in @larvit/smpp
### `message_payload` is never read (target 2) - confirmed against a real peer
**What happened.** Jasmin's connector accepts a `deliver_sm` with `sm_length` 0 and the text in a
`message_payload` TLV from our fake upstream SMSC without complaint, and relays it through
`morouter` to our real client bound to Jasmin's `smpps`. The `sms` event fires with the right
envelope (`from`/`to`) but `message` is the empty string - the actual text, which only ever existed
in `message_payload`, is lost. Matches the target exactly: `incoming-requests.ts` reads
`short_message` only.
**Reproducer.** `interop-tests/jasmin.test.ts`, `C8 (target 2)`: build a `deliver_sm` with
`short_message: Buffer.alloc(0)` and `tlvs: { message_payload: { tagValue: Buffer.from(text) } }`,
send it over the connector's session, and inspect the `sms` event at a client bound to Jasmin's
`smpps` - `sms.message === ''`.
**Severity.** Medium: silent data loss, not a wire error - the message is fully present on the wire
and Jasmin forwards it faithfully; only this library's read of it is incomplete.
### `sar_*`/UDH segmentation from an upstream SMSC reassembles fine (target 3, MO direction) - not reproduced as a defect here
C8's SAR and UDH MO pushes both reassembled into one whole `sms` at our real client. This does not
confirm target 3 is fixed in general (our own reassembly still keys on UDH only - a SAR-tagged
`deliver_sm` from Jasmin's connector would still arrive as an unrelated fragment if Jasmin ever
sent SAR MO unprompted), it confirms only that pushing SAR/UDH-tagged `deliver_sm`s ourselves,
directly over the connector session, reassembles correctly on the way out through `smpps` - Jasmin
does not re-segment or otherwise disturb an already-short single PDU in transit.
### `data_sm` is refused (target 4) - confirmed against a real peer
**What happened.** With `jasmin-datasm`'s `[dlr-thrower] dlr_pdu = data_sm`, a receipt requested via
`sendSms({ dlr: true, ... })` throws as a `data_sm` PDU on the client's bind, exactly as documented.
Our client's `incomingPduObj` sees it arrive; no `dlr` event ever fires. The receipt is lost -
`data_sm` falls to the generic unhandled-command path (`ESME_RINVCMDID`), matching the target.
**Reproducer.** `interop-tests/jasmin.test.ts`, `C9`: bind to `jasmin-datasm`, `sendSms({ dlr: true,
... })`, assert an incoming `data_sm` PDU arrives and no `dlr` event ever fires.
**Severity.** Medium: a real, documented Jasmin configuration (`dlr_pdu = data_sm`) that a receipt
depends on silently drops delivery reports, with no error surfaced to the application either.
### A multi-part MT send deadlocks against Jasmin's serialized per-connector relay - a library/peer interaction, not a wire defect
**What happened.** A 2-, 3-, 10-segment GSM or UCS-2 message sent through Jasmin's `smpps` (our real
client → Jasmin → `mtrouter` → the `upstream` connector → our fake upstream) never gets a receipt,
in every one of 3 runs, regardless of segment count or encoding - only the always-unsegmented
single-segment case succeeds. Jasmin's own `messages.log` shows why: `SubmitSmPDU[...] request
timed out through [cid:upstream], message requeued` after its 120s response timeout, and nothing
for the later segments at all (they stay queued behind the stuck one). The mechanism, confirmed by
dumping every `submit_sm` this library's `server()` received as Jasmin's fake upstream: segment 1
arrives intact (`esm_class: 64`, a correct `05 00 03 <ref> <total> <seq>` UDH, matching exactly what
`splitMessage()` itself would produce - Jasmin relays the UDH byte-for-byte) - but segment 2 never
arrives. `src/incoming-requests.ts`'s `onMessage()` answers nothing for an incomplete concatenation
group (`if (whole) this.emitSms(whole);` - a partial group falls through silently); `sms.sendResp()`
only exists once the whole group is in hand, so it answers all segments of a group atomically, in
one call, after the last one arrives. Jasmin, on its side, dispatches to a given connector one
`submit_sm` at a time, waiting for that one's response before sending the next queued message for
the same connector (confirmed indirectly: reordering the test file so `C13`'s 10 *independent*
single-segment sends and `S7`'s HTTP-send run *before* any multi-segment send turned both from
always-failing into always-passing at sub-second speed, while running them *after* a stuck
multi-segment send left them queued behind it for the same reason). Two designs that are each
individually reasonable - Jasmin never advances its connector queue past an unanswered request;
this library never answers part of a concatenated message - deadlock when composed: Jasmin will not
send segment 2 until segment 1 is acked, and this library will not ack segment 1 until segment 2
arrives.
Ruled out before landing on this: RabbitMQ/host CPU contention (real, but the failure is
deterministic across three runs regardless of load - not a timing flake); the connector's own
`submit_throughput` default of 1 msg/s (set to `0`, no change); the fake upstream's `idleTimeout`
dropping the connector mid-flight (widened to 300s, no change - the connector stayed bound
throughout, confirmed in its own log).
**Reproducer.** `interop-tests/jasmin.test.ts`, `C3+C7`, any multi-segment case, run after
`waitForUpstreamSession('main')`: `session.sendSms({ dlr: true, message: 'g'.repeat(200), from,
to })` against Jasmin's `smpps`, with a `server()` bound as the fake upstream that only calls
`sms.sendResp()`/`sendDlr()` once its own `'sms'` event fires. No receipt arrives within Jasmin's
own 120s response timeout; `messages.log` on the peer shows the requeue.
**Severity.** Medium, narrow: needs a real relaying gateway whose own dispatch is serialized
per-outbound-link, which is exactly Jasmin's `smppc` connector shape - a directly-connected SMSC
(every other peer in this suite) never exhibits it, since it always has a response ready for every
segment as they arrive rather than being a relay itself. Worth a decision record either way (answer
each segment as it's held, not only once the group completes; or document that a `server()` sitting
behind a serializing relay needs its own segment-level ack), but not fixed here per the phase rules.
## Peer quirks
- **Jasmin's own `enquireLinkTimerSecs` (30, `[smpp-server]` default) is an idle timer, not a strict
period.** Our client's own default 20s keepalive counts as activity and resets it, so Jasmin's own
probe never gets a chance to fire on a link that's never quiet for 30s. `C1` disables our own
keepalive (`enquireLinkInterval: 0`, widening `idleTimeout` to compensate) to observe it.
- **Message ids are UUIDs, self-consistent end to end when the upstream hands out one id per PDU.**
Every id the real client's `submit_sm_resp` carried matched exactly what the fake upstream's
`sendResp()` assigned; the later receipt named the same id. Since the fake upstream here is this
library's own `server()`, a multi-segment message's ids came back in this library's own
`<base>-<n>` shape - an artifact of the test rig (our own server reassembling before answering),
not evidence that Jasmin itself produces that convention; a real upstream SMSC would very likely
hand back unrelated ids per segment, as the research notes expected.
- **`smpps_throughput`, not the connector's `submit_throughput`, gates an ESME's own submission
rate.** The plan named `submit_throughput` "on the connector" for C12; that setting throttles the
connector's own *outbound* rate to its upstream. The knob that actually throttles submissions
*into* Jasmin's `smpps` from a bound ESME is a **user**-level quota
(`user -u <uid> mt_messaging_cred quota smpps_throughput <n>`), which does answer `ESME_RTHROTTLED`
reliably once exceeded.
- **`smppccm`'s `cid` must be 3-25 chars, `morouter`'s `smpps(<system_id>)` target is validated by
the same regex** (`[A-Za-z0-9_-]{3,25}`) - confirmed from `jasmin/protocols/cli/morouterm.py`.
- **`[dlr-thrower] dlr_pdu`** is a config-file setting (`deliver_sm` default, `data_sm` the other
option), not a per-connector or per-user key - the only way to test both is two Jasmin instances.
- **HTTP DLR callbacks carry fixed query args** (`id`, `level`, `message_status`, `connector`, plus
`id_smsc`/`sub`/`dlvrd`/`subdate`/`donedate`/`err`/`text` at `dlr-level` 2/3) appended by
`DLRThrower` itself to whatever bare URL `dlr-url` names - unlike Kannel's `%d`/`%F`
printf-style placeholders, nothing is written into the URL by the caller. `dlr-level=1` fires once,
immediately, with `message_status=ESME_ROK` (an SMSC-ack, not a terminal state); `dlr-level=3`
additionally fires once more, later, with the real terminal status (`DELIVRD` here).
- **jcli is a plain-text protocol dressed as Telnet** - it sends real `IAC`/option-negotiation bytes
and a couple of ANSI escapes in its banner, but never waits for or requires a reply to them; a raw
socket client that ignores negotiation entirely and just reads/writes lines works throughout.
## Open questions
- Whether Jasmin's own MT dispatch is serialized *per connector* specifically, or *globally* across
every connector on the instance - only one connector was configured, so the two are
indistinguishable here.
- Whether the deadlock above is specific to a message requesting a receipt (`registered_delivery`
set) or would also occur for a plain multi-segment send with no `dlr` - not isolated separately,
since every `C3+C7` case here requests one.
- Whether Jasmin, given an *upstream* connector that itself defaults to SAR (rather than our
library's own UDH), would relay an MT message using SAR instead of preserving our UDH bytes - the
120s-timeout deadlock always intervened before a second segment could be observed on the wire in
either direction.
+705
View File
@@ -0,0 +1,705 @@
import assert from 'node:assert/strict';
import http from 'node:http';
import test, { after, describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts';
import type { EncodingName } from '../src/defs/encodings.ts';
import type { PduObject } from '../src/pdu.ts';
import type { Session } from '../src/session.ts';
import type { Sms } from '../src/sms.ts';
import { ConcatReference } from '../src/udh.ts';
import { client } from '../src/client.ts';
import { closeAfter } from '../test/teardown.ts';
import { paramText } from '../src/defs/types.ts';
import { server } from '../src/server.ts';
import { splitMessage } from '../src/message.ts';
import { submitSmParams } from '../src/send-sms.ts';
const PEER_HOST = process.env.PEER_HOST ?? 'jasmin';
const PEER_PORT = Number(process.env.PEER_PORT ?? '2775');
const DATASM_HOST = process.env.DATASM_HOST ?? 'jasmin-datasm';
const HTTP_API_HOST = process.env.HTTP_API_HOST ?? 'jasmin';
const HTTP_API_PORT = Number(process.env.HTTP_API_PORT ?? '1401');
const UPSTREAM_PORT = Number(process.env.UPSTREAM_PORT ?? '2777');
const CALLBACK_PORT = Number(process.env.CALLBACK_PORT ?? '8080');
const USERNAME = 'esme1';
const PASSWORD = 'esme1pw';
const THROTTLED_USERNAME = 'esme2';
const THROTTLED_PASSWORD = 'esme2pw';
const UPSTREAM_USERNAME = 'upstreamesme';
const UPSTREAM_DATASM_USERNAME = 'upstreamdsesme';
const FROM = '46701113311';
const TO = '46709771337';
function delay(ms: number): Promise<void> {
return new Promise(resolve => { setTimeout(resolve, ms); });
}
/** Polls until `get()` stops returning undefined, or the budget runs out. */
async function waitFor<T>(get: () => T | undefined, budget = 8000): Promise<T | undefined> {
const deadline = Date.now() + budget;
let value = get();
while (value === undefined && Date.now() < deadline) {
await delay(20);
value = get();
}
return value;
}
async function bind(username: string, password: string, options: Parameters<typeof client>[0] = {}): ReturnType<typeof client> {
return client({ host: PEER_HOST, password, port: PEER_PORT, username, ...options });
}
// --- Shared infra: the fake upstream real-world SMSC that Jasmin's own smppc connector(s) bind
// out to (host `node`, port UPSTREAM_PORT - Jasmin resolves `node` via --use-aliases), and the HTTP
// listener that receives Jasmin's DLR-thrower webhook (S7). Both are wired once for the whole file,
// mirroring kannel.test.ts's shared-infra shape - every Jasmin variant dials in from container
// start, independent of when a given test runs. ---
type UpstreamVariant = 'datasm' | 'main';
const upstreamSessions = new Map<UpstreamVariant, Session>();
const upstreamSms: { sms: Sms; variant: UpstreamVariant }[] = [];
function variantFromSystemId(systemId: string): UpstreamVariant | undefined {
if (systemId === UPSTREAM_USERNAME) return 'main';
if (systemId === UPSTREAM_DATASM_USERNAME) return 'datasm';
return undefined;
}
const { err: upstreamErr, server: upstream } = await server({
authenticate: ({ password, systemId }) => {
// <=8 chars: Jasmin's own bind-PDU encoder enforces SMPP's 8-char password maximum strictly
// (see bootstrap.py) - a longer one made every single connector bind attempt throw.
if (password !== 'upstrmpw') return false;
const variant = variantFromSystemId(systemId);
return variant ? { userData: { variant } } : false;
},
// Jasmin's connector sends enquire_link every elink_interval (30s); this host's own contention
// (rabbitmq/redis under load) sometimes delays it past our server's 40s default idleTimeout,
// dropping the one long-lived connector session. A dropped connection strands whatever submit_sm
// was already in flight for Jasmin's own requeue_delay (120s default) before it retries - far past
// any per-test wait budget here - so this is generous specifically to never be the trigger.
idleTimeout: 300_000,
port: UPSTREAM_PORT,
});
assert.equal(upstreamErr, undefined);
assert.ok(upstream);
const upstreamServer = upstream;
upstreamServer.on('session', session => {
// `session` fires on raw connect, before authenticate() has run - session.userData is not set
// yet, so the map is populated off the bind PDU itself (like kannel.test.ts's bindPdus), not off
// userData; userData is only read later, from 'sms', where authenticate() has long since run.
session.on('incomingPduObj', pduObj => {
if (!pduObj.cmdName.startsWith('bind_')) return;
const variant = variantFromSystemId(paramText(pduObj.params.system_id));
if (variant) upstreamSessions.set(variant, session);
});
session.on('sms', sms => {
const variant = (session.userData as { variant?: UpstreamVariant } | undefined)?.variant;
if (variant) upstreamSms.push({ sms, variant });
void (async () => {
await sms.sendResp();
if (sms.dlr) {
await delay(150);
await sms.sendDlr('DELIVERED');
}
})();
});
});
async function waitForUpstreamSession(variant: UpstreamVariant, budget = 20_000): Promise<Session> {
const found = await waitFor(() => upstreamSessions.get(variant), budget);
assert.ok(found, `Jasmin's ${variant} connector never bound to our fake upstream within ${String(budget)}ms`);
return found;
}
type DlrCallback = { id: string; messageStatus: string };
const dlrCallbacks: DlrCallback[] = [];
const httpServer = http.createServer((req, res) => {
const url = new URL(req.url ?? '/', 'http://node');
if (url.pathname === '/dlr') {
dlrCallbacks.push({
id: url.searchParams.get('id') ?? '',
messageStatus: url.searchParams.get('message_status') ?? '',
});
res.writeHead(200);
res.end();
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>(resolve => { httpServer.listen(CALLBACK_PORT, resolve); });
after(async () => {
await upstreamServer.close();
await new Promise<void>(resolve => { httpServer.close(() => { resolve(); }); });
});
/** dlr-level 1 (SMSC-ack) fires once, immediately, with message_status ESME_ROK - not a terminal
* state - so a caller after the final DELIVRD needs dlr-level 3 (both) and its own status filter. */
async function waitForDlrCallback(id: string, status: string, budget = 15_000): Promise<DlrCallback> {
const found = await waitFor(() => dlrCallbacks.find(callback => callback.id === id && callback.messageStatus === status), budget);
assert.ok(found, `no dlr callback for id=${id} status=${status} arrived (seen: ${JSON.stringify(dlrCallbacks)})`);
return found;
}
/** Jasmin's HTTP send API (`/send`): `to` must be digits only, `content` is the message body. */
async function httpSend(params: Record<string, string>): Promise<{ body: string; status: number }> {
const url = new URL(`http://${HTTP_API_HOST}:${String(HTTP_API_PORT)}/send`);
url.search = new URLSearchParams({ password: PASSWORD, username: USERNAME, ...params }).toString();
const response = await fetch(url, { method: 'POST' });
const body = await response.text();
return { body, status: response.status };
}
const sarReference = new ConcatReference();
/** Splits into SAR segments: `splitMessage()`'s own encoding/chunking, its UDH stripped back off. */
function sarPayloads(message: string, encoding?: EncodingName): Buffer[] {
const reference = sarReference.next();
const segments = splitMessage(message, encoding === undefined ? { reference } : { encoding, reference });
return segments.length > 1 ? segments.map(segment => segment.subarray(6)) : segments;
}
/** Pushes a long MO into Jasmin over `session` (Jasmin's smppc connector bound to us) as
* `sar_msg_ref_num`/`sar_total_segments`/`sar_segment_seqnum` segments - Jasmin's own documented
* default MT segmentation (target 3). */
async function sendSarMo(session: Session, opts: { from: string; message: string; to: string }): Promise<void> {
const payloads = sarPayloads(opts.message);
const refNum = sarReference.next();
for (const [index, payload] of payloads.entries()) {
const sent = await session.send({
cmdName: 'deliver_sm',
params: {
destination_addr: opts.to,
short_message: payload,
source_addr: opts.from,
},
tlvs: {
sar_msg_ref_num: { tagValue: refNum },
sar_segment_seqnum: { tagValue: index + 1 },
sar_total_segments: { tagValue: payloads.length },
},
});
assert.equal(sent.err, undefined);
assert.ok(sent.pduObj);
assert.equal(sent.pduObj.cmdStatus, 'ESME_ROK');
}
}
/** As `sendSarMo`, but UDH concatenation (esm_class 0x40) - the same shape `sendSms()` emits, and
* Jasmin's documented "older system compatibility" alternative. */
async function sendUdhMo(session: Session, opts: { from: string; message: string; to: string }): Promise<void> {
const reference = sarReference.next();
const segments = splitMessage(opts.message, { reference });
const multipart = segments.length > 1;
for (const segment of segments) {
const params = submitSmParams({ from: opts.from, message: opts.message, to: opts.to }, segment, { encoding: 'ASCII', multipart });
const sent = await session.send({ cmdName: 'deliver_sm', params });
assert.equal(sent.err, undefined);
assert.ok(sent.pduObj);
assert.equal(sent.pduObj.cmdStatus, 'ESME_ROK');
}
}
/** A `deliver_sm` with `sm_length` 0 and the body in `message_payload` (target 2). */
async function sendMessagePayloadMo(session: Session, opts: { from: string; message: string; to: string }): ReturnType<Session['send']> {
return session.send({
cmdName: 'deliver_sm',
params: {
destination_addr: opts.to,
short_message: Buffer.alloc(0),
source_addr: opts.from,
},
tlvs: {
message_payload: { tagValue: Buffer.from(opts.message, 'latin1') },
},
});
}
describe('C1 - bind, enquire_link, unbind', () => {
test('binds transceiver, sees Jasmin\'s own enquire_link, unbinds clean', async () => {
// Jasmin's own enquireLinkTimerSecs (30) is an idle timer, not a strict period: our client's
// own default 20s keepalive counts as activity and resets it, so Jasmin's probe never has a
// chance to fire on its own. Disabling ours (and widening idleTimeout, which defaults off
// enquireLinkInterval and would otherwise become 0) leaves the link quiet long enough to see it.
const { err, session } = await bind(USERNAME, PASSWORD, { enquireLinkInterval: 0, idleTimeout: 60_000 });
assert.equal(err, undefined);
assert.ok(session);
const incoming: PduObject[] = [];
const closes: true[] = [];
const sessionErrors: Error[] = [];
session.on('incomingPduObj', pduObj => { incoming.push(pduObj); });
session.on('close', () => { closes.push(true); });
session.on('sessionError', sessionError => { sessionErrors.push(sessionError); });
// enquireLinkTimerSecs is 30 in Jasmin's default [smpp-server] config.
const theirs = await waitFor(() => incoming.find(pduObj => pduObj.cmdName === 'enquire_link'), 35_000);
assert.ok(theirs, 'expected Jasmin to send its own enquire_link within 35s');
const ours = await session.send({ cmdName: 'enquire_link' });
assert.equal(ours.err, undefined);
assert.ok(ours.pduObj);
assert.equal(ours.pduObj.cmdStatus, 'ESME_ROK');
const unbound = await session.unbind();
assert.equal(unbound.err, undefined);
assert.ok(await waitFor(() => (closes.length > 0 ? true : undefined), 5000), 'expected a clean close after unbind');
assert.deepEqual(sessionErrors, []);
});
test('binds transmitter', async t => {
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'transmitter' });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
assert.equal(session.boundAs, 'transmitter');
});
test('binds receiver', async t => {
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
assert.equal(session.boundAs, 'receiver');
});
});
describe('C13 - maxOutstanding 1 with 10 parallel sends', () => {
test('every send is answered, none lost, order preserved at the fake upstream', async t => {
await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { maxOutstanding: 1 });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const before = upstreamSms.length;
const texts = Array.from({ length: 10 }, (_, i) => `c13-order-${String(i).padStart(2, '0')}`);
const results = await Promise.all(texts.map(async message => session.sendSms({ from: FROM, message, to: TO })));
const ids: string[] = [];
for (const result of results) {
assert.equal(result.err, undefined);
assert.equal(result.smsIds.length, 1);
const [smsId] = result.smsIds;
assert.ok(smsId);
ids.push(smsId);
}
assert.equal(new Set(ids).size, ids.length, 'expected 10 distinct message ids, none lost or duplicated');
const arrivedOrder = await waitFor(() => {
const seen = upstreamSms.slice(before).filter(e => e.variant === 'main').map(e => e.sms.message);
return texts.every(text => seen.includes(text)) ? seen : undefined;
}, 60_000);
assert.ok(arrivedOrder, 'not all 10 messages reached the fake upstream');
const ordered = arrivedOrder.filter(m => texts.includes(m));
assert.deepEqual(ordered, texts, 'maxOutstanding:1 should serialise sends end to end, in order');
});
});
describe('S7 - Jasmin as the ESME against our server (HTTP send API, DLR callback)', () => {
test('a message pushed through /send arrives as submit_sm at our server; our receipt fires Jasmin\'s DLR webhook', async () => {
const before = upstreamSms.length;
// No printf-style placeholders (unlike Kannel's %d/%F): DLRThrower appends its own fixed
// query args - id, level, message_status, connector - to this bare URL (confirmed from
// jasmin/routing/throwers.py). dlr-method=get puts them in the query string; POST (Jasmin's
// own default) would need a form-body reader instead.
const dlrUrl = `http://node:${String(CALLBACK_PORT)}/dlr`;
const sent = await httpSend({
content: 's7 http send test',
dlr: 'yes',
// Level 3 (both): level 1 alone fires once, immediately, with message_status ESME_ROK -
// the SMSC-ack, not a terminal state - so proving Jasmin parses our own receipt needs the
// terminal-level callback too, which only follows the sendDlr('DELIVERED') below.
'dlr-level': '3',
'dlr-method': 'get',
'dlr-url': dlrUrl,
from: FROM,
to: TO,
});
assert.match(sent.body, /Success/i);
const arrived = await waitFor(() => upstreamSms.slice(before).find(e => e.variant === 'main' && e.sms.message === 's7 http send test'), 60_000);
assert.ok(arrived, 'expected the HTTP-submitted message to arrive as submit_sm at our server()');
// Our server() already answered (sendResp) and sent the DLR (sendDlr('DELIVERED')) from the
// shared session handler above - Jasmin's own DLR pipeline should throw the HTTP callback.
const msgidMatch = /Success "([^"]+)"/i.exec(sent.body);
const msgid = msgidMatch?.[1];
assert.ok(msgid, `expected /send's response to carry a message id: ${sent.body}`);
const ack = await waitForDlrCallback(msgid, 'ESME_ROK', 15_000);
assert.equal(ack.id, msgid);
const final = await waitForDlrCallback(msgid, 'DELIVRD', 15_000);
assert.equal(final.id, msgid);
});
test('a long GSM message from our server reassembles at Jasmin (or is recorded as fragments)', async () => {
const upstreamSession = await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
const sms: Sms[] = [];
session.on('sms', s => { sms.push(s); });
const text = `s7-long-${'p'.repeat(300)}`;
await sendUdhMo(upstreamSession, { from: TO, message: text, to: FROM });
const whole = await waitFor(() => sms.find(s => s.message === text), 10_000);
await session.close({ signal: AbortSignal.abort() });
assert.ok(whole ?? sms.length > 0, 'expected the long message to arrive whole or as recorded fragments');
});
test('a UCS-2 message with 一 and an emoji from our server (or is recorded as fragments)', async () => {
const upstreamSession = await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
const sms: Sms[] = [];
session.on('sms', s => { sms.push(s); });
const text = `一😀${'q'.repeat(60)}`;
await sendSarMo(upstreamSession, { from: TO, message: text, to: FROM });
const whole = await waitFor(() => sms.find(s => s.message === text), 10_000);
await session.close({ signal: AbortSignal.abort() });
assert.ok(whole ?? sms.length > 0, 'expected the UCS-2 message to arrive whole or as recorded fragments');
});
});
describe('C3+C7 - long MT through the fake upstream, receipts and id consistency', () => {
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(-\d+)?$/i;
const cases: { encoding?: 'UCS2'; expectedSegments: number; label: string; message: string }[] = [
{ expectedSegments: 1, label: 'single-segment GSM with extension chars', message: '€[]~single segment' },
{ expectedSegments: 2, label: '2-segment GSM with extension chars', message: `€[]~${'g'.repeat(200)}` },
{ expectedSegments: 3, label: '3-segment GSM with extension chars', message: `€[]~${'g'.repeat(400)}` },
{ expectedSegments: 10, label: '10-segment GSM with extension chars', message: `€[]~${'g'.repeat(1450)}` },
{ encoding: 'UCS2', expectedSegments: 2, label: '2-segment UCS2 with 一 and an emoji', message: `一😀${'x'.repeat(70)}` },
];
for (const testCase of cases) {
test(testCase.label, async t => {
await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD);
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const dlrs: { dlr: Dlr; pduObj: PduObject }[] = [];
const messageDlrs: unknown[] = [];
session.on('dlr', (dlr, pduObj) => { dlrs.push({ dlr, pduObj }); });
session.on('messageDlr', merged => { messageDlrs.push(merged); });
const sent = await session.sendSms({
dlr: true,
from: FROM,
message: testCase.message,
to: TO,
...(testCase.encoding ? { encoding: testCase.encoding } : {}),
});
assert.equal(sent.err, undefined);
assert.equal(sent.smsIds.length, testCase.expectedSegments);
for (const id of sent.smsIds) {
assert.match(id, uuidPattern, 'expected a UUID-shaped message id from Jasmin\'s submit_sm_resp');
// The full round trip - submit_sm to Jasmin's smpps, mtrouter, AMQP, the connector bind,
// our fake upstream's ack+DLR, AMQP again, DLRLookup, deliver_sm back - is slower than a
// single-segment send's, and visibly so under load; a generous budget beats a flaky one.
const received = await waitFor(() => dlrs.find(r => r.dlr.smsId === id), 25_000);
assert.ok(received, `no receipt for id ${id}`);
assert.equal(received.dlr.statusMsg, 'DELIVERED');
}
// Jasmin relays each segment as its own independent submit_sm to the connector (no MT-side
// UDH reassembly observed here - see findings), so the ids Jasmin hands back are whatever
// the fake upstream's own server() assigned per segment of ITS OWN reassembled view. That
// is this library's own <base>-<n> convention on both ends of this harness, which is why
// messageDlr can fire here - not evidence of Jasmin producing that convention itself; see
// findings/03-jasmin.md for what a real, independent upstream SMSC would hand back instead.
void messageDlrs;
});
}
});
describe('C8 (target 3) - long MO from an upstream SMSC, SAR vs UDH segmentation', () => {
test('SAR-segmented deliver_sm from the fake upstream', async () => {
const upstreamSession = await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
const sms: Sms[] = [];
session.on('sms', s => { sms.push(s); });
const text = `sar-mo-${'m'.repeat(300)}`;
await sendSarMo(upstreamSession, { from: TO, message: text, to: FROM });
// Recorded either way, per the task: one whole `sms` (Jasmin reassembled the SAR segments
// before forwarding) or several fragments (it relayed them, and our SAR-blind reassembler -
// keyed on UDH only - never groups them; see findings for which happened and the reproducer).
const whole = await waitFor(() => sms.find(s => s.message === text), 10_000);
const fragments = sms.filter(s => text.includes(s.message) && s.message !== '');
await session.close({ signal: AbortSignal.abort() });
assert.ok(whole ?? fragments.length > 0, 'expected either a reassembled sms or SAR fragments to arrive');
});
test('UDH-segmented deliver_sm from the fake upstream', async () => {
const upstreamSession = await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
const sms: Sms[] = [];
session.on('sms', s => { sms.push(s); });
const text = `udh-mo-${'n'.repeat(300)}`;
await sendUdhMo(upstreamSession, { from: TO, message: text, to: FROM });
const whole = await waitFor(() => sms.find(s => s.message === text), 10_000);
const fragments = sms.filter(s => text.includes(s.message) && s.message !== '');
await session.close({ signal: AbortSignal.abort() });
assert.ok(whole ?? fragments.length > 0, 'expected either a reassembled sms or UDH fragments to arrive');
});
});
describe('C8 (target 2) - message_payload with sm_length 0', () => {
test('a deliver_sm carrying message_payload instead of short_message', async () => {
const upstreamSession = await waitForUpstreamSession('main');
const { err, session } = await bind(USERNAME, PASSWORD, { bindType: 'receiver' });
assert.equal(err, undefined);
assert.ok(session);
const sms: Sms[] = [];
session.on('sms', s => { sms.push(s); });
const text = 'message-payload only, sm_length 0';
const pushed = await sendMessagePayloadMo(upstreamSession, { from: TO, message: text, to: FROM });
assert.equal(pushed.err, undefined, 'expected Jasmin to accept a message_payload-only deliver_sm from its connector');
const arrived = await waitFor(() => sms.find(s => s.message === ''), 5000);
await session.close({ signal: AbortSignal.abort() });
// Confirmed (target 2): Jasmin relays message_payload faithfully (sm_length 0, the real text
// in the TLV) - our own incoming-requests.ts reads short_message only, so the sms that arrives
// here has the right envelope (from/to) but an empty message, never the text carried in the
// TLV. See findings/03-jasmin.md for the reproducer.
assert.ok(arrived, 'expected an sms to arrive (with an empty message, per target 2) for the message_payload push');
});
});
describe('C9 (target 4) - DLR as data_sm against the jasmin-datasm instance', () => {
test('a receipt thrown as data_sm is not read as a dlr; the raw PDU still arrives', async t => {
await waitForUpstreamSession('datasm');
const { err, session } = await client({ host: DATASM_HOST, password: PASSWORD, port: PEER_PORT, username: USERNAME });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const dlrs: Dlr[] = [];
const incomingDataSm: PduObject[] = [];
session.on('dlr', dlr => { dlrs.push(dlr); });
session.on('incomingPduObj', pduObj => { if (pduObj.cmdName === 'data_sm') incomingDataSm.push(pduObj); });
const sent = await session.sendSms({ dlr: true, from: FROM, message: 'data_sm dlr test', to: TO });
assert.equal(sent.err, undefined);
const arrived = await waitFor(() => incomingDataSm[0], 15_000);
assert.ok(arrived, 'expected Jasmin to throw the receipt as data_sm (dlr_pdu = data_sm)');
await delay(2000);
assert.deepEqual(dlrs, [], 'data_sm is answered ESME_RINVCMDID and never reaches the dlr event (target 4)');
});
});
describe('C11 - bind refusal and reconnect backoff', () => {
test('wrong password: one attempt, no retry', async () => {
const refusals: { cmdStatus: unknown }[] = [];
const log = {
debug: () => undefined,
error: () => undefined,
info: (msg: string, metadata?: Record<string, boolean | number | string>) => {
if (msg === 'client - bind refused') refusals.push({ cmdStatus: metadata?.cmdStatus });
},
verbose: () => undefined,
warn: () => undefined,
};
const { err, session } = await bind(USERNAME, 'wrong-password', { log, reconnect: { maxDelay: 4000, minDelay: 1000 } });
assert.ok(err);
assert.equal(session, undefined);
await delay(2000);
assert.equal(refusals.length, 1, 'expected exactly one bind attempt, never a retry');
assert.equal(refusals[0]?.cmdStatus, 'ESME_RINVPASWD');
});
test('a rebind refused after a live link drops: backs off, never floods', async t => {
const options: Parameters<typeof client>[0] = {
host: PEER_HOST,
password: PASSWORD,
port: PEER_PORT,
reconnect: { maxDelay: 4000, minDelay: 1000 },
username: USERNAME,
};
const { err, session } = await client(options);
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const disconnectedAt: number[] = [];
session.on('disconnected', () => { disconnectedAt.push(Date.now()); });
options.password = 'wrong-after-drop';
session.sock.destroy();
await delay(12_000);
assert.ok(disconnectedAt.length >= 2 && disconnectedAt.length <= 8, `expected a handful of attempts, got ${String(disconnectedAt.length)}`);
for (let i = 1; i < disconnectedAt.length; i++) {
const previous = disconnectedAt[i - 1];
const current = disconnectedAt[i];
assert.ok(previous !== undefined && current !== undefined);
assert.ok(current - previous >= 150, 'expected each retry to wait at least close to minDelay');
}
});
});
describe('C12 - throttling (esme2\'s smpps_throughput quota)', () => {
test('flooding submits past the quota gets an err naming the status; the session stays bound; a later send works', async t => {
await waitForUpstreamSession('main');
const { err, session } = await bind(THROTTLED_USERNAME, THROTTLED_PASSWORD, { maxOutstanding: 20 });
assert.equal(err, undefined);
assert.ok(session);
closeAfter(t, session);
const results = await Promise.all(
Array.from({ length: 15 }, async (_unused, index) => session.sendSms({ from: FROM, message: `throttle-${String(index)}`, to: TO })),
);
const refused = results.filter(r => r.err !== undefined);
assert.ok(refused.length > 0, 'expected the 0.1/s quota to refuse at least one of 15 parallel sends');
assert.match(refused[0]?.err?.message ?? '', /ESME_RTHROTTLED/);
const keepalive = await session.send({ cmdName: 'enquire_link' });
assert.equal(keepalive.err, undefined);
assert.ok(keepalive.pduObj);
assert.equal(keepalive.pduObj.cmdStatus, 'ESME_ROK');
// 0.1/s is one slot every 10s, so a single fixed delay is either wasteful or flaky - polling
// finds the next open slot instead of guessing it.
const deadline = Date.now() + 25_000;
let later: Awaited<ReturnType<typeof session.sendSms>> | undefined;
while (!later && Date.now() < deadline) {
const attempt = await session.sendSms({ from: FROM, message: 'after the burst', to: TO });
if (!attempt.err) later = attempt;
else await delay(500);
}
assert.ok(later, 'expected a later send to succeed once the quota\'s next slot opened');
});
});
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Boot-time jcli bootstrap for one Jasmin instance: group, users, an smppc connector pointing at
our own server(), and the MT/MO routes that wire it up. jcli (port 8990) is a Twisted telnet
console: no negotiation reply is needed, the server proceeds regardless (confirmed empirically)."""
import os
import socket
import sys
import time
JCLI_HOST = os.environ.get('JCLI_HOST', 'jasmin')
JCLI_PORT = int(os.environ.get('JCLI_PORT', '8990'))
JCLI_USER = os.environ.get('JCLI_USER', 'jcliadmin')
JCLI_PASS = os.environ.get('JCLI_PASS', 'jclipwd')
GROUP = os.environ.get('GROUP', 'clients')
ESME_UID = os.environ.get('ESME_UID', 'esme1')
ESME_PASSWORD = os.environ.get('ESME_PASSWORD', 'esme1pw')
ESME2_UID = os.environ.get('ESME2_UID', 'esme2')
ESME2_PASSWORD = os.environ.get('ESME2_PASSWORD', 'esme2pw')
ESME2_THROUGHPUT = os.environ.get('ESME2_THROUGHPUT', '0.1')
CONNECTOR_CID = os.environ.get('CONNECTOR_CID', 'upstream')
CONNECTOR_HOST = os.environ.get('CONNECTOR_HOST', 'node')
CONNECTOR_PORT = os.environ.get('CONNECTOR_PORT', '2777')
CONNECTOR_USERNAME = os.environ.get('CONNECTOR_USERNAME', 'upstreamesme')
# <=8 chars: SMPP's password is a C-octet-string with an 8-char + NUL wire maximum, which Jasmin's
# own smpp.pdu encoder enforces strictly when it builds the connector's own bind PDU (unlike our
# library's encoder, which is permissive) - a longer one throws mid-bind on every single attempt.
CONNECTOR_PASSWORD = os.environ.get('CONNECTOR_PASSWORD', 'upstrmpw')
class Jcli:
def __init__(self, host, port):
last_err = None
for _ in range(60):
try:
self.sock = socket.create_connection((host, port), timeout=5)
self.sock.settimeout(5)
self._drain()
return
except OSError as err:
last_err = err
time.sleep(1)
raise RuntimeError(f'could not reach jcli at {host}:{port}: {last_err}')
def _drain(self, wait=0.4):
time.sleep(wait)
buf = b''
try:
while True:
chunk = self.sock.recv(65536)
if not chunk:
break
buf += chunk
except socket.timeout:
pass
return buf
def send(self, line, wait=0.4):
self.sock.sendall(line.encode() + b'\r\n')
return self._drain(wait).decode(errors='replace')
def login(self, user, password):
self._drain()
self.send(user)
reply = self.send(password)
if 'Welcome to Jasmin' not in reply:
raise RuntimeError(f'jcli login failed: {reply!r}')
def run(self, *lines, label=''):
"""Sends a sequence ending in 'ok' and fails loudly if Jasmin refused it. Checking only for
keywords missed 'Failed adding connector, check log for details' once, which left the
session stuck at the '>' sub-prompt and every later command misread as a key inside it - so
this also insists the last reply lands back on the top-level 'jcli :' prompt."""
out = []
for line in lines:
out.append(self.send(line))
joined = '\n'.join(out)
last = out[-1] if out else ''
back_at_top = last.rstrip().endswith('jcli :')
keyword_hit = any(word in joined.lower() for word in ('error', 'must set', 'unknown', 'invalid', 'failed'))
if keyword_hit or not back_at_top:
raise RuntimeError(f'{label} failed (last reply {last!r}):\n{joined}')
return joined
def main() -> int:
jcli = Jcli(JCLI_HOST, JCLI_PORT)
jcli.login(JCLI_USER, JCLI_PASS)
print(jcli.run('group -a', f'gid {GROUP}', 'ok', label='group'))
print(jcli.run(
'user -a', f'uid {ESME_UID}', f'gid {GROUP}', f'username {ESME_UID}', f'password {ESME_PASSWORD}', 'ok',
label='user esme1',
))
print(jcli.run(
'user -a', f'uid {ESME2_UID}', f'gid {GROUP}', f'username {ESME2_UID}', f'password {ESME2_PASSWORD}', 'ok',
label='user esme2',
))
print(jcli.run(
f'user -u {ESME2_UID}', f'mt_messaging_cred quota smpps_throughput {ESME2_THROUGHPUT}', 'ok',
label='user esme2 throughput quota',
))
print(jcli.run(
'smppccm -a',
f'cid {CONNECTOR_CID}',
f'host {CONNECTOR_HOST}',
f'port {CONNECTOR_PORT}',
f'username {CONNECTOR_USERNAME}',
f'password {CONNECTOR_PASSWORD}',
'bind transceiver',
'con_fail_delay 2',
'con_loss_delay 2',
# The connector's own default (1) is 1 msg/s - a second submit while the first is still
# in flight (a message's 2nd segment, or a 2nd message sent right after) then never reaches
# the connector's peer at all inside any sane test budget; see findings/03-jasmin.md.
'submit_throughput 0',
'ok',
label='smppccm',
))
started = jcli.send(f'smppccm -1 {CONNECTOR_CID}')
print(started)
if 'Successfully started' not in started:
raise RuntimeError(f'smppccm -1 {CONNECTOR_CID} failed: {started!r}')
print(jcli.run(
'mtrouter -a', 'type DefaultRoute', 'order 0', f'connector smppc({CONNECTOR_CID})', 'rate 0.0', 'ok',
label='mtrouter',
))
print(jcli.run(
'morouter -a', 'type DefaultRoute', 'order 0', f'connector smpps({ESME_UID})', 'ok',
label='morouter',
))
jcli.send('persist')
jcli.send('quit', wait=0.2)
print('jasmin bootstrap complete')
return 0
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,654 @@
#
# This is the main Jasmin SMS gateway configuration file.
# For any modifications to this file, refer to Jasmin Documentation.
# If that does not help, post your question on Jasmin's web forum
# hosted at Google Groups: https://groups.google.com/group/jasmin-sms-gateway
#
# Do NOT simply read the instructions in here without understanding
# what they do. They're here only as hints or reminders. If you are unsure
# consult the online docs.
[smpp-server]
# SMPP Server identifier
#id = "smpps_01"
# If you want you can bind a single interface, you can specify its IP here
#bind = 0.0.0.0
# Accept connections on the specified port, default is 2775
#port = 2775
# Activate billing feature
# May be disabled if not needed/used
#billing_feature = True
# Timeout for response to bind request
#sessionInitTimerSecs = 30
# Enquire link interval
#enquireLinkTimerSecs = 30
# Maximum time lapse allowed between transactions, after which,
# the connection is considered as inactive
#inactivityTimerSecs = 300
# Timeout for responses to any request PDU
#responseTimerSecs = 60
# Timeout for reading a single PDU, this is the maximum lapse of time between
# receiving PDU's header and its complete read, if the PDU reading timed out,
# the connection is considered as 'corrupt' and will reconnect
#pduReadTimerSecs = 10
# When message is routed to a SMPP Client connecter: How much time it is kept in
# redis waiting for receipt
#dlr_expiry = 86400
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/default-smpps_01.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = midnight
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
#log_privacy = False
[smpp-server-pb]
# If you want you can bind a single interface, you can specify its IP here
#bind = 0.0.0.0
# Accept connections on the specified port, default is 14000
#port = 14000
# If authentication is True, access will require entering a username and password
# as defined in admin_username and admin_password, you can disable this security
# layer by setting authentication to False, in this case admin_* values are ignored.
#authentication = True
#admin_username = smppsadmin
# This is a MD5 password digest hex encoded
#admin_password = e97ab122faa16beea8682d84f3d2eea4
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/smpp-server-pb.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[client-management]
# Jasmin persists its configuration profiles in /etc/jasmin/store by
# default. You can specify a custom location here
#store_path = /etc/jasmin/store
# If you want you can bind a single interface, you can specify its IP here
#bind = 0.0.0.0
# Accept connections on the specified port, default is 8989
#port = 8989
# If authentication is True, access will require entering a username and password
# as defined in admin_username and admin_password, you can disable this security
# layer by setting authentication to False, in this case admin_* values are ignored.
#authentication = True
#admin_username = cmadmin
# This is a MD5 password digest hex encoded
#admin_password = e1c5136acafb7016bc965597c992eb82
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/smppclient-manager.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
# The protocol version used to pickle objects before transfering
# them to client side, this is used in the client manager only,
# the pickle protocol defined in SMPPClientManagerPBProxy is set
# to 2 and is not configurable
#pickle_protocol = 2
[service-smppclient]
# For each smppclient connector a service is associated
# refer to "Message flows" documentation for more details
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/service-smppclients.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[sm-listener]
# SM listener consumes submit_sm and deliver_sm messages from amqp broker
# refer to "Message flows" documentation for more details
# If publish_submit_sm_resp is True, any received SubmitSm PDU will be published
# to the 'messaging' exchange on 'submit.sm.resp.CID' route, useful when you have
# a third party application waiting for these messages.
#publish_submit_sm_resp = False
# If the error is defined in submit_error_retrial, Jasmin will retry sending submit_sm if it
# gets one of these errors.
# submit_sm retrial will be executed 'count' times and delayed for 'delay' seconds each time.
#submit_error_retrial = {
# 'ESME_RSYSERR': {'count': 2, 'delay': 30},
# 'ESME_RTHROTTLED': {'count': 20, 'delay': 30},
# 'ESME_RMSGQFUL': {'count': 2, 'delay': 180},
# 'ESME_RINVSCHED': {'count': 2, 'delay': 300},
# }
# The maximum number of seconds a message can stay in queue waiting for SMPPC to get ready for
# delivey (connected and bound).
#submit_max_age_smppc_not_ready = 1200
# Delay (seconds) when retrying a submit with a not-yet ready SMPPc
# Hint: for large scale messaging deployment, it is advised to set this value to few seconds
# in order to keep Jasmin free.
#submit_retrial_delay_smppc_not_ready = 30
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/messages.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = midnight
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
#log_privacy = False
[dlr]
# DLRLookup process id
#pid = main
# DLRLookup mechanism configuration
#dlr_lookup_retry_delay = 10
#dlr_lookup_max_retries = 2
# If smpp_receipt_on_success_submit_sm_resp is True, every connected user to smpp server will
# receive a receipt (data_sm or deliver_sm) whenever a submit_sm_resp is received
# for a message he sent and requested receipt for it.
#smpp_receipt_on_success_submit_sm_resp = False
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/messages.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = midnight
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
#log_privacy = False
[amqp-broker]
host=rabbitmq
port=5672
# The following directives define the way how Jasmin is connecting to the AMQP Broker,
# default values must work with a freshly installed RabbitMQ server.
#host = 127.0.0.1
#vhost = /
#spec = /etc/jasmin/resource/amqp0-9-1.xml
#port = 5672
#username = guest
#password = guest
#heartbeat = 0
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/amqp-client.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
#connection_loss_retry = True
#connection_failure_retry = True
#connection_loss_retry_delay = 10
#connection_loss_failure_delay = 10
[http-api]
# If you want you can bind a single interface, you can specify its IP here
#bind = 0.0.0.0
# Accept connections on the specified port, default is 1401
#port = 1401
# Activate billing feature
# May be disabled if not needed/used
#billing_feature = True
# How many message parts you can get for a long message, default is 5 so you
# can't exceed 800 characters (160x5) when sending a long latin message.
#long_content_max_parts = 5
# Splitting long content can be made through SAR options or UDH
# Possible values are: sar and udh
#long_content_split = udh
# Specify the access log file path
#access_log = /var/log/jasmin/http-access.log
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/http-api.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
#log_privacy = False
[router]
# Jasmin router persists its routing configuration profiles in /etc/jasmin/store by
# default. You can specify a custom location here
#store_path = /etc/jasmin/store
# Router will automatically persist users and groups to disk whenever a critical information
# is updated (ex: user balance), persistence is executed every persistence_timer_secs
#persistence_timer_secs = 60
# If you want you can bind a single interface, you can specify its IP here
#bind = 0.0.0.0
# Accept connections on the specified port, default is 8988
#port = 8988
# If authentication is True, access will require entering a username and password
# as defined in admin_username and admin_password, you can disable this security
# layer by setting authentication to False, in this case admin_* values are ignored.
#authentication = True
#admin_username = radmin
# This is a MD5 password digest hex encoded
#admin_password = 82a606ca5a0deea2b5777756788af5c8
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/router.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
# The protocol version used to pickle objects before transfering
# them to client side, this is used in the client manager only,
# the pickle protocol defined in SMPPClientManagerPBProxy is set
# to 2 and is not configurable
#pickle_protocol = 2
[deliversm-thrower]
# The following directives define the process of delivery SMS-MO through http to third party
# application, it is explained in "HTTP API" documentation
# Sets socket timeout in seconds for outgoing client http connections.
#http_timeout = 30
# Define how many seconds should pass within the queuing system for retrying a failed throw.
#retry_delay = 30
# Define how many retries should be performed for failing throws of SMS-MO.
#max_retries = 3
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/deliversm-thrower.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[dlr-thrower]
# The following directives define the process of delivering delivery-receipts through http to third party
# application, it is explained in "HTTP API" documentation
# Sets socket timeout in seconds for outgoing client http connections.
#http_timeout = 30
# Define how many seconds should pass within the queuing system for retrying a failed throw.
#retry_delay = 30
# Define how many retries should be performed for failing throws of DLR.
#max_retries = 3
# Specify the pdu type to consider when throwing a receipt through SMPPs, possible values:
# - data_sm
# - deliver_sm (default pdu)
dlr_pdu = data_sm
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/dlr-thrower.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[redis-client]
host=redis
port=6379
# The following directives define the way how Jasmin is connecting to the redis server,
# default values must work with a freshly installed redis server.
#host = 127.0.0.1
#port = 6379
#dbid = 0
#password = None
#poolsize = 10
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/redis-client.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[jcli]
bind=0.0.0.0
# If you want you can bind a single interface, you can specify its IP here
#bind = 127.0.0.1
# Accept connections on the specified port, default is 8990
#port = 8990
# If authentication is True, access will require entering a username and password
# as defined in admin_username and admin_password, you can disable this security
# layer by setting authentication to False, in this case admin_* values are ignored.
#authentication = True
#admin_username = jcliadmin
# This is a MD5 password digest hex encoded
#admin_password = 79e9b0aa3f3e7c53e916f7ac47439bcb
# Specify the server verbosity level.
# This can be one of:
# NOTSET (disable logging)
# DEBUG (a lot of information, useful for development/testing)
# INFO (moderately verbose, what you want in production probably)
# WARNING (only very important / critical messages and errors are logged)
# ERROR (only errors / critical messages are logged)
# CRITICAL (only critical messages are logged)
#log_level = INFO
# Specify the log file path
#log_file = /var/log/jasmin/jcli.log
# When to rotate the log file, possible values:
# S: Seconds
# M: Minutes
# H: Hours
# D: Days
# W0-W6: Weekday (0=Monday)
# midnight: Roll over at midnight
#log_rotate = W6
# The following directives define logging patterns including:
# - log_format: using python logging's attributes
# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes
# -log_date_format: using python strftime formating directives
# refer to https://docs.python.org/2/library/time.html#time.strftime
#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s
#log_date_format = %Y-%m-%d %H:%M:%S
[interceptor-client]
# The following directives define client connector to InterceptorPB, it's used when jasmind
# is started with --enable-interceptor-client
#host = 127.0.0.1
#port = 8987
#username = iadmin
#password = ipwd
+1
View File
@@ -13,6 +13,7 @@ EXPERT_SEVERITY_ERROR = "8388608"
# The SMPP port each peer's compose overlay exposes its SMSC on. # The SMPP port each peer's compose overlay exposes its SMSC on.
PORT_BY_PEER = { PORT_BY_PEER = {
"jasmin": 2775,
"kannel": 2775, "kannel": 2775,
"smppsim": 2775, "smppsim": 2775,
"smscsim": 2775, "smscsim": 2775,