diff --git a/smpplib/client.py b/smpplib/client.py index 6c68601..16dd693 100644 --- a/smpplib/client.py +++ b/smpplib/client.py @@ -63,14 +63,15 @@ class Client(object): vendor = None _socket = None sequence_generator = None + timeout = 5 def __init__(self, host, port, timeout=5, sequence_generator=None): """Initialize""" - + self.timeout = timeout self.host = host self.port = int(port) self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._socket.settimeout(timeout) + self._socket.settimeout(self.timeout) self.receiver_mode = False if sequence_generator is None: sequence_generator = SimpleSequenceGenerator() @@ -103,6 +104,7 @@ def connect(self): try: if self._socket is None: self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(self.timeout) self._socket.connect((self.host, self.port)) self.state = consts.SMPP_CLIENT_STATE_OPEN except socket.error: @@ -152,14 +154,8 @@ def bind_transceiver(self, **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""" @@ -195,9 +191,16 @@ def read_pdu(self): logger.debug('Waiting for PDU...') + raw_len = '' try: - raw_len = self._socket.recv(4) + while len(raw_len) < 4: + logger.debug('Waiting for more header data') + new_data = self._socket.recv(4 - len(raw_len)) + raw_len += new_data + if new_data == '': + raise exceptions.ConnectionError() except socket.timeout: + logger.debug('Socket timeout') raise except socket.error as e: logger.warning(e) @@ -211,8 +214,13 @@ def read_pdu(self): logger.warning('Receive broken pdu... %s', repr(raw_len)) raise exceptions.PDUError('Broken PDU') - raw_pdu = self._socket.recv(length - 4) - raw_pdu = raw_len + raw_pdu + logger.debug('Reading PDU of %s bytes', length) + raw_pdu = raw_len + while len(raw_pdu) < length: + new_data = self._socket.recv(length - len(raw_pdu)) + raw_pdu += new_data + if new_data == '': + raise exceptions.ConnectionError() logger.debug('<<%s (%d bytes)', binascii.b2a_hex(raw_pdu), len(raw_pdu)) @@ -233,19 +241,40 @@ def accept(self, obj): raise NotImplementedError('not implemented') def _message_received(self, p): - """Handler for received message event""" + """Handler for received message event, Return False from + message_received_handler to make your own deliver_sm_resp, + for example to handle asyc functions.""" status = self.message_received_handler(pdu=p) if status is None: status = consts.SMPP_ESME_ROK - dsmr = smpp.make_pdu('deliver_sm_resp', client=self, status=status) - #, message_id=args['pdu'].sm_default_msg_id) - dsmr.sequence = p.sequence - self.send_pdu(dsmr) + if status != False: + dsmr = smpp.make_pdu('deliver_sm_resp', client=self, status=status) + dsmr.sequence = p.sequence + self.send_pdu(dsmr) + + def _unbind_received(self, p): + """Response to unbind""" + status = self.unbind_received_handler(pdu=p) + if status is None: + status = consts.SMPP_ESME_ROK + if status != False: + resp_pdu = smpp.make_pdu( + 'unbind_resp', + client=self, + command_status=consts.SMPP_ESME_ROK + ) + logger.info('Unbind command received stopping sending.') + resp_pdu.sequence=p.sequence + self.send_pdu(resp_pdu) + logger.info('Unbind resp sent') + self.state = consts.SMPP_CLIENT_STATE_OPEN + raise exceptions.UnbindFromServer('Server made unbind') - def _enquire_link_received(self): + def _enquire_link_received(self, p): """Response to enquire_link""" ler = smpp.make_pdu('enquire_link_resp', client=self) #, message_id=args['pdu'].sm_default_msg_id) + ler.sequence = p.sequence self.send_pdu(ler) logger.debug("Link Enquiry...") @@ -261,10 +290,14 @@ def set_message_sent_handler(self, func): """Set new function to handle message sent event""" self.message_sent_handler = func + def set_unbind_received_handler(self, func): + """Set new function to handle message receive event""" + self.unbind_received_handler = func + @staticmethod def message_received_handler(pdu, **kwargs): - """Custom handler to process received message. May be overridden""" - + """Custom handler to process received message. + May be overridden""" logger.warning('Message received handler (Override me)') @staticmethod @@ -273,6 +306,11 @@ def message_sent_handler(pdu, **kwargs): May be overridden""" logger.warning('Message sent handler (Override me)') + @staticmethod + def unbind_received_handler(pdu, **kwargs): + """Called when SMPP server sends undbind. + May be overridden""" + logger.warning('Ubind from SMPP server (Override me)') def read_once(self, ignore_error_codes=None): """Read a PDU and act""" @@ -285,22 +323,26 @@ def read_once(self, ignore_error_codes=None): self.send_pdu(p) return - if p.is_error(): + if p.is_error() and not p.command =='submit_sm_resp' and not 'generic_nack': raise exceptions.PDUError( '({}) {}: {}'.format(p.status, p.command, consts.DESCRIPTIONS.get(p.status, 'Unknown status')), int(p.status)) - if p.command == 'unbind': # unbind_res - logger.info('Unbind command received') - return - elif p.command == 'submit_sm_resp': + if p.command == 'unbind': + self._unbind_received(p) + elif p.command in ['submit_sm_resp', 'generic_nack']: self.message_sent_handler(pdu=p) elif p.command == 'deliver_sm': self._message_received(p) elif p.command == 'enquire_link': - self._enquire_link_received() + self._enquire_link_received(p) elif p.command == 'enquire_link_resp': pass + elif p.command == 'unbind_resp': + if p.status == consts.SMPP_ESME_ROK: + self.run = False + else: + logger.warning('SMPP unbind failed with error "%s"', p.status) elif p.command == 'alert_notification': self._alert_notification(p) else: @@ -311,6 +353,9 @@ def read_once(self, ignore_error_codes=None): and e.args[1] in ignore_error_codes: logging.warning('(%d) %s. Ignored.' % (e.args[1], e.args[0])) + elif self.state == consts.SMPP_CLIENT_STATE_OPEN \ + or self.state == consts.SMPP_CLIENT_STATE_CLOSED: + raise exceptions.UnbindFromServer('Server unbind complete') else: raise @@ -322,9 +367,13 @@ def poll(self, ignore_error_codes=None): break self.read_once(ignore_error_codes) + def listen_stop(self): + self.run = False + def listen(self, ignore_error_codes=None): """Listen for PDUs and act""" - while True: + self.run = True + while self.run: self.read_once(ignore_error_codes) def send_message(self, **kwargs): diff --git a/smpplib/command.py b/smpplib/command.py index 6ae1211..a054c4b 100644 --- a/smpplib/command.py +++ b/smpplib/command.py @@ -71,9 +71,7 @@ def get_optional_name(code): if value == code: return key - raise exceptions.UnknownCommandError( - 'Unknown SMPP command code "0x%x"' % code) - + return "TLV_%x" % code def get_optional_code(name): """Return optional_params code by given command name. If name is unknown, @@ -347,8 +345,12 @@ def parse_optional_params(self, data): type_code, pos = unpack_short(data, pos) field = get_optional_name(type_code) length, pos = unpack_short(data, pos) - - param = self.params[field] + if field in self.params: + param = self.params[field] + else: + param = Param(type=str, size=length) + self.params[field] = param + logger.warning('Unkown TLV: %s', field) if param.type is int: data, pos = self._parse_int(field, data, pos) elif param.type in (str, ostr): @@ -444,7 +446,7 @@ class BindTransmitterResp(Command): """Response for bind as a transmitter command""" params = { - 'system_id': Param(type=str), + 'system_id': Param(type=str, max=16), 'sc_interface_version': Param(type=int, size=1), } @@ -570,7 +572,8 @@ def __init__(self, command, **kwargs): class GenericNAck(Command): """General Negative Acknowledgement class""" - _defs = [] + params_order = tuple() + parms = {} def __init__(self, command, **kwargs): """Initialize""" @@ -839,7 +842,7 @@ class Unbind(Command): def __init__(self, command, **kwargs): """Initialize""" - super(Unbind, self).__init__(command, need_sequence=False, **kwargs) + super(Unbind, self).__init__(command, **kwargs) class UnbindResp(Command): @@ -861,8 +864,7 @@ class EnquireLink(Command): def __init__(self, command, **kwargs): """Initialize""" - super(EnquireLink, self).__init__(command, need_sequence=False, - **kwargs) + super(EnquireLink, self).__init__(command, **kwargs) class EnquireLinkResp(Command): diff --git a/smpplib/exceptions.py b/smpplib/exceptions.py index 8f06f64..4389c75 100644 --- a/smpplib/exceptions.py +++ b/smpplib/exceptions.py @@ -11,9 +11,14 @@ class ConnectionError(Exception): """Connection error""" +class UnbindFromServer(Exception): + """Unbind from SMPP server""" + + class PDUError(RuntimeError): """Error processing PDU""" class MessageTooLong(ValueError): """Text too long to fit 255 SMS""" + diff --git a/smpplib/gsm.py b/smpplib/gsm.py index 4d51b1d..d51777a 100644 --- a/smpplib/gsm.py +++ b/smpplib/gsm.py @@ -36,6 +36,18 @@ def gsm_encode(plaintext, hex=False): raise EncodeError() return binascii.b2a_hex(res) if hex else res +def gsm_decode(instring, hex=False): + if hex: + instring = binascii.a2b_hex(instring) + chars = iter(instring) + result = [] + for c in chars: + if c == chr(27): + c = next(chars) + result.append(ext[ord(c)]) + else: + result.append(gsm[ord(c)]) + return ''.join(result) def make_parts(text): """Returns tuple(parts, encoding, esm_class)""" @@ -46,10 +58,11 @@ def make_parts(text): partsize = consts.SEVENBIT_MP_SIZE encode = six.b except EncodeError: + text = binascii.hexlify(text.encode('utf-16-be')) encoding = consts.SMPP_ENCODING_ISO10646 - need_split = len(text) > consts.UCS2_SIZE - partsize = consts.UCS2_MP_SIZE - encode = lambda s: s.encode('utf-16-be') + need_split = len(text) > consts.UCS2_SIZE * 4 + partsize = consts.UCS2_MP_SIZE * 4 + encode = lambda s: binascii.unhexlify(s) esm_class = consts.SMPP_MSGTYPE_DEFAULT diff --git a/smpplib/pdu.py b/smpplib/pdu.py index c74324e..ede8c14 100644 --- a/smpplib/pdu.py +++ b/smpplib/pdu.py @@ -106,6 +106,18 @@ def get_status_desc(self, status=None): return desc + def parse_udh(self): + """Parsing the UDH""" + (udh_lenght, ) = struct.unpack('>B', self.short_message[0:1]) + (udh_data_type, udh_data_lenght ) = struct.unpack('>BB', self.short_message[1:3]) + if udh_data_type == consts.SMPP_UDHIEIE_CONCATENATED and udh_data_lenght == 3: + ( + self.sar_msg_ref_num, + self.sar_total_segments, + self.sar_segment_seqnum + ) = struct.unpack('>BBB', self.short_message[3:3+udh_data_lenght]) + self.short_message = self.short_message[udh_lenght+1:] + def parse(self, data): """Parse raw PDU""" @@ -132,6 +144,9 @@ def parse(self, data): if len(data) > 16: self.parse_params(data[16:]) + if int(getattr(self, 'esm_class', '0')) & consts.SMPP_GSMFEAT_UDHI: + self.parse_udh() + def generate(self): """Generate raw PDU"""