SMPP client and server implementation in node.js.
This is a complete implementation of SMPP v5.0 in node.js, with support for custom commands and TLVs.
SMPP v5.0, by design, is backward compatible with v3.4, so you would be able to use this module with 3.4 implementations. Even you can use this module with 3.3 implementations as far as you don't use TLV parameters and don't bind in transceiver mode.
The name of the methods and parameters in this implementation are equivalent to the names defined in SMPP specification. So get a copy of SMPP v5.0 Specification for a list of available operations and their parameters.
npm install @semyonf/smppnpm install smppvar smpp = require('smpp');
var session = smpp.connect({
url: 'smpp://example.com:2775',
auto_enquire_link_period: 10000,
debug: true
}, function() {
session.bind_transceiver({
system_id: 'YOUR_SYSTEM_ID',
password: 'YOUR_PASSWORD'
}, function(pdu) {
if (pdu.command_status === 0) {
// Successfully bound
session.submit_sm({
destination_addr: 'DESTINATION NUMBER',
short_message: 'Hello!'
}, function(pdu) {
if (pdu.command_status === 0) {
// Message successfully sent
console.log(pdu.message_id);
}
});
}
});
});var smpp = require('smpp');
var server = smpp.createServer({
debug: true
}, function(session) {
session.on('error', function (err) {
// Something occurred, not listening for this event will terminate the program
});
session.on('bind_transceiver', function(pdu) {
// we pause the session to prevent further incoming pdu events,
// until we authorize the session with some async operation.
session.pause();
checkAsyncUserPass(pdu.system_id, pdu.password, function(err) {
if (err) {
session.send(pdu.response({
command_status: smpp.ESME_RBINDFAIL
}));
session.close();
return;
}
session.send(pdu.response());
session.resume();
});
});
});
server.listen(2775);It's very important to listen for session errors, not listening for error events will terminate the program.
To enable a simple debug of ingoing/outgoing messages pass debug: true as
server/client option. Debug is disabled by default.
Alternatively, you can listen for the debug event and write your own implementation:
session.on('debug', function(type, msg, payload) {
console.log({type: type, msg: msg, payload: payload});
});A debugListener option is also supported:
var options = {
debug: false,
debugListener: function(type, msg, payload) {
console.log({type: type, msg: msg, payload: payload});
}
}In case of errors while trying to connect, an error event will be emitted by the session
and the program will be terminated if it's not listened. This is how you should check for
errors.
session.on('error', function(e) {
// empty callback to catch emitted errors to prevent exit due unhandled errors
if (e.code === "ETIMEOUT") {
// TIMEOUT
} else if (e.code === "ECONNREFUSED" {
// CONNECTION REFUSED
} else {
// OTHER ERROR
}
});By default the socket will be dropped after 30000 ms if it doesn't connect.
A connectTimeout option can be sent when making connections with the server in order
to change this setting.
By default a request waits for its response for as long as the session stays open: if the
response never comes, its responseCallback is simply never called. Pass a response_timeout
(in milliseconds) to smpp.connect(), or to smpp.createServer() for the sessions it accepts,
to stop waiting after that long. It is disabled when unset or 0; anything other than a number
between 0 and 2147483647 throws a RangeError.
var session = smpp.connect({ url: 'smpp://example.com:2775', response_timeout: 30000 });
session.submit_sm({
destination_addr: 'DESTINATION NUMBER',
short_message: 'Hello!'
}, function(pdu) {
// The response arrived in time.
}, null, function(pdu, err) {
if (err && err.code === 'ERESPONSETIMEOUT') {
// No response within 30 seconds. The message may or may not have been accepted.
} else if (err && err.code === 'ESESSIONCLOSED') {
// The session closed first. The message may or may not have been accepted.
} else {
// It could not be written to the socket, see session.send().
}
});With response_timeout set, a request sent with a responseCallback ends in exactly one of:
- its
responseCallback, called with the response; - its
failureCallback, called with the request pdu and an error whosecodeis'ERESPONSETIMEOUT'when no response arrived withinresponse_timeoutms ofsend()(err.timeoutholds the timeout). The'response_timeout'event is emitted as well, so a timeout is observable even for a request sent without afailureCallback; - its
failureCallback, called with the request pdu and an error whosecodeis'ESESSIONCLOSED'when the session closed while the request was still waiting: there is no point in waiting out the timeout on a connection that is gone. If the connection was closed by a socket error, it is available aserr.cause. The'response_aborted'event is emitted as well, before'close'; - its
failureCallback, called as before when the request could not be written to the socket.
A timeout, or a closed session, does not mean that the peer did not process the request:
it may well have, and only its response is missing. Resending a submit_sm that failed this
way can deliver the message twice. This is why the request pdu is passed on untouched, rather
than with its command_status set to ESME_RSUBMITFAIL as on a write failure: no status was
received from the peer, so check err.code instead.
A response that arrives after its request timed out does not call any callback, but it is
still emitted as a 'pdu' event and as its command event (e.g. 'submit_sm_resp').
Without response_timeout, nothing changes: unanswered requests are never reported, and a
failureCallback is only called for requests that could not be written to the socket.
Both sides of an SMPP session are expected to answer enquire_link with enquire_link_resp, and
to drop a link whose peer stops answering. Both are opt-in, on client sessions (smpp.connect()
options) and server sessions (smpp.createServer() options, applied to every accepted session):
| Option | Default | Description |
|---|---|---|
auto_enquire_link_response |
false |
Answer every incoming enquire_link with an enquire_link_resp. Don't also answer it from your own enquire_link handler, or the peer gets two responses. |
auto_enquire_link_period |
unset | Send an enquire_link every this many ms. Client sessions start one period after the TCP connection is established (so before the TLS handshake and the bind), server sessions one period after accepting the connection. |
enquire_link_timeout |
unset | How long, in ms, to wait for the enquire_link_resp to each of those enquire_link. When it does not arrive in time, an enquire_link_timeout event is emitted. Unset or 0 disables the check. |
close_on_enquire_link_timeout |
true |
Destroy the session after emitting enquire_link_timeout. Set to false to decide on your own in the event handler. |
var session = smpp.connect({
url: 'smpp://example.com:2775',
auto_enquire_link_response: true,
auto_enquire_link_period: 30000,
enquire_link_timeout: 10000
});
session.on('enquire_link_timeout', function(pdu) {
console.log('no enquire_link_resp for sequence', pdu.sequence_number, ', dropping the link');
});
session.on('close', function() {
setTimeout(function() {
session.connect(); // the enquire_link options apply to the new connection too
}, 5000);
});The same can be controlled at runtime with session.startEnquireLink(), session.stopEnquireLink()
and session.autoEnquireLinkResponse.
The enquire_link sent by auto_enquire_link_period only wait for enquire_link_timeout:
response_timeout doesn't apply to them, and they never show up in 'response_timeout' or
'response_aborted' events. An enquire_link you send yourself is an ordinary request.
Any response carrying the sequence number of the enquire_link counts as an answer, including a
generic_nack: some SMSCs reject an enquire_link sent before the bind that way, but they still
answered, so the link is alive.
Responses are only read while the session is not paused, so keep session.pause() shorter than
enquire_link_timeout, or the link is considered dead. A paused session doesn't answer incoming
enquire_link automatically either, so the peer's own timeout applies as well.
SMPP expects every unbind to be answered with unbind_resp. With auto_unbind_response: true
(a smpp.connect() or smpp.createServer() option, default false), the session answers an
incoming unbind with an unbind_resp of status ESME_ROK, stops its enquire_link schedule and
closes the connection: the socket is ended, and destroyed as soon as the unbind_resp has been
flushed, so a 'close' event follows even if the peer never closes its side. The 'pdu' and
'unbind' events are still emitted, after the response was handed to the socket. By then the
connection is already ending, so anything your unbind listener sends, including an unbind_resp
of its own, fails with send() returning false, and so do responses to earlier requests that
are still being processed. Pdus that followed the unbind in the same chunk are still emitted. The
option is off by default so that handlers which already answer unbind keep working as they are.
Can be changed at runtime through session.autoUnbindResponse.
An exception thrown by one of your listeners or callbacks never unwinds the session's own code:
the session keeps reading, the remaining pdus of the chunk are delivered, a 'send' listener that
throws does not skip the sendCallback, a throwing failureCallback does not keep the other
pending requests from failing or the 'close' event from being emitted, and so on. This covers
every event the session emits ('pdu', the command events, 'outgoing', 'send', 'connect',
'secureConnect', 'close', 'error', 'response_timeout', 'response_aborted',
'enquire_link_timeout', 'debug', 'metrics'), the debugListener option, and the
responseCallback, sendCallback and failureCallback of send(). The exception is emitted as a
'listener_error' event instead:
session.on('listener_error', function(report) {
console.error('listener for', report.event, 'failed', report.pdu ? 'on ' + report.pdu.command : '', report.error);
});report.event is the event whose listener threw, or the send() callback that threw
('responseCallback', 'sendCallback' or 'failureCallback'), and report.pdu the pdu that was
being handled, or null for an event that is not about one. Without a 'listener_error' listener
the exception is rethrown on the next tick, so it still ends up as an uncaught exception (which
kills the process unless you handle 'uncaughtException'), with these differences from before: it
is no longer thrown out of the send() or resume() call that triggered the listener, so it cannot
be caught around them, the rest of the chunk is handled first, and a chunk can produce several
uncaught exceptions. The 'error' event without a listener is rethrown the same way instead of
throwing synchronously from inside the socket handler.
Two things are not covered: the other listeners of the same event are still skipped once one of
them throws, as with any EventEmitter, and a rejected promise returned by an async listener is
not an exception, so it is reported nowhere.
Proxy Protocol v1 is now supported as an experimental feature, both for TCP4 and TCP6.
Pass enable_proxy_protocol_detection: true as server option to enable proxy protocol
detection, if the option is not provided the feature is completely ignored.
session.remoteAddresswill contain the proxied source IP.session.proxyProtocolProxywill contain the proxy IP.- Even with proxy protocol detection enabled the server will understand non-proxied requests.
- Security: Proxy CIDRs validation is yet to be implemented.
- Uses findhit-proxywrap, a third party library to wrap the net/tls implementations and decode the proxy-protocol
- header before forwarding it to the standard implementations.
- On proxied, non-tls connections (with Nodejs < v8): Proxywrap shows some misbehaviour
with the way this library inherits from the net server, the socket looses the ability
to emit events. As a minor-fix, the
socket.emitmethod is backed up and restored after the proxying to the net server, making everything work as expected.
This smpp implementation supports 3 encodings: ASCII (GSM 03.38), LATIN1, and UCS2.
data_coding for these encodings are 0x01, 0x03, and 0x08 respectively.
Default encoding for data_coding:0 is ASCII. You can change it as follows:
smpp.encodings.default = 'LATIN1';String messages will be automatically encoded using one of these three encodings. If the SMSC you are communicating with doesn't support one of these encodings, you can simply remove it as follows:
delete smpp.encodings.ASCII;You can also manually convert a message to a buffer and pass it as short_message
or message_payload parameter to bypass automatic message encoding.
Creates a new smpp session using the specified connection url.
url must be a string in this format: smpp://host:port. To establish a secure
smpp connection use ssmpp as scheme like in ssmpp://host:port.
If port is omitted in the url, the default port (2775 for smpp and 3550 for
secure smpp) will be used.
If called without arguments, smpp://localhost:2775 will be assumed.
The callback, if specified, will be added as a listener for the connect
event in plain connections and secureConnect event in secure connections.
This is the base object for a SMPP session. sessions can be created by calling
smpp.connect() or can be created by a smpp server when a client
establishes a connection to the server. In this case the server passes the
session object to the 'session' event listener.
Sends a pdu request/response to the MC/ESME over the session.
The pdu is an instance of smpp.PDU which might be either a response or
a request pdu.
When sending a request pdu, pdu.sequence_number will be automatically set to
the proper value.
If the pdu is a request pdu, when the relevant response is received, the
optional responseCallback parameter will be invoked with the response pdu passed to it.
Optional sendCallback will be called when the pdu is successfully flushed.
Optional failureCallback will be called whenever it is not possible to write to the socket,
with the pdu (its command_status set to ESME_RSUBMITFAIL) and, when the write itself failed,
the write error.
If the session has a response_timeout, it is also called for a request whose response did not
arrive in time, or had not arrived when the session closed. See Response timeout.
Closes the current session connection.
If supplied, the callback is called once the session is fully closed.
Forcibly closes the current session connection. This aids some broken servers,
that don't honor graceful tear-down. ( Looking at you SMPPSim )
If supplied, the callback is called once the session is fully closed.
Connects a client session again, typically from a 'close', 'error' or 'enquire_link_timeout'
listener. The session gets a new connection: sequence numbers start over, and a PDU the previous
connection delivered only partly is discarded. Requests still waiting for a response on the
previous connection fail with ESESSIONCLOSED if response_timeout is set (see
Response timeout), and are dropped otherwise. Event listeners, options and
the enquire_link schedule are kept, and the connect listener passed to smpp.connect() runs
again.
If the previous connection is still open or still being established, it is destroyed first. Either
way none of its events are emitted any more, so a 'close' that the previous connection emits
after the error that you reconnected on does not reach your listeners.
Throws when called on a server session, and when net.connect() or tls.connect() reject the
options, in which case the session is left as it was.
Can be used to postpone incoming pdu events until calling session.resume().
Resumes the session after a call to pause().
Sends an enquire_link every period ms. If timeout is given (it defaults to the
enquire_link_timeout option), every enquire_link that gets no response within timeout ms
emits enquire_link_timeout and, unless close_on_enquire_link_timeout is false, destroys the
session. Replaces the schedule set by a previous call or by the auto_enquire_link_period option;
enquire_link already sent are still awaited with their original timeout. The schedule is kept
across session.connect().
On a client session that is not connected yet, the schedule starts once the connection is
established.
Stops sending enquire_link and stops waiting for responses to the ones already sent.
session.close() and session.destroy() also stop sending them. After session.close() the
response to an enquire_link still in flight is awaited, so if the peer never closes its side of
the connection, the timeout destroys the session (unless close_on_enquire_link_timeout is
false). With nothing in flight, session.close() waits for the peer like before: call
session.destroy() if the 'close' event doesn't follow in time.
Whether incoming enquire_link are answered automatically. Initialized from the
auto_enquire_link_response option and can be changed at any time.
Whether incoming unbind are answered automatically, closing the connection afterwards (see
Answering unbind). Initialized from the auto_unbind_response option and can
be changed at any time.
The addresses of both ends of the connection, as a string and a number, or null until the first
connection is established (on a client session, they are set when 'connect' is emitted; across
session.connect() the previous connection's values are kept until the new one is established).
They are kept after the connection has closed, when the socket itself has forgotten its
localAddress/localPort, so they can still be logged from a 'close' listener. On a TLS session
they are those of the TCP connection. On a proxy-protocol session remoteAddress/remotePort are
the client's address from the PROXY header, while localAddress/localPort are those of the TCP
connection from the proxy.
Returns the certificate presented by the TLS peer, as returned by node's
tls.TLSSocket.getPeerCertificate().
Pass true to get the certificate chain as well.
On a client session the certificate is available once the secureConnect event has been emitted.
The result is undefined for a session that is not running over TLS, and an empty object for a TLS
session whose peer sent no certificate, which is the usual case for a server session created without
the requestCert option.
var session = smpp.connect('ssmpp://smpp.example.org:3550', function() {
console.log('provider certificate expires on', session.getPeerCertificate().valid_to);
});For all smpp operations you can call methods with the same name as the operation name, which is equivalent to creating a pdu instance and then sending it over the session.
For example calling session.submit_sm(options, [responseCallback], [sendCallback], [failureCallback]) is equivalent to:
var pdu = new smpp.PDU('submit_sm', options);
session.send(pdu, responseCallback);Emitted when the session connection successfully is established.
The secureConnect event is emitted after the handshaking process for
a secure connection has successfully completed.
Emitted when the connection is fully closed.
Emitted when an error occurs: a socket error, or a pdu that cannot be parsed (for instance one
longer than smpp.PDU.maxLength), after which the rest of the stream cannot be trusted and the
connection is destroyed. The 'close' event will be called directly following this event. Without
an 'error' listener the error is rethrown on the next tick, see
Exceptions in listeners.
Emitted synchronously from session.send() (and the shortcut methods) for every pdu handed to the
socket, right after socket.write() was called: requests and responses, including the
enquire_link sent by auto_enquire_link_period and the responses sent by
auto_enquire_link_response and auto_unbind_response. For a request, pdu.sequence_number is
already assigned, and the event always comes before the 'pdu' event of its response, which
'send' does not guarantee. The pdu has been encoded at this point (short_message is a Buffer,
data_coding is set), like in 'send', and modifying it has no effect on what is sent. Not emitted
when send() returns false (socket not writable) or throws (pdu that cannot be encoded). The
write can still fail afterwards: 'send' or the failureCallback tell the outcome.
Emitted once a pdu has been flushed to the socket, from the write callback. Node does not guarantee
that the write callback runs before the response is read, so the 'pdu' event of the response can
come before the request's 'send': use 'outgoing' to see a request before its response.
Emitted when one of your listeners or callbacks threw. See Exceptions in listeners.
Emitted upon receiving a pdu.
Emitted when an enquire_link sent by auto_enquire_link_period or session.startEnquireLink()
got no enquire_link_resp within the timeout. pdu is that enquire_link. Unless
close_on_enquire_link_timeout is false, the session is destroyed right after, so a 'close'
event follows.
Emitted upon receiving an unknown pdu.
Emitted with the request pdu when its response did not arrive within the response_timeout,
whether or not the request was sent with a failureCallback.
Emitted, on a session with a response_timeout, with each request pdu that was still waiting for
its response when the session closed. Emitted before the 'close' event.
When a pdu is received, after emitting the 'pdu' event, an event with the same
name as the operation of that pdu will also be emitted.
Creates a new SMPP server. The sessionListener argument is automatically set
as a listener for the 'session' event.
If options include key and cert, a TLS secured server will be created.
Include rejectUnauthorized: false to disable the certificate validation.
The enquire_link options described in
Keeping the link alive and auto_unbind_response (see
Answering unbind) apply to every session the server accepts.
The base object for a SMPP server created with smpp.createServer().
It is a child class of node's net.Server.
Begin accepting connections on the specified port and host. If port is
omitted 2775 will be used. If the host is omitted, the server will accept
connections directed to any IPv4 address.
This function is asynchronous. The last parameter callback will be called when
the server has been bound.
Emitted when a new session connection is established.
session is an instance of smpp.Session.
for other server methods/events documentations see node's net.Server docs.
This is the base object for a PDU request or response.
Creates a new PDU object with the specified command and options.
options is a list of parameters acceptable by the specified command.
The name of the parameters are equivalent to the names specified in SMPP
specification v5.0. The order of the parameters doesn't matter. If you don't
specify a required parameter in options a default value (usually null or 0 for
integers) will be used.
For the type of the parameters note the following rules:
- For
Integerparameters (no matter what the length is) you must specify a value of typenumberin JavaScript. - For
Octet-StringandCOctet-Stringparameters you can specify either aBufferor aString. - For the fields that accept SMPP Time Format (
broadcast_end_time,schedule_delivery_time,validity_period,final_date) you can specify a Javascript Date instance which will be automatically converted to a SMPP absolute time string. For relative times you don't need to specify the whole string, specifying a portion of it is enough. for example '0430' will be converted to '000000000430000R'. - For
short_messageandmessage_payloadfields you can specify a buffer or a string or an object containingudhandmessageproperties, whileudhis a buffer andmessageis either a string or a buffer. strings will be automatically encoded using ASCII, LATIN1, or UCS2 depending on their characters.data_coding(if not specified) will be automatically set to 0x01, 0x03, or 0x08 for ASCII, LATIN1, and UCS2 encodings respectively. Also UDH indicator bit inesm_classis automatically set ifudhexists. sm_lengthparameter is not needed. It will be automatically set depending on the length of theshort_message.dest_addressparameter insubmit_multioperation must be an array of objects containing eitherdest_addr_ton,dest_addr_npiand,destination_addrproperties ordl_nameproperty for SME addresses or Distribution Lists respectively.unsuccess_smeparameter insubmit_multi_respoperation must be an array of objects containingdest_addr_ton,dest_addr_npi,destination_addrand,error_status_codeproperties.number_of_destsandno_unsuccessparameters are not needed. They will be automatically set depending on thedest_addressandunsuccess_smeparameters respectively.- TLV parameters which can be specified multiple times
(e.g.
broadcast_area_identifier), must be specified as an array, even if you want to specify just one item.
Returns true if the pdu is a response pdu, otherwise returns false;
For a request pdu, calling response() creates and returns a response pdu for
that request.
For an unknown pdu, response() creates and returns a generic_nack pdu.
session.on('submit_sm', function(pdu) {
var msgid = .... ; // generate a message_id for this message.
session.send(pdu.response({
message_id: msgid
}));
});
session.on('unbind', function(pdu) {
session.send(pdu.response());
session.close();
});
session.on('enquire_link', function(pdu) {
session.send(pdu.response());
});- Support for Nodejs < v4 (2015) has been dropped due compatibility issues with findhit-proxywrap library
- Proxy protocol v1 support has been added as an experimental feature. Disabled by default, will be completely ignored if not enabled.
- Add some usage examples (e.g client, server, and cluster examples)
node-smpp is released under the MIT license.