# # smpplib -- SMPP Library for Python # Copyright (c) 2005 Martynas Jocius # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """SMPP client module""" import binascii import logging import select import socket import struct import warnings from smpplib import consts, exceptions, smpp class SimpleSequenceGenerator(object): MIN_SEQUENCE = 0x00000001 MAX_SEQUENCE = 0x7FFFFFFF def __init__(self): self._sequence = self.MIN_SEQUENCE @property def sequence(self): return self._sequence def next_sequence(self): if self._sequence == self.MAX_SEQUENCE: self._sequence = self.MIN_SEQUENCE else: self._sequence += 1 return self._sequence class Client(object): """SMPP client class""" state = consts.SMPP_CLIENT_STATE_CLOSED host = None port = None vendor = None _socket = None _ssl_context = None sequence_generator = None def __init__( self, host, port, timeout=5, sequence_generator=None, logger_name=None, ssl_context=None, allow_unknown_opt_params=None, ): self.host = host self.port = int(port) self._ssl_context = ssl_context self.timeout = timeout self.logger = logging.getLogger(logger_name or 'smpp.Client.{}'.format(id(self))) if sequence_generator is None: sequence_generator = SimpleSequenceGenerator() self.sequence_generator = sequence_generator if allow_unknown_opt_params is None: warnings.warn( "Unknown optional parameters during PDU parsing will stop " "causing an exception in a future smpplib version " "(in order to comply with the SMPP spec). To switch behavior " "now set allow_unknown_opt_params to True.", DeprecationWarning, ) self.allow_unknown_opt_params = False else: self.allow_unknown_opt_params = allow_unknown_opt_params self._socket = None def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if self._socket is not None: try: self.unbind() except (exceptions.PDUError, exceptions.ConnectionError) as e: if len(getattr(e, 'args', tuple())) > 1: self.logger.warning('(%d) %s. Ignored', e.args[1], e.args[0]) else: self.logger.warning('%s. Ignored', e) self.disconnect() def __del__(self): if self._socket is not None: self.logger.warning('%s was not closed', self) @property def sequence(self): return self.sequence_generator.sequence def next_sequence(self): return self.sequence_generator.next_sequence() def _create_socket(self): raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) raw_socket.settimeout(self.timeout) if self._ssl_context is None: return raw_socket return self._ssl_context.wrap_socket(raw_socket) def connect(self): """Connect to SMSC""" self.logger.info('Connecting to %s:%s...', self.host, self.port) try: if self._socket is None: self._socket = self._create_socket() self._socket.connect((self.host, self.port)) self.state = consts.SMPP_CLIENT_STATE_OPEN except socket.error: raise exceptions.ConnectionError("Connection refused") def disconnect(self): """Disconnect from the SMSC""" self.logger.info('Disconnecting...') if self.state != consts.SMPP_CLIENT_STATE_OPEN: self.logger.warning('%s is disconnecting in the bound state', self) if self._socket is not None: self._socket.close() self._socket = None self.state = consts.SMPP_CLIENT_STATE_CLOSED def _bind(self, command_name, **kwargs): """Send bind_transmitter command to the SMSC""" if command_name in ('bind_receiver', 'bind_transceiver'): self.logger.debug('Receiver mode') p = smpp.make_pdu(command_name, client=self, **kwargs) self.send_pdu(p) try: resp = self.read_pdu() except socket.timeout: raise exceptions.ConnectionError() if resp.is_error(): raise exceptions.PDUError('({}) {}: {}'.format( resp.status, resp.command, consts.DESCRIPTIONS.get(resp.status, 'Unknown code')), int(resp.status), ) return resp def bind_transmitter(self, **kwargs): """Bind as a transmitter""" return self._bind('bind_transmitter', **kwargs) def bind_receiver(self, **kwargs): """Bind as a receiver""" return self._bind('bind_receiver', **kwargs) def bind_transceiver(self, **kwargs): """Bind as a transmitter and receiver at once""" return self._bind('bind_transceiver', **kwargs) def unbind(self): """Unbind from the SMSC""" p = smpp.make_pdu('unbind', client=self) self.send_pdu(p) try: return self.read_pdu() except socket.timeout: raise exceptions.ConnectionError() def send_pdu(self, p): """Send PDU to the SMSC""" if self.state not in consts.COMMAND_STATES[p.command]: raise exceptions.PDUError("Command %s failed: %s" % ( p.command, consts.DESCRIPTIONS[consts.SMPP_ESME_RINVBNDSTS], )) self.logger.debug('Sending %s PDU', p.command) generated = p.generate() self.logger.debug('>>%s (%d bytes)', binascii.b2a_hex(generated), len(generated)) try: self._socket.sendall(generated) except socket.error as e: self.logger.warning(e) raise exceptions.ConnectionError() return True def _recv_exact(self, exact_size): """ Keep reading from self._socket until exact_size bytes have been read """ parts = [] received = 0 while received < exact_size: try: part = self._socket.recv(exact_size - received) except socket.timeout: raise except socket.error as e: self.logger.warning(e) raise exceptions.ConnectionError() if not part: raise exceptions.ConnectionError() received += len(part) parts.append(part) return b"".join(parts) def read_pdu(self): """Read PDU from the SMSC""" self.logger.debug('Waiting for PDU...') raw_len = self._recv_exact(4) try: length = struct.unpack('>L', raw_len)[0] except struct.error: self.logger.warning('Receive broken pdu... %s', repr(raw_len)) raise exceptions.PDUError('Broken PDU') raw_pdu = raw_len + self._recv_exact(length - 4) self.logger.debug('<<%s (%d bytes)', binascii.b2a_hex(raw_pdu), len(raw_pdu)) pdu = smpp.parse_pdu( raw_pdu, client=self, allow_unknown_opt_params=self.allow_unknown_opt_params, ) self.logger.debug('Read %s PDU', pdu.command) if pdu.is_error(): return pdu elif pdu.command in consts.STATE_SETTERS: self.state = consts.STATE_SETTERS[pdu.command] return pdu def accept(self, obj): """Accept an object""" raise NotImplementedError('not implemented') def _message_received(self, pdu): """Handler for received message event""" status = self.message_received_handler(pdu=pdu) if status is None: status = consts.SMPP_ESME_ROK dsmr = smpp.make_pdu('deliver_sm_resp', client=self, status=status) dsmr.sequence = pdu.sequence self.send_pdu(dsmr) def _enquire_link_received(self, pdu): """Response to enquire_link""" ler = smpp.make_pdu('enquire_link_resp', client=self) ler.sequence = pdu.sequence self.send_pdu(ler) def _alert_notification(self, pdu): """Handler for alert notification event""" self.message_received_handler(pdu=pdu) def set_message_received_handler(self, func): """Set new function to handle message receive event""" self.message_received_handler = func def set_message_sent_handler(self, func): """Set new function to handle message sent event""" self.message_sent_handler = func def set_query_resp_handler(self, func): """Set new function to handle query resp event""" self.query_resp_handler = func def set_error_pdu_handler(self, func): """Set new function to handle PDUs with an error status""" self.error_pdu_handler = func def message_received_handler(self, pdu, **kwargs): """Custom handler to process received message. May be overridden""" self.logger.warning('Message received handler (Override me)') def message_sent_handler(self, pdu, **kwargs): """ Called when SMPP server accept message (SUBMIT_SM_RESP). May be overridden """ self.logger.warning('Message sent handler (Override me)') def query_resp_handler(self, pdu, **kwargs): """Custom handler to process response to queries. May be overridden""" self.logger.warning('Query resp handler (Override me)') def error_pdu_handler(self, pdu): raise exceptions.PDUError('({}) {}: {}'.format( pdu.status, pdu.command, consts.DESCRIPTIONS.get(pdu.status, 'Unknown status')), int(pdu.status), ) def read_once(self, ignore_error_codes=None, auto_send_enquire_link=True): """Read a PDU and act""" if ignore_error_codes is not None: warnings.warn( "ignore_error_codes is deprecated, use set_error_pdu_handler to " "configure a custom error PDU handler instead.", DeprecationWarning, ) try: try: pdu = self.read_pdu() except socket.timeout: if not auto_send_enquire_link: raise self.logger.debug('Socket timeout, listening again') pdu = smpp.make_pdu('enquire_link', client=self) self.send_pdu(pdu) return if pdu.is_error(): self.error_pdu_handler(pdu) if pdu.command == 'unbind': # unbind_res self.logger.info('Unbind command received') return elif pdu.command == 'submit_sm_resp': self.message_sent_handler(pdu=pdu) elif pdu.command == 'deliver_sm': self._message_received(pdu) elif pdu.command == 'query_sm_resp': self.query_resp_handler(pdu) elif pdu.command == 'enquire_link': self._enquire_link_received(pdu) elif pdu.command == 'enquire_link_resp': pass elif pdu.command == 'alert_notification': self._alert_notification(pdu) else: self.logger.warning('Unhandled SMPP command "%s"', pdu.command) except exceptions.PDUError as e: if ignore_error_codes and len(e.args) > 1 and e.args[1] in ignore_error_codes: self.logger.warning('(%d) %s. Ignored.', e.args[1], e.args[0]) else: raise def poll(self, ignore_error_codes=None, auto_send_enquire_link=True): """Act on available PDUs and return""" while True: readable, _writable, _exceptional = select.select([self._socket], [], [], 0) if not readable: break self.read_once(ignore_error_codes, auto_send_enquire_link) def listen(self, ignore_error_codes=None, auto_send_enquire_link=True): """Listen for PDUs and act""" while True: self.read_once(ignore_error_codes, auto_send_enquire_link) def send_message(self, **kwargs): """Send message Required Arguments: source_addr_ton -- Source address TON source_addr -- Source address (string) dest_addr_ton -- Destination address TON destination_addr -- Destination address (string) short_message -- Message text (string) """ ssm = smpp.make_pdu('submit_sm', client=self, **kwargs) self.send_pdu(ssm) return ssm def query_message(self, **kwargs): """Query message state Required Arguments: message_id -- SMSC assigned Message ID source_addr_ton -- Original source address TON source_addr_npi -- Original source address NPI source_addr -- Original source address (string) """ qsm = smpp.make_pdu('query_sm', client=self, **kwargs) self.send_pdu(qsm) return qsm