Skip to content
krot3232Public

Repository files navigation

SMPP test message scripts

Eight command line tools for exercising a running SMPPSim instance over real SMPP, so it can be checked without installing an SMPP client. All of them are plain bash over /dev/tcp — no client library, no Java, nothing to install.

They share the SMPP wire layer in smpp_lib.sh, which lives here too. Keep the directory intact: every script sources the library from beside itself, which is also why the scripts can be invoked from any working directory, by an absolute path if you like.

Option Meaning Default
-H host 127.0.0.1
-P SMPP port 2775
-i system_id smppclient1
-w password password
-t response timeout in seconds 10
-h show help

send_deliver_sm.sh stretches two of them: its -H is also the host of the web interface it injects through, and its -t is how long to wait for a DELIVER_SM rather than for a response, defaulting to 60 seconds. The table under each script lists only the options that script adds.

send_submit_sm.sh — submit a message to the SMSC

send_submit_sm.sh submits a single message to a running simulator, which is the quickest way to confirm that it really speaks SMPP and not just that the port is open. It performs bind_transmitter, sends one submit_sm, prints the message_id the SMSC assigned and unbinds.

./send_submit_sm.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
submit_sm_resp: ESME_ROK, message_id=0
unbound

Override what you need:

./send_submit_sm.sh -s 79001234567 -d 79007654321 -m "Hello there" -r 1
Option Meaning Default
-s source_addr 12345
-d destination_addr 447700900000
-m short_message a fixed test string
-c data_coding 0
-r registered_delivery, 1 asks for a delivery receipt 0

The exit status distinguishes the failure modes, so the script also works as a smoke test in CI: 0 accepted, 1 bad usage, 2 connection failed, 3 bind rejected, 4 submit rejected. A rejected PDU is reported with its SMPP status name, for example bind_transmitter_resp: ESME_RINVPASWD (0x0000000e).

Both addresses are sent with TON/NPI 1/1 (international/ISDN); change ADDR_TON / ADDR_NPI at the top of the script if you need something else. short_message is limited to 254 bytes, the maximum that fits in sm_length — note that the limit is on bytes, so a UTF-8 message runs out sooner than its character count suggests.

send_deliver_sm.sh — make the SMSC deliver a message

DELIVER_SM travels from the SMSC to the ESME, so a client cannot simply transmit one: SMPPSim has no case for a client originated DELIVER_SM and answers such a PDU with GENERIC_NAK. send_deliver_sm.sh therefore drives the whole exchange. It binds as a receiver, asks the simulator to originate an MO message through the injection endpoint of the web interface, prints the DELIVER_SM that arrives and acknowledges it with DELIVER_SM_RESP.

./send_deliver_sm.sh
connected to 127.0.0.1:2775
bound as receiver, address_range=12345, smsc system_id=SMPPSim
injected MO 447700900000 -> 12345 via http://127.0.0.1:8088/inject_mo
waiting up to 60s for 1 deliver_sm...
deliver_sm #1: 447700900000 -> 12345, esm_class=0, data_coding=0, 36 bytes
  short_message: Test message from send_deliver_sm.sh
received 1 deliver_sm, unbound

With -l it skips the injection and only listens, which is how to watch delivery receipts, MO service traffic or LOOPBACK messages. A receipt is recognised and labelled:

./send_deliver_sm.sh -l            # in one shell
./send_submit_sm.sh -r 1 -s 12345  # in another
deliver_sm #1 (delivery receipt): 447700900000 -> 12345, esm_class=4, data_coding=0, 108 bytes
  short_message: id:1 sub:001 dlvrd:001 submit date:2609131522 done date:2609131522 stat:DELIVRD err:000 Text:receipt probe 2
  message_state: DELIVERED (2)

