Interception#
Starting from 0.7.0, Jasmin provides a convenient way for users to hook third party logics on intercepted messages (submit_sm or deliver_sm) before proceding to The message router.
Interception of message is based on filter matching, just like the router; every intercepted message will be handed to a user-script written in Python.
This feature permits users to implement custom behaviors on top of Jasmin router, here’s some possible scenarios:
Billing & charging of MO messages,
Implement HLR lookup for a better SMS MT routing,
Change a pdu content: fix npi/ton, prefixing/suffixing numbers, etc …
Modify Jasmin’s response for the message: send back a ESME_RINVDSTADR instead of ESME_ROK for example.
etc ..
Enabling interceptor#
Jasmin’s interceptor is a system service that run separately from Jasmin, it can be hosted on remote server as well; interceptord is a system service just like jasmind, so simply start it by typing:
sudo systemctl start jasmin-interceptord
Note
After starting the interceptord service, you may check /var/log/jasmin/interceptor.log to ensure everything is okay.
Then you need to enable communication between jasmind and interceptord services by editing jasmind start script (locate the jasmind.service file in /etc/systemd) and replacing the following line:
ExecStart=/usr/bin/jasmind.py --username jcliadmin --password jclipwd
by:
ExecStart=/usr/bin/jasmind.py --username jcliadmin --password jclipwd --enable-interceptor-client
The last step is to restart jasmind and check /var/log/jasmin/interceptor.log to ensure connection has been successfully established by finding the following line:
INFO XXXX Authenticated Avatar: iadmin
Intercepting a message#
As stated earlier, interceptor is behaving similarly to The message router, here’s an example of setting up a MO message (deliver_sm) interception rule through jcli management console:
jcli : mointerceptor -a
Adding a new MO Interceptor: (ok: save, ko: exit)
> type DefaultInterceptor
<class 'jasmin.routing.Interceptors.DefaultInterceptor'> arguments:
script
> script python3(/opt/jasmin-scripts/interception/mo-interceptor.py)
> ok
Successfully added MOInterceptor [DefaultInterceptor] with order:0
Same thing apply to setting up a MT message (submit_sm) interception rule, here’s another example using a filtered rule instead of a default one:
jcli : mtinterceptor -a
Adding a new MT Interceptor: (ok: save, ko: exit)
> type StaticMTInterceptor
<class 'jasmin.routing.Interceptors.DefaultInterceptor'> arguments:
filters, script
> script python3(/opt/jasmin-scripts/interception/mt-interceptor.py)
> filters U-foo;DA-33
> order 100
> ok
Successfully added MTInterceptor [StaticMTInterceptor] with order:100
As show in the above examples, the interception rules are straightforward, any matched message will be handed to the script you set through the script python3(<path_to_pyfile>) instruction.
When your python script is called it will get the following global variables set:
routable: one of the jasmin.routing.Routables.Routable inheriters (Routable for more details)
smpp_status: (default to 0) it is the smpp response that Jasmin must return for the message, more details in Controlling response
http_status: (default to 0) it is the http response that Jasmin must return for the message, more details in Controlling response
The script can:
Override routable parameters like setting destination or source addresses, short message, etc …
Tag the routable to help the router matching a desired rule (useful for HRL lookup routing)
Control Jasmin response by setting smpp_status and/or http_status.
Some practical examples are given below.
Controlling response#
The interceptor script can reject message before it goes to the router, this can be useful for implementing third party controls like:
Billing and charging authorization: reject message if user has no credits,
Reject some illegal message content,
Enable anti-spam to protect destination users from getting flooded,
etc …
In order to reject a message, depending on the source of message (httpapi ? smpp server ? smpp client ?) the script must set smpp_status and/or http_status accordingly to the error to be returned back, here’s an error mapping table for smpp:
Value |
SMPP Status |
Description |
---|---|---|
0 |
ESME_ROK |
No error |
1 |
ESME_RINVMSGLEN |
Message Length is invalid |
2 |
ESME_RINVCMDLEN |
Command Length is invalid |
3 |
ESME_RINVCMDID |
Invalid Command ID |
4 |
ESME_RINVBNDSTS |
Invalid BIND Status for given command |
5 |
ESME_RALYBND |
ESME Already in Bound State |
6 |
ESME_RINVPRTFLG |
Invalid Priority Flag |
7 |
ESME_RINVREGDLVFLG |
Invalid Registered Delivery Flag |
8 |
ESME_RSYSERR |
System Error |
265 |
ESME_RINVBCASTAREAFMT |
Broadcast Area Format is invalid |
10 |
ESME_RINVSRCADR |
Invalid Source Address |
11 |
ESME_RINVDSTADR |
Invalid Dest Addr |
12 |
ESME_RINVMSGID |
Message ID is invalid |
13 |
ESME_RBINDFAIL |
Bind Failed |
14 |
ESME_RINVPASWD |
Invalid Password |
15 |
ESME_RINVSYSID |
Invalid System ID |
272 |
ESME_RINVBCAST_REP |
Number of Repeated Broadcasts is invalid |
17 |
ESME_RCANCELFAIL |
Cancel SM Failed |
274 |
ESME_RINVBCASTCHANIND |
Broadcast Channel Indicator is invalid |
19 |
ESME_RREPLACEFAIL |
Replace SM Failed |
20 |
ESME_RMSGQFUL |
Message Queue Full |
21 |
ESME_RINVSERTYP |
Invalid Service Type |
196 |
ESME_RINVOPTPARAMVAL |
Invalid Optional Parameter Value |
260 |
ESME_RINVDCS |
Invalid Data Coding Scheme |
261 |
ESME_RINVSRCADDRSUBUNIT |
Source Address Sub unit is Invalid |
262 |
ESME_RINVDSTADDRSUBUNIT |
Destination Address Sub unit is Invalid |
263 |
ESME_RINVBCASTFREQINT |
Broadcast Frequency Interval is invalid |
257 |
ESME_RPROHIBITED |
ESME Prohibited from using specified operation |
273 |
ESME_RINVBCASTSRVGRP |
Broadcast Service Group is invalid |
264 |
ESME_RINVBCASTALIAS_NAME |
Broadcast Alias Name is invalid |
270 |
ESME_RBCASTQUERYFAIL |
query_broadcast_sm operation failed |
51 |
ESME_RINVNUMDESTS |
Invalid number of destinations |
52 |
ESME_RINVDLNAME |
Invalid Distribution List Name |
267 |
ESME_RINVBCASTCNTTYPE |
Broadcast Content Type is invalid |
266 |
ESME_RINVNUMBCAST_AREAS |
Number of Broadcast Areas is invalid |
192 |
ESME_RINVOPTPARSTREAM |
Error in the optional part of the PDU Body |
64 |
ESME_RINVDESTFLAG |
Destination flag is invalid (submit_multi) |
193 |
ESME_ROPTPARNOTALLWD |
Optional Parameter not allowed |
66 |
ESME_RINVSUBREP |
Invalid submit with replace request (i.e. submit_sm with replace_if_present_flag set) |
67 |
ESME_RINVESMCLASS |
Invalid esm_class field data |
68 |
ESME_RCNTSUBDL |
Cannot Submit to Distribution List |
69 |
ESME_RSUBMITFAIL |
submit_sm or submit_multi failed |
256 |
ESME_RSERTYPUNAUTH |
ESME Not authorised to use specified service_type |
72 |
ESME_RINVSRCTON |
Invalid Source address TON |
73 |
ESME_RINVSRCNPI |
Invalid Source address NPI |
258 |
ESME_RSERTYPUNAVAIL |
Specified service_type is unavailable |
269 |
ESME_RBCASTFAIL |
broadcast_sm operation failed |
80 |
ESME_RINVDSTTON |
Invalid Destination address TON |
81 |
ESME_RINVDSTNPI |
Invalid Destination address NPI |
83 |
ESME_RINVSYSTYP |
Invalid system_type field |
84 |
ESME_RINVREPFLAG |
Invalid replace_if_present flag |
85 |
ESME_RINVNUMMSGS |
Invalid number of messages |
88 |
ESME_RTHROTTLED |
Throttling error (ESME has exceeded allowed message limits |
271 |
ESME_RBCASTCANCELFAIL |
cancel_broadcast_sm operation failed |
97 |
ESME_RINVSCHED |
Invalid Scheduled Delivery Time |
98 |
ESME_RINVEXPIRY |
Invalid message validity period (Expiry time) |
99 |
ESME_RINVDFTMSGID |
Predefined Message Invalid or Not Found |
100 |
ESME_RX_T_APPN |
ESME Receiver Temporary App Error Code |
101 |
ESME_RX_P_APPN |
ESME Receiver Permanent App Error Code |
102 |
ESME_RX_R_APPN |
ESME Receiver Reject Message Error Code |
103 |
ESME_RQUERYFAIL |
query_sm request failed |
259 |
ESME_RSERTYPDENIED |
Specified service_type is denied |
194 |
ESME_RINVPARLEN |
Invalid Parameter Length |
268 |
ESME_RINVBCASTMSGCLASS |
Broadcast Message Class is invalid |
255 |
ESME_RUNKNOWNERR |
Unknown Error |
254 |
ESME_RDELIVERYFAILURE |
Delivery Failure (used for data_sm_resp) |
195 |
ESME_RMISSINGOPTPARAM |
Expected Optional Parameter missing |
As for http errors, the value you set in http_status will be the http error code to return.
Note
When setting http_status to some value different from 0, the smpp_status value will be automatically set to 255 (ESME_RUNKNOWNERR).
Note
When setting smpp_status to some value different from 0, the http_status value will be automatically set to 520 (Unknown error).
Note
When setting smpp_status to 0, the routing process will be bypassed and an ESME_ROK status is returned.
Checkout the MO Charging example to see how’s rejection is done.
Scripting examples#
You’ll find below some helping examples of scripts used to intercept MO and/or MT messages.
HLR Lookup routing#
The following script will help the router decide where to send the MT message, let’s say we have some HLR lookup webservice to call in order to know to which network the destination number belong, and then tag the routable for later filtering in router:
"This script will call HLR lookup api to get the MCC/MNC of the destination number"
import requests, json
hlr_lookup_url = "https://api.some-provider.com/hlr/lookup"
data = json.dumps({'number': routable.pdu.params['destination_addr']})
r = requests.post(hlr_lookup_url, data, auth=('user', '*****'))
if r.json['mnc'] == '214':
# Spain
if r.json['mcc'] == '01':
# Vodaphone
routable.addTag(21401)
elif r.json['mcc'] == '03':
# Orange
routable.addTag(21403)
elif r.json['mcc'] == '25':
# Lyca mobile
routable.addTag(21425)
The script is tagging the routable if destination is Vodaphone, Orange or Lyca mobile; that’s because we need to route message to different connector based on destination network, let’s say:
Vodaphone needs to be routed through connectorA
Orange needs to be routed through connectorB
Lyca mobile needs to be routed through connectorC
All the rest needs to be routed through connectorD
Here’s the routing table to execute the above example:
jcli : mtrouter -l
#Order Type Rate Connector ID(s) Filter(s)
#102 StaticMTRoute 0 (!) smppc(connectorA) <TG (tag=21401)>
#101 StaticMTRoute 0 (!) smppc(connectorB) <TG (tag=21403)>
#100 StaticMTRoute 0 (!) smppc(connectorC) <TG (tag=21425)>
#0 DefaultRoute 0 (!) smppc(connectorD)
Total MT Routes: 4
MO Charging#
In this case, the script is calling CGRateS charging system to check if user has sufficient balance to send sms, based on the following script, Jasmin will return a ESME_ROK if user balance, or ESME_RDELIVERYFAILURE if not:
"""This script will receive Mobile-Originated messages and
ask CGRateS for authorization through ApierV2.GetMaxUsage call.
"""
import json, socket
from datetime import datetime
CGR_HOST="172.20.20.140"
CGR_PORT=3422
def call(sck, name, params):
# Build the request
request = dict(id=1,
params=list(params),
method=name)
sck.sendall(json.dumps(request).encode())
# This must loop if resp is bigger than 4K
buffer = ''
data = True
while data:
data = sck.recv(4096)
buffer += data
if len(data) < 4096:
break
response = json.loads(buffer.decode())
if response.get('id') != request.get('id'):
raise Exception("expected id=%s, received id=%s: %s"
%(request.get('id'), response.get('id'),
response.get('error')))
if response.get('error') is not None:
raise Exception(response.get('error'))
return response.get('result')
sck = None
globals()['sck'] = sck
globals()['json'] = json
try:
sck = socket.create_connection((CGR_HOST, CGR_PORT))
# Prepare for RPC call
name = "ApierV2.GetMaxUsage"
params = [{
"Category": "sms-mt",
"Usage": "1",
"Direction": "*outbound",
"ReqType": "*subscribers",
"TOR": "*sms-mt",
"ExtraFields": {"Cli": routable.pdu.params['source_addr']},
"Destination": routable.pdu.params['destination_addr'],
"Account": "*subscribers",
"Tenant": "*subscribers",
"SetupTime": datetime.utcnow().isoformat() + 'Z'}]
result = call(sck, name, params)
except Exception as e:
# We got an error when calling for charging
# Return ESME_RDELIVERYFAILURE
smpp_status = 254
else:
# CGRateS has returned a value
if type(result) == int and result >= 1:
# Return ESME_ROK
smpp_status = 0
else:
# Return ESME_RDELIVERYFAILURE
smpp_status = 254
finally:
if sck is not None:
sck.close()
Overriding source address#
There’s some cases where you need to override sender-id due to some MNO policies, in the following example all intercepted messages will have their sender-id set to 123456789:
"This script will override sender-id"
routable.pdu.params['source_addr'] = '123456789'
Note
Some pdu parameters require locking to protect them from being updated by Jasmin, more on this.
Chaning TON or NPI#
In order to change the ton or npi value for source or destination address, the according values need to be set and locked, in order to prevent them from getting overwritten by the client connector:
from smpp.pdu.pdu_types import AddrTon, AddrNpi
routable.pdu.params['source_addr_ton'] = AddrTon.ALPHANUMERIC;;
routable.lockPduParam('source_addr_ton');
routable.pdu.params['source_addr_npi'] = AddrNpi.ISDN;
routable.lockPduParam('source_addr_npi');
routable.pdu.params['dest_addr_ton'] = AddrTon.INTERNATIONAL;
routable.lockPduParam('dest_addr_ton');
routable.pdu.params['dest_addr_npi'] = AddrNpi.ISDN;
routable.lockPduParam('dest_addr_npi');
Activate logging#
The following is an example of activating log inside a script:
"This is how logging is done inside interception script"
import logging
# Set logger
logger = logging.getLogger('logging-example')
if len(logger.handlers) != 1:
hdlr = logging.FileHandler('/var/log/jasmin/some_file.log')
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.DEBUG)
logger.info('Got pdu: %s' % routable.pdu)
Enforcing DLR#
Ask for DLR for all submit_sm pdus, no matter the downstream user choice, can be used for route qualification and scoring purposes.
"This script will enforce sending message while asking for DLR"
from smpp.pdu.pdu_types import RegisteredDeliveryReceipt, RegisteredDelivery
routable.pdu.params['registered_delivery'] = RegisteredDelivery(
RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED)