Pau Espin Pedrol | 2d16f6f | 2017-05-30 15:33:57 +0200 | [diff] [blame] | 1 | # osmo_gsm_tester: SMPP ESME to talk to SMSC |
| 2 | # |
| 3 | # Copyright (C) 2017 by sysmocom - s.f.m.c. GmbH |
| 4 | # |
| 5 | # Author: Pau Espin Pedrol <pespin@sysmocom.de> |
| 6 | # |
| 7 | # This program is free software: you can redistribute it and/or modify |
| 8 | # it under the terms of the GNU Affero General Public License as |
| 9 | # published by the Free Software Foundation, either version 3 of the |
| 10 | # License, or (at your option) any later version. |
| 11 | # |
| 12 | # This program is distributed in the hope that it will be useful, |
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | # GNU Affero General Public License for more details. |
| 16 | # |
| 17 | # You should have received a copy of the GNU Affero General Public License |
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 19 | |
| 20 | import smpplib.gsm |
| 21 | import smpplib.client |
| 22 | import smpplib.consts |
| 23 | import smpplib.exceptions |
| 24 | |
| 25 | from . import log, util, event_loop, sms |
| 26 | |
| 27 | # if you want to know what's happening inside python-smpplib |
| 28 | #import logging |
| 29 | #logging.basicConfig(level='DEBUG') |
| 30 | |
| 31 | MAX_SYS_ID_LEN = 16 |
| 32 | MAX_PASSWD_LEN = 16 |
| 33 | |
| 34 | class Esme(log.Origin): |
| 35 | client = None |
| 36 | smsc = None |
| 37 | |
| 38 | def __init__(self, msisdn): |
| 39 | self.msisdn = msisdn |
| 40 | # Get last characters of msisdn to stay inside MAX_SYS_ID_LEN. Similar to modulus operator. |
| 41 | self.set_system_id('esme-' + self.msisdn[-11:]) |
| 42 | super().__init__(log.C_TST, self.system_id) |
| 43 | self.set_password('esme-pwd') |
| 44 | self.connected = False |
| 45 | self.bound = False |
| 46 | self.listening = False |
| 47 | |
| 48 | def __del__(self): |
| 49 | try: |
| 50 | self.disconnect() |
| 51 | except smpplib.exceptions.ConnectionError: |
| 52 | pass |
| 53 | |
| 54 | def set_smsc(self, smsc): |
| 55 | self.smsc = smsc |
| 56 | |
| 57 | def set_system_id(self, name): |
| 58 | if len(name) > MAX_SYS_ID_LEN: |
| 59 | raise log.Error('Esme system_id too long! %d vs %d', len(name), MAX_SYS_ID_LEN) |
| 60 | self.system_id = name |
| 61 | |
| 62 | def set_password(self, password): |
| 63 | if len(password) > MAX_PASSWD_LEN: |
| 64 | raise log.Error('Esme password too long! %d vs %d', len(password), MAX_PASSWD_LEN) |
| 65 | self.password = password |
| 66 | |
| 67 | def conf_for_smsc(self): |
| 68 | config = { 'system_id': self.system_id, 'password': self.password } |
| 69 | return config |
| 70 | |
| 71 | def poll(self): |
| 72 | self.client.poll() |
| 73 | |
| 74 | def start_listening(self): |
| 75 | self.listening = True |
| 76 | event_loop.register_poll_func(self.poll) |
| 77 | |
| 78 | def stop_listening(self): |
| 79 | if not self.listening: |
| 80 | return |
| 81 | self.listening = False |
| 82 | # Empty the queue before processing the unbind + disconnect PDUs |
| 83 | event_loop.unregister_poll_func(self.poll) |
| 84 | self.poll() |
| 85 | |
| 86 | def connect(self): |
| 87 | host, port = self.smsc.addr_port |
| 88 | if self.client: |
| 89 | self.disconnect() |
| 90 | self.client = smpplib.client.Client(host, port, timeout=None) |
| 91 | self.client.set_message_sent_handler( |
| 92 | lambda pdu: self.dbg('message sent:', repr(pdu)) ) |
| 93 | self.client.set_message_received_handler( |
| 94 | lambda pdu: self.dbg('message received:', repr(pdu)) ) |
| 95 | self.client.connect() |
| 96 | self.connected = True |
| 97 | self.client.bind_transceiver(system_id=self.system_id, password=self.password) |
| 98 | self.bound = True |
| 99 | self.log('Connected and bound successfully. Starting to listen') |
| 100 | self.start_listening() |
| 101 | |
| 102 | def disconnect(self): |
| 103 | self.stop_listening() |
| 104 | if self.bound: |
| 105 | self.client.unbind() |
| 106 | self.bound = False |
| 107 | if self.connected: |
| 108 | self.client.disconnect() |
| 109 | self.connected = False |
| 110 | |
| 111 | def run_method_expect_failure(self, errcode, method, *args): |
| 112 | try: |
| 113 | method(*args) |
| 114 | #it should not succeed, raise an exception: |
| 115 | raise log.Error('SMPP Failure: %s should have failed with SMPP error %d (%s) but succeeded.' % (method, errcode, smpplib.consts.DESCRIPTIONS[errcode])) |
| 116 | except smpplib.exceptions.PDUError as e: |
| 117 | if e.args[1] != errcode: |
| 118 | raise e |
| 119 | |
| 120 | def sms_send(self, sms_obj): |
| 121 | parts, encoding_flag, msg_type_flag = smpplib.gsm.make_parts(str(sms_obj)) |
| 122 | |
| 123 | self.log('Sending SMS "%s" to %s' % (str(sms_obj), sms_obj.dst_msisdn())) |
| 124 | for part in parts: |
| 125 | pdu = self.client.send_message( |
| 126 | source_addr_ton=smpplib.consts.SMPP_TON_INTL, |
| 127 | source_addr_npi=smpplib.consts.SMPP_NPI_ISDN, |
| 128 | source_addr=sms_obj.src_msisdn(), |
| 129 | dest_addr_ton=smpplib.consts.SMPP_TON_INTL, |
| 130 | dest_addr_npi=smpplib.consts.SMPP_NPI_ISDN, |
| 131 | destination_addr=sms_obj.dst_msisdn(), |
| 132 | short_message=part, |
| 133 | data_coding=encoding_flag, |
| 134 | esm_class=smpplib.consts.SMPP_MSGMODE_FORWARD, |
| 135 | registered_delivery=False, |
| 136 | ) |
| 137 | |
| 138 | # vim: expandtab tabstop=4 shiftwidth=4 |