From e53a34a7e1ee03afde47ec73e8e912be6be6ec05 Mon Sep 17 00:00:00 2001 From: Oliver Smith Date: Wed, 20 Nov 2019 12:48:12 +0100 Subject: contrib/dgsm/ add example esme and dialplan Add example scripts for the distributed GSM network: esme_dgsm.py: connect to the SMPP port of OsmoMSC A and forward 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. contrib/dgsm/freeswitch_dialplan_dgsm.py: resolve the destination SIP servers of calls with osmo-mslookup-client and bridge the calls accordingly. For a detailed overview of the D-GSM and mslookup related files, please see the elaborate comment at the top of mslookup.c (already added in an earlier patch). Related: OS#4254 Related: OS#4255 Change-Id: I26e8dd8d9a08187fccb3e74ee91366bc24f6c608 --- contrib/dgsm/Makefile.am | 2 + contrib/dgsm/esme_dgsm.py | 158 +++++++++++++++++++++++++++++++ contrib/dgsm/freeswitch_dialplan_dgsm.py | 77 +++++++++++++++ 3 files changed, 237 insertions(+) create mode 100755 contrib/dgsm/esme_dgsm.py create mode 100755 contrib/dgsm/freeswitch_dialplan_dgsm.py diff --git a/contrib/dgsm/Makefile.am b/contrib/dgsm/Makefile.am index c759302..5392646 100644 --- a/contrib/dgsm/Makefile.am +++ b/contrib/dgsm/Makefile.am @@ -1,4 +1,6 @@ EXTRA_DIST = \ + esme_dgsm.py \ + freeswitch_dialplan_dgsm.py \ osmo-mslookup-pipe.py \ osmo-mslookup-socket.py \ $(NULL) diff --git a/contrib/dgsm/esme_dgsm.py b/contrib/dgsm/esme_dgsm.py new file mode 100755 index 0000000..75cf93d --- /dev/null +++ b/contrib/dgsm/esme_dgsm.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +""" +SPDX-License-Identifier: MIT +Copyright 2019 sysmocom s.f.m.c GmbH + +WARNING: this is just a proof-of-concept implementation, it blocks for every +received SMPP request and is not suitable for servicing more than one request +at a time. + +Based on esme.py from RCCN (license changed with permission from author): +https://github.com/Rhizomatica/rccn/blob/master/rccn/esme.py +Copyright 2017 keith + +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_dgsm.py + esme OSMPP + no alert-notifications + password foo + default-route +# incoming from esme_dgsm.py + esme ISMPP + no alert-notifications + password foo +""" +import argparse +import json +import logging +import smpplib +import subprocess +import time + + +def can_handle_pdu(pdu): + if not isinstance(pdu, smpplib.command.DeliverSM): + logging.info('PDU is not a DeliverSM, ignoring') + 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, registered_delivery, + 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=registered_delivery, + ) + + smpp_client.unbind() + smpp_client.disconnect() + del pdu + del smpp_client + + +def rx_deliver_sm(pdu): + if not can_handle_pdu(pdu): + return smpplib.consts.SMPP_ESME_RSYSERR + + msisdn = pdu.destination_addr.decode() + logging.info("Incoming SMS for: " + msisdn) + + if args.sleep: + logging.info("Sleeping for %i seconds" % (args.sleep)) + time.sleep(args.sleep) + logging.info("Sleep done") + + 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 smpplib.consts.SMPP_ESME_RSYSERR + + dst_host, dst_port = result['v4'] + tx_sms(dst_host, dst_port, pdu.source_addr, + pdu.destination_addr, int(pdu.registered_delivery), + pdu.short_message) + + 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() diff --git a/contrib/dgsm/freeswitch_dialplan_dgsm.py b/contrib/dgsm/freeswitch_dialplan_dgsm.py new file mode 100755 index 0000000..502fa6e --- /dev/null +++ b/contrib/dgsm/freeswitch_dialplan_dgsm.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +SPDX-License-Identifier: MIT +Copyright 2019 sysmocom s.f.m.c GmbH + +This is a freeswitch dialplan implementation, see: +https://freeswitch.org/confluence/display/FREESWITCH/mod_python + +Find the right SIP server with mslookup (depending on the destination number) +and bridge calls accordingly. +""" +import json +import subprocess + + +def query_mslookup(service_type, id, id_type='msisdn'): + query_str = '%s.%s.%s' % (service_type, id, id_type) + print('[dialplan-dgsm] 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') + + print('[dialplan-dgsm] mslookup result: ' + result_line) + return json.loads(result_line) + + +def handler(session, args): + """ Handle calls: bridge to the SIP server found with mslookup. """ + print('[dialplan-dgsm] call handler') + msisdn = session.getVariable('destination_number') + + # Run osmo-mslookup-client binary. We have also tried to directly call the + # C functions with ctypes but this has lead to hard-to-debug segfaults. + try: + result = query_mslookup("sip.voice", msisdn) + + # This example only makes use of IPv4 + if not result['v4']: + print('[dialplan-dgsm] no IPv4 result from mslookup') + session.hangup('UNALLOCATED_NUMBER') + return + + sip_ip, sip_port = result['v4'] + dial_str = 'sofia/internal/sip:{}@{}:{}'.format( + msisdn, sip_ip, sip_port) + print('[dialplan-dgsm] dial_str: ' + str(dial_str)) + + session.execute('bridge', dial_str) + except: + print('[dialplan-dgsm]: exception during call handler') + session.hangup('UNALLOCATED_NUMBER') + + +def fsapi(session, stream, env, args): + """ Freeswitch refuses to load the module without this. """ + stream.write(env.serialize()) + + +def main(): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('id', type=int) + parser.add_argument('-i', '--id-type', default='msisdn', + help='default: "msisdn"') + parser.add_argument('-s', '--service', default='sip.voice', + help='default: "sip.voice"') + args = parser.parse_args() + + result = query_mslookup(args.service, args.id, args.id_type) + print(json.dumps(result)) + + +if __name__ == '__main__': + main() -- cgit v1.2.3