For a receipt the script also prints the message_state it reports. On a terminal the state is coloured: green once the message is DELIVERED, red for the states that mean it never arrived — EXPIRED, DELETED, UNDELIVERABLE, UNKNOWN and REJECTED. The value is read from the message_state optional parameter (tag 0x0427) that SMPPSim attaches to receipts for clients bound as SMPP 3.4 or later; if that parameter is missing, because DELIVERY_RECEIPT_OPTIONAL_PARAMS is off, the stat: field of the receipt text is used instead.

Option Meaning Default
-W web interface port, used for the injection 8088
-s source_addr of the injected MO 447700900000
-d destination_addr of the injected MO 12345
-m short_message of the injected MO a fixed test string
-c data_coding of the injected MO 0
-a address_range for the bind the -d address, or .* with -l
-n how many DELIVER_SM to wait for 1
-l listen only, do not inject off
-t how long to wait for a DELIVER_SM 60
-C colour the state: auto, always or never auto

Exit status: 0 received, 1 bad usage, 2 connection failed, 3 bind rejected, 4 injection failed, 5 nothing delivered before the timeout.

A delivery receipt is not immediate: it is only queued once the life cycle simulator moves the message to a final state, which can take considerably longer than the sweep interval suggests. If no receiver is bound at that moment the receipt waits in the pending queue and is handed over as soon as one binds, so a run that timed out often shows the backlog on the next attempt. Sixty seconds is the default for that reason; raise it with -t on a busy queue.

auto colours only when standard output is a terminal and NO_COLOR is unset, so a redirected or piped run stays free of escape codes.

The address_range given at bind time decides which messages the SMSC routes to this session: it is a regular expression and the destination address is searched for it, so the default of using the destination address verbatim always matches. See Fixes for why that is a search rather than a whole-string match.

send_submit_multi.sh — submit one message to many destinations

send_submit_multi.sh sends a single SUBMIT_MULTI carrying a list of destinations and reports the message_id together with the destinations the SMSC refused.

./send_submit_multi.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
submit_multi_resp: ESME_ROK, message_id=0, 3 destinations, 0 refused
unbound

Destinations come from -D as a comma separated list. An entry prefixed with dl: is sent as a distribution list name rather than an address, which is the second form the PDU allows:

./send_submit_multi.sh -D "79001112233,dl:friends" -m "Hello everyone"

The standard protocol handler accepts every destination, so the refused list stays empty. TestProtocolHandler1 refuses anything that is not numeric, so pointing the script at the instance that runs it — conf/props.test1 on port 2776, see Testing — exercises the other half of the response:

./send_submit_multi.sh -P 2776 -i smppclient -D "447700900001,not-a-number,447700900003"
submit_multi_resp: ESME_ROK, message_id=0, 3 destinations, 1 refused
  refused not-a-number: ESME_RINVDSTADR (0x0000000b)
Option Meaning Default
-s source_addr 12345
-D comma separated destinations, dl: for a distribution list name three test addresses
-m short_message a fixed test string
-c data_coding 0
-r registered_delivery, 1 asks for a delivery receipt 0

Exit status: 0 all destinations accepted, 1 bad usage, 2 connection failed, 3 bind rejected, 4 submit_multi rejected, 5 accepted but some destinations were refused. That last one is the reason the script is useful in CI: a partial failure is reported in the body of an otherwise successful response, and nothing else in the exchange gives it away.

number_of_dests is a single byte, so at most 255 destinations fit in one PDU; the script refuses a longer list rather than sending a malformed one.

send_data_sm.sh — submit with DATA_SM instead of SUBMIT_SM

DATA_SM is the leaner alternative to SUBMIT_SM. It has no short_message field at all: the text travels in the message_payload optional parameter, whose length field is two bytes wide, so the 254 byte ceiling of sm_length does not apply.

./send_data_sm.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
data_sm_resp: ESME_ROK, message_id=0, message_payload=33 bytes
verify: query_sm answers ESME_RQUERYFAIL, as expected for a DATA_SM
unbound

A message that send_submit_sm.sh refuses to send goes through here unchanged:

./send_data_sm.sh -m "$(head -c 500 /dev/zero | tr '\0' x)"
data_sm_resp: ESME_ROK, message_id=1, message_payload=500 bytes
Option Meaning Default
-s source_addr 12345
-d destination_addr 447700900000
-m the message, sent as the message_payload TLV a fixed test string
-c data_coding 0
-e esm_class 0
-r registered_delivery 0

Exit status: 0 accepted, 1 bad usage, 2 connection failed, 3 bind rejected, 4 data_sm rejected.

The closing query is not a formality. SMPPSim never puts a DATA_SM in its outbound queue — the code that would do it is commented out — so such a message has no life cycle, produces no delivery receipt and cannot be queried, cancelled or replaced. The script asks anyway and reports the ESME_RQUERYFAIL that proves it, which is the one observable difference from send_submit_sm.sh.

The simulator does validate the addresses: an empty -s is answered with ESME_RINVSRCADR and an empty -d with ESME_RINVDSTADR, both counted as a rejected submission.

With LOOPBACK enabled the simulator sends the message back to a bound receiver as another DATA_SM, addresses swapped, rather than converting it to a DELIVER_SM. send_deliver_sm.sh only acknowledges DELIVER_SM, so it reports such a PDU as ignoring PDU with command_id 0x00000103.

send_query_sm.sh — ask for the state of a message

send_query_sm.sh submits a message and then asks the SMSC what became of it with QUERY_SM, which is the way to watch the life cycle simulator from the outside.

./send_query_sm.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
submitted, message_id=0
query_sm_resp: message_id=0, state=ENROUTE (1), error_code=0, final_date=
unbound

What you get back is not deterministic: the outbound queue is only swept every MESSAGE_STATE_CHECK_FREQUENCY milliseconds, and the state a message lands in is drawn at random from the PERCENTAGE_* settings. A query right after the submit therefore usually still says ENROUTE.

With -p the script keeps querying until the message leaves ENROUTE, so the transition itself becomes visible:

./send_query_sm.sh -p -e 1
submitted, message_id=1
query_sm_resp: message_id=1, state=ENROUTE (1), error_code=0, final_date=
query_sm_resp: message_id=1, state=ACCEPTED (6), error_code=0, final_date=260913150537000+

-M asks about a message you already have an id for, without submitting a new one:

./send_query_sm.sh -M 0
Option Meaning Default
-s source_addr, of both the submit and the query 12345
-d destination_addr of the submit 447700900000
-m short_message of the submit a fixed test string
-M query this message_id instead of submitting
-p keep querying until the state is final off
-e interval between polls 2
-T how long to keep polling 60

Exit status: 0 answered, 1 bad usage, 2 connection failed, 3 bind rejected, 4 submit rejected, 5 query rejected.

Two things make a query fail with ESME_RQUERYFAIL. The SMSC matches on the message_id together with the source address and its TON/NPI, so a query has to repeat the source address of the original submission — -s sets it for both, but -M with the wrong -s will not find the message. And the state is only kept for DISCARD_FROM_QUEUE_AFTER milliseconds, 60 seconds by default, after which the message is forgotten and the query fails too.

send_cancel_sm.sh — cancel a message before it is delivered

send_cancel_sm.sh submits a message, cancels it with CANCEL_SM and then confirms the cancel actually took effect. That last step is worth having, because CANCEL_SM_RESP carries no body: the only way to see the difference is that the SMSC has dropped the message from its outbound queue and no longer answers a query about it.

./send_cancel_sm.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
submitted, message_id=0
cancel_sm_resp: ESME_ROK, cancelled message_id=0
verify: query_sm now answers ESME_RQUERYFAIL, the message is gone
unbound

-M cancels a message you already have an id for. -A leaves the message_id field empty, which SMPP defines as "cancel everything queued between these two addresses"; -S narrows that to one service_type:

