diff options
author | Oliver Smith <osmith@sysmocom.de> | 2019-11-14 16:02:51 +0100 |
---|---|---|
committer | Neels Hofmeyr <neels@hofmeyr.de> | 2019-11-18 15:46:33 +0100 |
commit | b3248667280f026fc2cbf6e931ff1f713c74f995 (patch) | |
tree | 1cdccd84b1d5611a4da59e967e1186b84c7200b3 | |
parent | 8a50cfbc4e2eff215014c91b6ee7ad7f65d66101 (diff) |
contrib/esme_mslookup.py: new file
Add example script for the distributed GSM network, which connects to
the SMPP port of OsmoMSC A and forwards SMS to the SMPP port of
OsmoMSC B. The IP and port of OsmoMSC B is retrieved by the receiver's
MSISDN using osmo-mslookup-client.
Related: OS#4254
Change-Id: I56372d6d66c6fb54ae3fcc052543fd78f8a5ef1e
-rwxr-xr-x | contrib/esme_mslookup.py | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/contrib/esme_mslookup.py b/contrib/esme_mslookup.py new file mode 100755 index 000000000..6751eb7ce --- /dev/null +++ b/contrib/esme_mslookup.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +SPDX-License-Identifier: AGPL-3.0-or-later +Copyright 2019 sysmocom s.f.m.c GmbH <info@sysmocom.de> + +Based on esme.py from RCCN: +https://github.com/Rhizomatica/rccn/blob/master/rccn/esme.py +Copyright 2017 keith <keith@rhizomatica.org> + +Forward SMS to the receiver's SMSC, as determined with mslookup. +Requires smpplip (pip3 install --user smpplib) and osmo-mslookup-client. + +Example SMPP configuration for osmo-msc.cfg: +smpp + local-tcp-ip 127.0.0.1 2775 + policy closed + smpp-first +# outgoing to esme_mslookup.py + esme OSMPP + no alert-notifications + password foo + default-route +# incoming from esme_mslookup.py + esme ISMPP + no alert-notifications + password foo +""" +import argparse +import json +import logging +import smpplib +import subprocess +import threading +import time + + +def can_handle_pdu(pdu): + if not isinstance(pdu, smpplib.command.DeliverSM): + logging.info('PDU is not a DeliverSM. Is OsmoMSC configured properly?') + return False + + # Multipart SMS etc. not handled here (see RCCN's esme.py) + if pdu.esm_class & smpplib.consts.SMPP_GSMFEAT_UDHI: + logging.info("UDH (User Data Header) handling not implemented in this" + " example, dropping message.") + return False + + if int(pdu.dest_addr_ton) == smpplib.consts.SMPP_TON_INTL: + logging.info("Unable to handle SMS for %s: SMPP_TON_INTL" % + (pdu.destination_addr)) + return False + + return True + + +def query_mslookup(service_type, id, id_type='msisdn'): + query_str = '%s.%s.%s' % (service_type, id, id_type) + logging.info('mslookup: ' + query_str) + + result_line = subprocess.check_output(['osmo-mslookup-client', query_str, + '-f', 'json']) + if isinstance(result_line, bytes): + result_line = result_line.decode('ascii') + + logging.info('mslookup result: ' + result_line.rstrip()) + return json.loads(result_line) + + +def tx_sms(dst_host, dst_port, source, destination, unicode_text): + smpp_client = smpplib.client.Client(dst_host, dst_port, 90) + smpp_client.connect() + smpp_client.bind_transceiver(system_id=args.dst_id, password=args.dst_pass) + logging.info('Connected to destination SMSC (%s@%s:%s)' % (args.dst_id, + dst_host, dst_port)) + + pdu = smpp_client.send_message( + source_addr_ton=smpplib.consts.SMPP_TON_ALNUM, + source_addr_npi=smpplib.consts.SMPP_NPI_UNK, + source_addr=source.decode(), + dest_addr_ton=smpplib.consts.SMPP_TON_SBSCR, + dest_addr_npi=smpplib.consts.SMPP_NPI_ISDN, + destination_addr=destination.decode(), + short_message=unicode_text, + registered_delivery=False, + ) + + smpp_client.unbind() + smpp_client.disconnect() + del pdu + del smpp_client + + +thread_id_short = 0 + + +class ForwardSMSThread(threading.Thread): + def __init__(self, pdu): + global thread_id_short + name = "ForwardSMSThread#" + str(thread_id_short) + thread_id_short += 1 + + threading.Thread.__init__(self, name=name) + self.pdu = pdu + + def run(self): + logging.info("Thread started") + msisdn = self.pdu.destination_addr.decode() + result = query_mslookup("smpp.sms", msisdn) + if 'v4' not in result or not result['v4']: + logging.info('No IPv4 result from mslookup! This example only' + ' makes use of IPv4, dropping.') + return + + if args.sleep: + logging.info("Sleeping for %i seconds" % (args.sleep)) + time.sleep(args.sleep) + logging.info("Sleep done") + + dst_host, dst_port = result['v4'] + tx_sms(dst_host, dst_port, self.pdu.source_addr, + self.pdu.destination_addr, self.pdu.short_message) + logging.info("Thread is done") + + +def rx_deliver_sm(pdu): + if not can_handle_pdu(pdu): + return smpplib.consts.SMPP_ESME_RSYSERR + + # Start a background thread for every incoming SMS, so we are not blocking + # until mslookup is done. For real world usage, one needs to store SMS that + # failed to deliver in the thread and try again later. + thread = ForwardSMSThread(pdu) + thread.start() + + return smpplib.consts.SMPP_ESME_ROK + + +def smpp_bind(): + client = smpplib.client.Client(args.src_host, args.src_port, 90) + client.set_message_received_handler(rx_deliver_sm) + client.connect() + client.bind_transceiver(system_id=args.src_id, password=args.src_pass) + logging.info('Connected to source SMSC (%s@%s:%s)' % (args.src_id, + args.src_host, args.src_port)) + logging.info('Waiting for SMS...') + client.listen() + + +def main(): + global args + parser = argparse.ArgumentParser() + parser.add_argument('--src-host', default='127.0.0.1', + help='source SMSC (OsmoMSC) host (default: 127.0.0.1)') + parser.add_argument('--src-port', default=2775, type=int, + help='source SMSC (OsmoMSC) port (default: 2775)') + parser.add_argument('--src-id', default='OSMPP', + help='source system id, as configured in osmo-msc.cfg' + ' (default: OSMPP)') + parser.add_argument('--src-pass', default='foo', + help='source system password, as configured in' + ' osmo-msc.cfg (default: foo)') + parser.add_argument('--dst-id', default='ISMPP', + help='destination system id, as configured in' + ' osmo-msc.cfg (default: ISMPP)') + parser.add_argument('--dst-pass', default='foo', + help='destination system password, as configured in' + ' osmo-msc.cfg (default: foo)') + parser.add_argument('--sleep', default=0, type=float, + help='sleep time in seconds before forwarding an SMS,' + ' to test multithreading (default: 0)') + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format='[%(asctime)s]' + ' (%(threadName)s) %(message)s', datefmt="%H:%M:%S") + smpp_bind() + + +if __name__ == "__main__": + main() |