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:

smpp_status Error mapping
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)