./send_cancel_sm.sh -M 3                    # one message
./send_cancel_sm.sh -A -s 55555 -d 66666    # every message between the two
Option Meaning Default
-s source_addr, of both the submit and the cancel 12345
-d destination_addr 447700900000
-m short_message of the submit a fixed test string
-M cancel this message_id instead of submitting
-A cancel every queued message between -s and -d off
-S service_type, narrows -A empty

Exit status: 0 cancelled, 1 bad usage, 2 connection failed, 3 bind rejected, 4 submit rejected, 5 cancel rejected.

A single cancel is matched the same way as a query — on message_id plus the source address and its TON/NPI — so it fails with ESME_RCANCELFAIL for an unknown id, for the right id with the wrong -s, and for a message the SMSC has already discarded after DISCARD_FROM_QUEUE_AFTER. The bulk form reports success even when it matched nothing; the count only appears in the simulator's own log.

send_replace_sm.sh — change a message that is still queued

send_replace_sm.sh submits a message, replaces its text with REPLACE_SM and then queries it to show it is still queued afterwards.

./send_replace_sm.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
submitted, message_id=0, text=Original text from send_replace_sm.sh
replace_sm_resp: ESME_ROK, message_id=0 now carries 37 bytes: Replaced text from send_replace_sm.sh
verify: the message is still queued, state=ENROUTE
unbound

REPLACE_SM_RESP has no body and QUERY_SM does not return the text, so the query only proves the message survived. To see the replacement itself, store it with registered_delivery set and read the Text: field of the receipt, which carries the first twenty bytes of whatever the message says at that point:

./send_deliver_sm.sh -l                                            # in one shell
./send_replace_sm.sh -m "BEFORE the replace" -N "AFTER the replace" -r 1
  short_message: id:1 sub:001 ... stat:UNDELIV err:000 Text:AFTER the replace

That also shows what -r on a replace is good for: the original submission asked for no receipt, and the replacement turned one on.

Option Meaning Default
-s source_addr, of both the submit and the replace 12345
-d destination_addr of the submit 447700900000
-m short_message of the submit a fixed test string
-N the replacement short_message a fixed test string
-M replace this message_id instead of submitting
-r registered_delivery to store with the replacement 0

Exit status: 0 replaced, 1 bad usage, 2 connection failed, 3 bind rejected, 4 submit rejected, 5 replace rejected.

The replace is matched exactly like a query or a cancel, so ESME_RREPLACEFAIL means the same three things: unknown id, right id with the wrong -s, or a message the SMSC has already discarded after DISCARD_FROM_QUEUE_AFTER. REPLACE_SM also carries schedule_delivery_time and validity_period; this script sends both empty and changes only the text and registered_delivery.

send_enquire_link.sh — ping a session and time the answers

ENQUIRE_LINK is the SMPP keepalive: an empty PDU whose only job is to prove the session is still alive. send_enquire_link.sh binds, sends a few of them, times each answer and prints a summary.

./send_enquire_link.sh
connected to 127.0.0.1:2775
bound as smppclient1, smsc system_id=SMPPSim
enquire_link #1: ESME_ROK, seq=2, 8.7 ms
enquire_link #2: ESME_ROK, seq=3, 13.1 ms
enquire_link #3: ESME_ROK, seq=4, 8.9 ms
3 sent, 3 answered, 0 lost; min/avg/max 8.7/10.2/13.1 ms
unbound
Option Meaning Default
-n how many ENQUIRE_LINK to send 3
-e interval between them in seconds 1

Exit status: 0 every ping answered, 1 bad usage, 2 connection failed, 3 bind rejected, 5 at least one ping lost or refused.

Read the timings as a health check, not as a benchmark. They cover the script's own read path — a timeout, a dd and two more processes per PDU — so single digit milliseconds on a loopback connection is the floor, and what matters is a figure that suddenly grows or a ping that never comes back.

The script binds before pinging because SMPPSim answers ENQUIRE_LINK on an unbound session with ESME_RINVBNDSTS. Any bind type would do; this one uses a transmitter like the rest of the scripts.

Two properties on the simulator side make the answers late or absent on purpose, which is what turns this script from a health check into a test of the client's own keepalive handling — see Configuration:

Property Effect Default
ENQUIRE_LINK_RESPONSE_DELAY hold every ENQUIRE_LINK_RESP for this many ms 0
DROP_ENQUIRE_LINK_RESPONSES never answer ENQUIRE_LINK at all; takes precedence over the delay false

With ENQUIRE_LINK_RESPONSE_DELAY=3000 the round trip times report the delay itself:

./send_enquire_link.sh -n 2
enquire_link #1: ESME_ROK, seq=2, 3016.6 ms
enquire_link #2: ESME_ROK, seq=3, 3021.4 ms
2 sent, 2 answered, 0 lost; min/avg/max 3016.6/3019.0/3021.4 ms

Give the script a -t shorter than the delay, or set DROP_ENQUIRE_LINK_RESPONSES=true, and the pings are counted as lost and the script exits with status 5:

./send_enquire_link.sh -n 1 -t 1
enquire_link #1: no answer within 1s
1 sent, none answered

The delay occupies the connection handler thread of that session, so the simulator reads nothing else from this client while it lasts — which is exactly the busy SMSC being simulated. Dropped answers are counted as enquire_link_err on the statistics page, since nothing was answered OK.

smpp_lib.sh — the shared wire layer

smpp_lib.sh implements the SMPP wire format the scripts are built on: assembling and sending PDUs, reading responses and walking a PDU body. It is a library, not a program — source it, do not run it. The scripts source it relative to their own location rather than the working directory, so they can be invoked by an absolute path from anywhere, as long as the library stays beside them.

Everything is computed on hex strings and turned into bytes only on the way out to the socket, because shell variables cannot hold the NUL bytes an SMPP frame is full of.

What the library expects from the script that sources it:

Name Meaning
fd 3 an open read/write socket to the SMSC
$TIMEOUT seconds read_pdu waits before giving up
$SEQ sequence number counter, initialised by the library

What it provides:

Function Purpose
hex_of, hex_cstr a string as hex, plain or as a C-Octet String
hex_u8, hex_u32, hex_bytes integer fields and the byte count of a hex string
hex_to_bin, hex_to_text hex back to bytes, or to a printable string
send_pdu, send_pdu_seq frame a PDU and write it to fd 3, with the next or a given sequence number
read_pdu read one PDU into RESP_ID, RESP_STATUS, RESP_SEQ, RESP_BODY; returns 1 on timeout
take, take_cstr cursor over $BODY, each call leaving the field in $TAKEN
command_status_name the ESME_* name for a command_status
message_state_name, message_state_is_final the name of a message_state, and whether it can still change
urlencode, die percent encoding, and exit with a message

That is enough to write a client of your own. A session that binds, pings and unbinds:

#! /bin/bash
set -u
. "$(cd "$(dirname "$0")" && pwd)/smpp_lib.sh"
TIMEOUT=10

exec 3<>/dev/tcp/127.0.0.1/2775

# BIND_TRANSMITTER: system_id, password, system_type, then
# interface_version 0x34, addr_ton, addr_npi and an empty address_range.
send_pdu 00000002 "$(hex_cstr smppclient1)$(hex_cstr password)$(hex_cstr '')34000000"
read_pdu && printf 'bind: %s\n' "$(command_status_name "$RESP_STATUS")"

send_pdu 00000015 ""                    # ENQUIRE_LINK
read_pdu && printf 'enquire_link: %s\n' "$(command_status_name "$RESP_STATUS")"

send_pdu 00000006 ""                    # UNBIND
read_pdu
exec 3>&-
bind: ESME_ROK
enquire_link: ESME_ROK

Releases

Packages

Contributors

Languages