├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.markdown ├── dev_requirements.txt ├── requirements.txt ├── setup.py └── smpp ├── __init__.py └── twisted ├── __init__.py ├── client.py ├── config.py ├── examples ├── __init__.py └── transceiver.py ├── protocol.py ├── server.py └── tests ├── __init__.py ├── smpp_client_test.py ├── smpp_protocol_test.py ├── smsc_simulator.py └── test_smpp_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg 4 | *.egg-info 5 | _trial_temp 6 | *~ 7 | *.swp 8 | build/ 9 | dist/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | install: 8 | - "pip install -r requirements.txt --use-mirrors" 9 | - "pip install -r dev_requirements.txt --use-mirrors" 10 | 11 | script: py.test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009-2010 Mozes, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.markdown 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | SMPP 3.4 client built on Twisted 2 | 3 | http://www.nowsms.com/discus/messages/1/24856.html 4 | 5 | Example 6 | ------- 7 | import logging 8 | from twisted.internet import reactor, defer 9 | from smpp.twisted.client import SMPPClientTransceiver, SMPPClientService 10 | from smpp.twisted.config import SMPPClientConfig 11 | 12 | class SMPP(object): 13 | 14 | def __init__(self, config=None): 15 | if config is None: 16 | config = SMPPClientConfig(host='localhost', port=999, username='uname', password='pwd') 17 | self.config = config 18 | 19 | @defer.inlineCallbacks 20 | def run(self): 21 | try: 22 | #Bind 23 | smpp = yield SMPPClientTransceiver(self.config, self.handleMsg).connectAndBind() 24 | #Wait for disconnect 25 | yield smpp.getDisconnectedDeferred() 26 | except Exception, e: 27 | print "ERROR: %s" % str(e) 28 | finally: 29 | reactor.stop() 30 | 31 | def handleMsg(self, smpp, pdu): 32 | """ 33 | NOTE: you can return a Deferred here 34 | """ 35 | print "Received pdu %s" % pdu 36 | 37 | if __name__ == '__main__': 38 | logging.basicConfig(level=logging.DEBUG) 39 | SMPP().run() 40 | reactor.run() 41 | 42 | Credits 43 | ======= 44 | * Thanks to [rtrdev](https://github.com/rtrdev) for adding support for SMPP servers 45 | * Thanks to [Fourat Zouari](https://github.com/fourat) for finding and fixing an enquirelinks bug -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | twisted 2 | enum 3 | pyOpenSSL 4 | smpp.pdu 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | from setuptools import setup, find_packages 8 | setup( 9 | name = "smpp.twisted", 10 | version = "0.4", 11 | author = "Roger Hoover", 12 | author_email = "roger.hoover@gmail.com", 13 | description = "SMPP 3.4 client built on Twisted", 14 | license = 'Apache License 2.0', 15 | packages = find_packages(), 16 | long_description=read('README.markdown'), 17 | keywords = "smpp twisted", 18 | url = "https://github.com/mozes/smpp.twisted", 19 | py_modules=["smpp.twisted"], 20 | include_package_data = True, 21 | package_data={'smpp.twisted': ['README.markdown']}, 22 | zip_safe = False, 23 | install_requires = [ 24 | 'twisted', 25 | 'enum', 26 | 'pyOpenSSL', 27 | 'smpp.pdu', 28 | ], 29 | tests_require = [ 30 | 'mock', 31 | ], 32 | test_suite = 'smpp.twisted.tests', 33 | classifiers=[ 34 | "Development Status :: 5 - Production/Stable", 35 | "Framework :: Twisted", 36 | "Topic :: System :: Networking", 37 | "Operating System :: OS Independent", 38 | "License :: OSI Approved :: Apache Software License", 39 | "Intended Audience :: Developers", 40 | "Programming Language :: Python", 41 | "Topic :: Software Development :: Libraries :: Python Modules", 42 | ], 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /smpp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | __import__('pkg_resources').declare_namespace(__name__) -------------------------------------------------------------------------------- /smpp/twisted/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | -------------------------------------------------------------------------------- /smpp/twisted/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | import logging 17 | from OpenSSL import SSL 18 | from twisted.internet.protocol import ClientFactory 19 | from twisted.internet import defer, reactor, ssl 20 | from twisted.application import service 21 | from smpp.twisted.protocol import SMPPClientProtocol, DataHandlerResponse 22 | 23 | LOG_CATEGORY="smpp.twisted.client" 24 | 25 | class SMPPClientFactory(ClientFactory): 26 | 27 | protocol = SMPPClientProtocol 28 | 29 | def __init__(self, config): 30 | self.config = config 31 | self.buildProtocolDeferred = defer.Deferred() 32 | self.log = logging.getLogger(LOG_CATEGORY) 33 | 34 | def getConfig(self): 35 | return self.config 36 | 37 | def buildProtocol(self, addr): 38 | p = ClientFactory.buildProtocol(self, addr) 39 | #This is a sneaky way of passing the protocol instance back to the caller 40 | reactor.callLater(0, self.buildProtocolDeferred.callback, p) 41 | return p 42 | 43 | def clientConnectionFailed(self, connector, reason): 44 | """Connection failed 45 | """ 46 | self.log.error("Connection failed. Reason: %s" % str(reason)) 47 | self.buildProtocolDeferred.errback(reason) 48 | 49 | class CtxFactory(ssl.ClientContextFactory): 50 | 51 | def __init__(self, config): 52 | self.smppConfig = config 53 | 54 | def getContext(self): 55 | self.method = SSL.SSLv23_METHOD 56 | ctx = ssl.ClientContextFactory.getContext(self) 57 | if self.smppConfig.SSLCertificateFile: 58 | ctx.use_certificate_file(self.smppConfig.SSLCertificateFile) 59 | return ctx 60 | 61 | class SMPPClientBase(object): 62 | msgHandler = None 63 | 64 | def __init__(self, config): 65 | self.config = config 66 | self.log = logging.getLogger(LOG_CATEGORY) 67 | self.smpp = None 68 | self.bindDeferred = None 69 | 70 | def connect(self): 71 | factory = SMPPClientFactory(self.config) 72 | if self.config.useSSL: 73 | self.log.warning('Establishing SSL connection to %s:%d' % (self.config.host, self.config.port)) 74 | reactor.connectSSL(self.config.host, self.config.port, factory, CtxFactory(self.config)) 75 | else: 76 | self.log.warning('Establishing TCP connection to %s:%d' % (self.config.host, self.config.port)) 77 | reactor.connectTCP(self.config.host, self.config.port, factory) 78 | return factory.buildProtocolDeferred.addCallback(self.onConnect) 79 | 80 | def onConnect(self, smpp): 81 | self.smpp = smpp 82 | if self.msgHandler is not None: 83 | smpp.setDataRequestHandler(self.msgHandler) 84 | return smpp 85 | 86 | def connectAndBind(self): 87 | self.bindDeferred = defer.Deferred() 88 | self.connect().addCallback(self.doBind).addErrback(self.bindDeferred.errback) 89 | return self.bindDeferred 90 | 91 | def doBind(self, smpp): 92 | self.bind(smpp).addCallback(self.bound).addErrback(self.bindFailed, smpp) 93 | return smpp 94 | 95 | def bind(self, smpp): 96 | raise NotImplementedError() 97 | 98 | #If bind fails, don't errback until we're disconnected 99 | def bindFailed(self, error, smpp): 100 | smpp.getDisconnectedDeferred().addCallback(lambda result: self.bindDeferred.errback(error)) 101 | 102 | def bound(self, result): 103 | self.bindDeferred.callback(result.smpp) 104 | 105 | class SMPPClientTransmitter(SMPPClientBase): 106 | 107 | def bind(self, smpp): 108 | return smpp.bindAsTransmitter() 109 | 110 | class SMPPClientReceiver(SMPPClientBase): 111 | 112 | def __init__(self, config, msgHandler): 113 | SMPPClientBase.__init__(self, config) 114 | self.msgHandler = msgHandler 115 | 116 | def bind(self, smpp): 117 | return smpp.bindAsReceiver(self.msgHandler) 118 | 119 | class SMPPClientTransceiver(SMPPClientReceiver): 120 | 121 | def bind(self, smpp): 122 | return smpp.bindAsTransceiver(self.msgHandler) 123 | 124 | #TODO - move this to mozes code base since 125 | # the service support in Twisted is so crappy 126 | class SMPPClientService(service.Service): 127 | 128 | def __init__(self, smppClient): 129 | self.client = smppClient 130 | self.stopDeferred = defer.Deferred() 131 | self.log = logging.getLogger(LOG_CATEGORY) 132 | 133 | def getStopDeferred(self): 134 | return self.stopDeferred 135 | 136 | @defer.inlineCallbacks 137 | def startService(self): 138 | service.Service.startService(self) 139 | bindDeferred = self.client.connectAndBind() 140 | bindDeferred.addErrback(self.handleStartError) 141 | smpp = yield bindDeferred 142 | smpp.getDisconnectedDeferred().chainDeferred(self.stopDeferred) 143 | defer.returnValue(smpp) 144 | 145 | def handleStartError(self, error): 146 | self.stopDeferred.errback(error) 147 | return error 148 | 149 | def stopService(self): 150 | service.Service.stopService(self) 151 | if self.client.smpp: 152 | self.log.info("Stopping SMPP Client") 153 | return self.client.smpp.unbindAndDisconnect() 154 | 155 | -------------------------------------------------------------------------------- /smpp/twisted/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | class SMPPConfig(object): 17 | 18 | def __init__(self, **kwargs): 19 | self.sessionInitTimerSecs = kwargs.get('sessionInitTimerSecs', 30) 20 | self.enquireLinkTimerSecs = kwargs.get('enquireLinkTimerSecs', 10) 21 | self.inactivityTimerSecs = kwargs.get('inactivityTimerSecs', 120) 22 | self.responseTimerSecs = kwargs.get('responseTimerSecs', 60) 23 | self.pduReadTimerSecs = kwargs.get('pduReadTimerSecs', 10) 24 | 25 | 26 | class SMPPClientConfig(SMPPConfig): 27 | 28 | def __init__(self, **kwargs): 29 | super(SMPPClientConfig, self).__init__(**kwargs) 30 | self.port = kwargs['port'] 31 | self.host = kwargs['host'] 32 | self.username = kwargs['username'] 33 | self.password = kwargs['password'] 34 | self.systemType = kwargs.get('systemType', '') 35 | self.useSSL = kwargs.get('useSSL', False) 36 | self.SSLCertificateFile = kwargs.get('SSLCertificateFile', None) 37 | self.addressRange = kwargs.get('addressRange', None) 38 | self.addressTon = kwargs.get('addressTon', None) 39 | self.addressNpi = kwargs.get('addressNpi', None) 40 | 41 | class SMPPServerConfig(SMPPConfig): 42 | 43 | def __init__(self, **kwargs): 44 | """ 45 | @param systems: A dict of data representing the available 46 | systems. 47 | { "username1": {"max_bindings" : 2}, 48 | "username2": {"max_bindings" : 1} 49 | } 50 | """ 51 | super(SMPPServerConfig, self).__init__(**kwargs) 52 | self.systems = kwargs.get('systems', {}) 53 | self.msgHandler = kwargs['msgHandler'] 54 | 55 | -------------------------------------------------------------------------------- /smpp/twisted/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozes/smpp.twisted/887e1640532d93929187d6b9541c686f019dab45/smpp/twisted/examples/__init__.py -------------------------------------------------------------------------------- /smpp/twisted/examples/transceiver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from twisted.internet import reactor, defer 3 | from smpp.twisted.client import SMPPClientTransceiver, SMPPClientService 4 | from smpp.twisted.config import SMPPClientConfig 5 | 6 | class SMPP(object): 7 | 8 | def __init__(self, config=None): 9 | if config is None: 10 | config = SMPPClientConfig(host='localhost', port=999, username='uname', password='pwd') 11 | 12 | # Uncomment line below to recv SMS via #322223322 only 13 | # config = SMPPClientConfig(host='localhost', port=999, username='uname', password='pwd', addressTon=AddrTon.UNKNOWN, addressNpi=AddrNpi.ISDN, addressRange='^322223322$') 14 | self.config = config 15 | 16 | @defer.inlineCallbacks 17 | def run(self): 18 | try: 19 | #Bind 20 | smpp = yield SMPPClientTransceiver(self.config, self.handleMsg).connectAndBind() 21 | #Wait for disconnect 22 | yield smpp.getDisconnectedDeferred() 23 | except Exception, e: 24 | print "ERROR: %s" % str(e) 25 | finally: 26 | reactor.stop() 27 | 28 | def handleMsg(self, smpp, pdu): 29 | """ 30 | NOTE: you can return a Deferred here 31 | """ 32 | print "Received pdu %s" % pdu 33 | 34 | if __name__ == '__main__': 35 | logging.basicConfig(level=logging.DEBUG) 36 | SMPP().run() 37 | reactor.run() 38 | -------------------------------------------------------------------------------- /smpp/twisted/protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | import struct, logging, StringIO, binascii 18 | from enum import Enum 19 | 20 | from smpp.pdu.namedtuple import namedtuple 21 | from smpp.pdu.operations import * 22 | from smpp.pdu.pdu_encoding import PDUEncoder 23 | from smpp.pdu.pdu_types import PDURequest, PDUResponse, PDUDataRequest, CommandStatus 24 | from smpp.pdu.error import * 25 | from smpp.pdu.constants import command_status_name_map 26 | 27 | from twisted.internet import protocol, defer, reactor 28 | from twisted.internet.defer import inlineCallbacks 29 | from twisted.cred import error 30 | import exceptions 31 | 32 | 33 | LOG_CATEGORY="smpp.twisted.protocol" 34 | 35 | SMPPSessionStates = Enum( 36 | 'NONE', 37 | 'OPEN', 38 | 'BIND_TX_PENDING', 39 | 'BOUND_TX', 40 | 'BIND_RX_PENDING', 41 | 'BOUND_RX', 42 | 'BIND_TRX_PENDING', 43 | 'BOUND_TRX', 44 | 'UNBIND_PENDING', 45 | 'UNBIND_RECEIVED', 46 | 'UNBOUND' 47 | ) 48 | 49 | SMPPOutboundTxn = namedtuple('SMPPOutboundTxn', 'request, timer, ackDeferred') 50 | SMPPOutboundTxnResult = namedtuple('SMPPOutboundTxnResult', 'smpp, request, response') 51 | 52 | def _safelylogOutPdu(content): 53 | try: 54 | return binascii.b2a_hex(content) 55 | except exceptions.UnicodeEncodeError: 56 | return "Couldn't log out the pdu content due to non-ascii characters." 57 | 58 | 59 | class DataHandlerResponse(object): 60 | 61 | def __init__(self, status, **params): 62 | self.status = status 63 | self.params = params 64 | 65 | class SMPPProtocolBase( protocol.Protocol ): 66 | """Short Message Peer to Peer Protocol v3.4 implementing ESME (client)""" 67 | version = 0x34 68 | 69 | def __init__( self ): 70 | self.recvBuffer = "" 71 | self.connectionCorrupted = False 72 | self.pduReadTimer = None 73 | self.enquireLinkTimer = None 74 | self.inactivityTimer = None 75 | self.dataRequestHandler = None 76 | self.lastSeqNum = 0 77 | self.inTxns = {} 78 | self.outTxns = {} 79 | self.sessionState = SMPPSessionStates.NONE 80 | self.encoder = PDUEncoder() 81 | self.disconnectedDeferred = defer.Deferred() 82 | # Overriden in tests 83 | self.callLater = reactor.callLater 84 | self.port = None 85 | 86 | def config(self): 87 | return self.factory.getConfig() 88 | 89 | def connectionMade(self): 90 | """When TCP connection is made 91 | """ 92 | protocol.Protocol.connectionMade(self) 93 | self.port = self.transport.getHost().port 94 | #Start the inactivity timer the connection is dropped if we receive no data 95 | self.activateInactivityTimer() 96 | self.sessionState = SMPPSessionStates.OPEN 97 | self.log.warning("SMPP connection established from %s to port %s", self.transport.getPeer().host, self.port) 98 | 99 | def connectionLost( self, reason ): 100 | protocol.Protocol.connectionLost( self, reason ) 101 | self.log.warning("SMPP %s disconnected from port %s: %s", self.transport.getPeer().host, self.port, reason) 102 | 103 | self.sessionState = SMPPSessionStates.NONE 104 | 105 | self.cancelEnquireLinkTimer() 106 | self.cancelInactivityTimer() 107 | 108 | self.disconnectedDeferred.callback(None) 109 | 110 | def dataReceived( self, data ): 111 | """ Looks for a full PDU (protocol data unit) and passes it from 112 | rawMessageReceived. 113 | """ 114 | # if self.log.isEnabledFor(logging.DEBUG): 115 | # self.log.debug("Received data [%s]" % _safelylogOutPdu(data)) 116 | 117 | self.recvBuffer = self.recvBuffer + data 118 | 119 | while True: 120 | if self.connectionCorrupted: 121 | return 122 | msg = self.readMessage() 123 | if msg is None: 124 | break 125 | self.endPDURead() 126 | self.rawMessageReceived(msg) 127 | 128 | if len(self.recvBuffer) > 0: 129 | self.incompletePDURead() 130 | 131 | def incompletePDURead(self): 132 | if self.pduReadTimer and self.pduReadTimer.active(): 133 | return 134 | self.pduReadTimer = self.callLater(self.config().pduReadTimerSecs, self.onPDUReadTimeout) 135 | 136 | def endPDURead(self): 137 | if self.pduReadTimer and self.pduReadTimer.active(): 138 | self.pduReadTimer.cancel() 139 | 140 | def readMessage(self): 141 | pduLen = self.getMessageLength() 142 | if pduLen is None: 143 | return None 144 | return self.getMessage(pduLen) 145 | 146 | def getMessageLength(self): 147 | if len(self.recvBuffer) < 4: 148 | return None 149 | return struct.unpack('!L', self.recvBuffer[:4])[0] 150 | 151 | def getMessage(self, pduLen): 152 | if len(self.recvBuffer) < pduLen: 153 | return None 154 | 155 | message = self.recvBuffer[:pduLen] 156 | self.recvBuffer = self.recvBuffer[pduLen:] 157 | return message 158 | 159 | def corruptDataRecvd(self, status=CommandStatus.ESME_RINVCMDLEN): 160 | self.sendPDU(GenericNack(status=status)) 161 | self.onCorruptConnection() 162 | 163 | def onCorruptConnection(self): 164 | """ Once the connection is corrupt, the PDU boundaries are lost and it's impossible to 165 | continue processing messages. 166 | - Set a flag to indicate corrupt connection 167 | - no more parse attempts should be made for inbound data 168 | - no more outbound requests should be attempted (they should errback immediately) 169 | - Cancel outstanding outbound requests (which have not yet been ack'ed) 170 | (removed from the list and errback called) 171 | - Shutdown 172 | """ 173 | self.log.critical("Connection is corrupt!!! Shutting down...") 174 | self.connectionCorrupted = True 175 | self.cancelOutboundTransactions(SMPPClientConnectionCorruptedError()) 176 | self.shutdown() 177 | 178 | def getHeader(self, message): 179 | try: 180 | return self.encoder.decodeHeader(StringIO.StringIO(message[:self.encoder.HEADER_LEN])) 181 | except: 182 | return {} 183 | 184 | def onPDUReadTimeout(self): 185 | self.log.critical('PDU read timed out. Buffer is now considered corrupt') 186 | self.corruptDataRecvd() 187 | 188 | def rawMessageReceived( self, message ): 189 | """Called once a PDU (protocol data unit) boundary is identified. 190 | 191 | Creates an SMPP PDU class from the data and calls PDUReceived dispatcher 192 | """ 193 | pdu = None 194 | try: 195 | pdu = self.encoder.decode(StringIO.StringIO(message)) 196 | except PDUCorruptError, e: 197 | self.log.exception(e) 198 | self.log.critical("Received corrupt PDU %s" % _safelylogOutPdu(message)) 199 | self.corruptDataRecvd(status=e.status) 200 | except PDUParseError, e: 201 | self.log.exception(e) 202 | self.log.critical("Received unparsable PDU %s" % _safelylogOutPdu(message)) 203 | header = self.getHeader(message) 204 | seqNum = header.get('sequence_number', None) 205 | commandId = header.get('command_id', None) 206 | self.sendPDU(getPDUClass(commandId).requireAck(seqNum=seqNum, status=e.status)) 207 | else: 208 | self.PDUReceived(pdu) 209 | 210 | def PDUReceived( self, pdu ): 211 | """Dispatches incoming PDUs 212 | """ 213 | if self.log.isEnabledFor(logging.DEBUG): 214 | self.log.debug("Received PDU: %s" % pdu) 215 | 216 | encoded = self.encoder.encode(pdu) 217 | 218 | if self.log.isEnabledFor(logging.DEBUG): 219 | self.log.debug("Receiving data [%s]" % _safelylogOutPdu(encoded)) 220 | 221 | #Signal SMPP operation 222 | self.onSMPPOperation() 223 | 224 | if isinstance(pdu, PDURequest): 225 | self.PDURequestReceived(pdu) 226 | elif isinstance(pdu, PDUResponse): 227 | self.PDUResponseReceived(pdu) 228 | else: 229 | getattr(self, "onPDU_%s" % str(pdu.id))(pdu) 230 | 231 | def PDURequestReceived(self, reqPDU): 232 | """Handle incoming request PDUs 233 | """ 234 | if isinstance(reqPDU, PDUDataRequest): 235 | self.PDUDataRequestReceived(reqPDU) 236 | return 237 | 238 | getattr(self, "onPDURequest_%s" % str(reqPDU.id))(reqPDU) 239 | 240 | def onPDURequest_enquire_link(self, reqPDU): 241 | self.sendResponse(reqPDU) 242 | 243 | def onPDURequest_unbind(self, reqPDU): 244 | #Allow no more outbound data requests 245 | #Accept no more inbound requests 246 | self.sessionState = SMPPSessionStates.UNBIND_RECEIVED 247 | self.cancelEnquireLinkTimer() 248 | #Cancel outbound requests 249 | self.cancelOutboundTransactions(SMPPClientSessionStateError('Unbind received')) 250 | #Wait for inbound requests to finish then ack and disconnect 251 | self.finishInboundTxns().addCallback(lambda r: (self.sendResponse(reqPDU) or True) and self.disconnect()) 252 | 253 | def sendResponse(self, reqPDU, status=CommandStatus.ESME_ROK, **params): 254 | self.sendPDU(reqPDU.requireAck(reqPDU.seqNum, status, **params)) 255 | 256 | def PDUDataRequestReceived(self, reqPDU): 257 | if self.sessionState == SMPPSessionStates.UNBIND_PENDING: 258 | self.log.info("Unbind is pending...Ignoring data request PDU %s" % reqPDU) 259 | return 260 | 261 | if not self.isBound(): 262 | errMsg = 'Received data request when not bound %s' % reqPDU 263 | self.cancelOutboundTransactions(SessionStateError(errMsg, CommandStatus.ESME_RINVBNDSTS)) 264 | return self.fatalErrorOnRequest(reqPDU, errMsg, CommandStatus.ESME_RINVBNDSTS) 265 | 266 | if self.dataRequestHandler is None: 267 | return self.fatalErrorOnRequest(reqPDU, 'Missing dataRequestHandler', CommandStatus.ESME_RX_T_APPN) 268 | 269 | self.doPDURequest(reqPDU, self.dataRequestHandler) 270 | 271 | def fatalErrorOnRequest(self, reqPDU, errMsg, status): 272 | self.log.critical(errMsg) 273 | self.sendResponse(reqPDU, status) 274 | self.shutdown() 275 | 276 | def doPDURequest(self, reqPDU, handler): 277 | self.startInboundTransaction(reqPDU) 278 | 279 | handlerCall = defer.maybeDeferred(handler, self, reqPDU) 280 | handlerCall.addCallback(self.PDURequestSucceeded, reqPDU) 281 | handlerCall.addErrback(self.PDURequestFailed, reqPDU) 282 | handlerCall.addBoth(self.PDURequestFinished, reqPDU) 283 | 284 | def PDURequestSucceeded(self, dataHdlrResp, reqPDU): 285 | if reqPDU.requireAck: 286 | status = CommandStatus.ESME_ROK 287 | params = {} 288 | if dataHdlrResp: 289 | if dataHdlrResp in CommandStatus: 290 | status = dataHdlrResp 291 | elif isinstance(dataHdlrResp, DataHandlerResponse): 292 | status = dataHdlrResp.status 293 | params = dataHdlrResp.params 294 | else: 295 | self.log.critical("Invalid response type returned from data handler %s" % type(dataHdlrResp)) 296 | status = CommandStatus.ESME_RX_T_APPN 297 | self.shutdown() 298 | 299 | self.sendResponse(reqPDU, status, **params) 300 | 301 | def PDURequestFailed(self, error, reqPDU): 302 | if error.check(SMPPProtocolError): 303 | # Get the original error 304 | try: 305 | error.raiseException() 306 | except SMPPProtocolError as validation_error: 307 | self.log.debug("Application raised error '%s', forwarding to client. Inbound PDU was [%s], hex[%s]" % (validation_error, reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU)))) 308 | return_cmd_status = validation_error.commandStatusName 309 | shutdown = False 310 | else: 311 | self.log.critical('Exception raised handling inbound PDU [%s] hex[%s]: %s' % (reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU)), error)) 312 | return_cmd_status = CommandStatus.ESME_RX_T_APPN 313 | shutdown = True 314 | 315 | if reqPDU.requireAck: 316 | self.sendResponse(reqPDU, return_cmd_status) 317 | 318 | if shutdown: 319 | self.shutdown() 320 | 321 | def PDURequestFinished(self, result, reqPDU): 322 | self.endInboundTransaction(reqPDU) 323 | return result 324 | 325 | def finishTxns(self): 326 | return defer.DeferredList([self.finishInboundTxns(), self.finishOutboundTxns()]) 327 | 328 | def finishInboundTxns(self): 329 | return defer.DeferredList(self.inTxns.values()) 330 | 331 | def finishOutboundTxns(self): 332 | return defer.DeferredList([txn.ackDeferred for txn in self.outTxns.values()]) 333 | 334 | def PDUResponseReceived(self, pdu): 335 | """Handle incoming response PDUs 336 | """ 337 | if isinstance(pdu, GenericNack): 338 | self.log.critical("Recevied generic_nack %s" % pdu) 339 | if pdu.seqNum is None: 340 | self.onCorruptConnection() 341 | return 342 | 343 | if pdu.seqNum not in self.outTxns: 344 | self.log.critical('Response PDU received with unknown outbound transaction sequence number %s' % pdu) 345 | return 346 | 347 | self.endOutboundTransaction(pdu) 348 | 349 | def sendPDU(self, pdu): 350 | """Send a SMPP PDU 351 | """ 352 | if self.log.isEnabledFor(logging.DEBUG): 353 | self.log.debug("Sending PDU: %s" % pdu) 354 | encoded = self.encoder.encode(pdu) 355 | 356 | if self.log.isEnabledFor(logging.DEBUG): 357 | self.log.debug("Sending data [%s]" % _safelylogOutPdu(encoded)) 358 | 359 | self.transport.write( encoded ) 360 | self.onSMPPOperation() 361 | 362 | def sendBindRequest(self, pdu): 363 | return self.sendRequest(pdu, self.config().sessionInitTimerSecs) 364 | 365 | def sendRequest(self, pdu, timeout): 366 | return defer.maybeDeferred(self.doSendRequest, pdu, timeout) 367 | 368 | def doSendRequest(self, pdu, timeout): 369 | if self.connectionCorrupted: 370 | raise SMPPClientConnectionCorruptedError() 371 | if not isinstance( pdu, PDURequest ) or pdu.requireAck is None: 372 | raise SMPPClientError("Invalid PDU to send: %s" % pdu) 373 | 374 | pdu.seqNum = self.claimSeqNum() 375 | self.sendPDU(pdu) 376 | return self.startOutboundTransaction(pdu, timeout) 377 | 378 | def onSMPPOperation(self): 379 | """Called whenever an SMPP PDU is sent or received 380 | """ 381 | if self.isBound(): 382 | self.activateEnquireLinkTimer() 383 | 384 | self.activateInactivityTimer() 385 | 386 | def activateEnquireLinkTimer(self): 387 | if self.enquireLinkTimer and self.enquireLinkTimer.active(): 388 | self.enquireLinkTimer.reset(self.config().enquireLinkTimerSecs) 389 | elif self.config().enquireLinkTimerSecs: 390 | self.enquireLinkTimer = self.callLater(self.config().enquireLinkTimerSecs, self.enquireLinkTimerExpired) 391 | 392 | def activateInactivityTimer(self): 393 | if self.inactivityTimer and self.inactivityTimer.active(): 394 | self.inactivityTimer.reset(self.config().inactivityTimerSecs) 395 | elif self.config().inactivityTimerSecs: 396 | self.inactivityTimer = self.callLater(self.config().inactivityTimerSecs, self.inactivityTimerExpired) 397 | 398 | def cancelEnquireLinkTimer(self): 399 | if self.enquireLinkTimer and self.enquireLinkTimer.active(): 400 | self.enquireLinkTimer.cancel() 401 | self.enquireLinkTimer = None 402 | 403 | def cancelInactivityTimer(self): 404 | if self.inactivityTimer and self.inactivityTimer.active(): 405 | self.inactivityTimer.cancel() 406 | self.inactivityTimer = None 407 | 408 | def enquireLinkTimerExpired(self): 409 | txn = self.sendRequest(EnquireLink(), self.config().responseTimerSecs) 410 | txn.addErrback(self.enquireLinkErr) 411 | 412 | def enquireLinkErr(self, failure): 413 | # Unbinding already anyway. No need to raise another error 414 | failure.trap(SMPPError) 415 | 416 | def inactivityTimerExpired(self): 417 | self.log.critical("Inactivity timer expired...shutting down") 418 | self.shutdown() 419 | 420 | def isBound(self): 421 | return self.sessionState in (SMPPSessionStates.BOUND_TX, SMPPSessionStates.BOUND_RX, SMPPSessionStates.BOUND_TRX) 422 | 423 | def shutdown(self): 424 | """ Unbind if appropriate and disconnect """ 425 | 426 | if self.isBound() and not self.connectionCorrupted: 427 | self.log.warning("Shutdown requested...unbinding") 428 | self.unbind().addBoth(lambda result: self.disconnect()) 429 | elif self.sessionState not in (SMPPSessionStates.UNBIND_RECEIVED, SMPPSessionStates.UNBIND_PENDING): 430 | self.log.warning("Shutdown requested...disconnecting") 431 | self.disconnect() 432 | else: 433 | self.log.debug("Shutdown already in progress") 434 | 435 | def startInboundTransaction(self, reqPDU): 436 | if reqPDU.seqNum in self.inTxns: 437 | raise SMPPProtocolError('Duplicate message id [%s] received. Already in progess.' % reqPDU.seqNum, CommandStatus.ESME_RUNKNOWNERR) 438 | txnDeferred = defer.Deferred() 439 | self.inTxns[reqPDU.seqNum] = txnDeferred 440 | self.log.debug("Inbound transaction started with message id %s" % reqPDU.seqNum) 441 | return txnDeferred 442 | 443 | def endInboundTransaction(self, reqPDU): 444 | if not reqPDU.seqNum in self.inTxns: 445 | raise ValueError('Unknown inbound sequence number in transaction for request PDU %s' % reqPDU) 446 | 447 | self.log.debug("Inbound transaction finished with message id %s" % reqPDU.seqNum) 448 | self.inTxns[reqPDU.seqNum].callback(reqPDU) 449 | del self.inTxns[reqPDU.seqNum] 450 | 451 | def startOutboundTransaction(self, reqPDU, timeout): 452 | if reqPDU.seqNum in self.outTxns: 453 | raise ValueError('Seq number [%s] is already in progess.' % reqPDU.seqNum) 454 | 455 | #Create callback deferred 456 | ackDeferred = defer.Deferred() 457 | #Create response timer 458 | timer = self.callLater(timeout, self.onResponseTimeout, reqPDU, timeout) 459 | #Save transaction 460 | self.outTxns[reqPDU.seqNum] = SMPPOutboundTxn(reqPDU, timer, ackDeferred) 461 | self.log.debug("Outbound transaction started with message id %s" % reqPDU.seqNum) 462 | return ackDeferred 463 | 464 | def closeOutboundTransaction(self, seqNum): 465 | self.log.debug("Outbound transaction finished with message id %s" % seqNum) 466 | 467 | txn = self.outTxns[seqNum] 468 | #Remove txn 469 | del self.outTxns[seqNum] 470 | #Cancel response timer 471 | if txn.timer.active(): 472 | txn.timer.cancel() 473 | return txn 474 | 475 | def endOutboundTransaction(self, respPDU): 476 | txn = self.closeOutboundTransaction(respPDU.seqNum) 477 | 478 | if respPDU.status == CommandStatus.ESME_ROK: 479 | if not isinstance(respPDU, txn.request.requireAck): 480 | txn.ackDeferred.errback(SMPPProtocolError("Invalid PDU response type [%s] returned for request type [%s]" % (type(respPDU), type(txn.request)))) 481 | return 482 | #Do callback 483 | txn.ackDeferred.callback(SMPPOutboundTxnResult(self, txn.request, respPDU)) 484 | return 485 | 486 | if isinstance(respPDU, GenericNack): 487 | txn.ackDeferred.errback(SMPPGenericNackTransactionError(respPDU, txn.request)) 488 | return 489 | 490 | errCode = respPDU.status 491 | txn.ackDeferred.errback(SMPPTransactionError(respPDU, txn.request)) 492 | 493 | def endOutboundTransactionErr(self, reqPDU, error): 494 | self.log.error(error) 495 | txn = self.closeOutboundTransaction(reqPDU.seqNum) 496 | #Do errback 497 | txn.ackDeferred.errback(error) 498 | 499 | def cancelOutboundTransactions(self, error): 500 | for txn in self.outTxns.values(): 501 | self.endOutboundTransactionErr(txn.request, error) 502 | 503 | def onResponseTimeout(self, reqPDU, timeout): 504 | errMsg = 'Request timed out after %s secs: %s' % (timeout, reqPDU) 505 | self.endOutboundTransactionErr(reqPDU, SMPPRequestTimoutError(errMsg)) 506 | self.shutdown() 507 | 508 | def claimSeqNum(self): 509 | self.lastSeqNum += 1 510 | return self.lastSeqNum 511 | 512 | def unbindSucceeded(self, result): 513 | self.sessionState = SMPPSessionStates.UNBOUND 514 | self.log.warning("Unbind succeeded") 515 | return result 516 | 517 | def unbindFailed(self, reason): 518 | self.log.error("Unbind failed [%s]. Disconnecting..." % reason) 519 | self.disconnect() 520 | if reason.check(SMPPRequestTimoutError): 521 | raise SMPPSessionInitTimoutError(str(reason)) 522 | return reason 523 | 524 | def unbindAfterInProgressTxnsFinished(self, result, unbindDeferred): 525 | self.log.warning('Issuing unbind request') 526 | self.sendBindRequest(Unbind()).addCallbacks(self.unbindSucceeded, self.unbindFailed).chainDeferred(unbindDeferred) 527 | 528 | ############################################################################ 529 | # Public command functions 530 | ############################################################################ 531 | def unbind(self): 532 | """Unbind from SMSC 533 | 534 | Result is a Deferred object 535 | """ 536 | if not self.isBound(): 537 | return defer.fail(SMPPClientSessionStateError('unbind called with illegal session state: %s' % self.sessionState)) 538 | 539 | self.cancelEnquireLinkTimer() 540 | 541 | self.log.info('Waiting for in-progress transactions to finish...') 542 | 543 | #Signal that 544 | # - no new data requests should be sent 545 | # - no new incoming data requests should be accepted 546 | self.sessionState = SMPPSessionStates.UNBIND_PENDING 547 | 548 | unbindDeferred = defer.Deferred() 549 | #Wait for any in-progress txns to finish 550 | self.finishTxns().addCallback(self.unbindAfterInProgressTxnsFinished, unbindDeferred) 551 | #Result is the deferred for the unbind txn 552 | return unbindDeferred 553 | 554 | def unbindAndDisconnect(self): 555 | """Unbind from SMSC and disconnect 556 | 557 | Result is a Deferred object 558 | """ 559 | return self.unbind().addBoth(lambda result: self.disconnect()) 560 | 561 | def disconnect(self): 562 | """Disconnect from SMSC 563 | """ 564 | if self.isBound(): 565 | self.log.warning("Disconnecting while bound to SMSC...") 566 | else: 567 | self.log.warning("Disconnecting...") 568 | self.sessionState = SMPPSessionStates.UNBOUND 569 | self.transport.loseConnection() 570 | 571 | def getDisconnectedDeferred(self): 572 | """Get a Deferred so you can be notified on disconnect 573 | """ 574 | return self.disconnectedDeferred 575 | 576 | def sendDataRequest( self, pdu ): 577 | """Send a SMPP Request Message 578 | 579 | Argument is an SMPP PDUDataRequest (protocol data unit). 580 | Result is a Deferred object 581 | """ 582 | if not isinstance( pdu, PDUDataRequest ): 583 | return defer.fail(SMPPClientError("Invalid PDU passed to sendDataRequest(): %s" % pdu)) 584 | if not self.isBound(): 585 | return defer.fail(SMPPClientSessionStateError('Not bound')) 586 | return self.sendRequest(pdu, self.config().responseTimerSecs) 587 | 588 | 589 | class SMPPClientProtocol(SMPPProtocolBase): 590 | 591 | def __init__(self): 592 | self.log = logging.getLogger(LOG_CATEGORY) 593 | SMPPProtocolBase.__init__(self) 594 | 595 | self.alertNotificationHandler = None 596 | 597 | def PDUReceived( self, pdu ): 598 | """Dispatches incoming PDUs 599 | """ 600 | self.log.info("SMPP Client received PDU [command: %s, sequence_number: %s, command_status: %s]" % (pdu.id, pdu.seqNum, pdu.status)) 601 | SMPPProtocolBase.PDUReceived(self, pdu) 602 | 603 | def bind(self, pdu, pendingState, boundState): 604 | if self.sessionState != SMPPSessionStates.OPEN: 605 | return defer.fail(SMPPClientSessionStateError('bind called with illegal session state: %s' % self.sessionState)) 606 | 607 | bindDeferred = self.sendBindRequest(pdu) 608 | bindDeferred.addCallback(self.bindSucceeded, boundState) 609 | bindDeferred.addErrback(self.bindFailed) 610 | self.sessionState = pendingState 611 | return bindDeferred 612 | 613 | def doBindAsReceiver(self): 614 | self.log.warning('Requesting bind as receiver') 615 | pdu = BindReceiver( 616 | system_id = self.config().username, 617 | password = self.config().password, 618 | system_type = self.config().systemType, 619 | address_range = self.config().addressRange, 620 | addr_ton = self.config().addressTon, 621 | addr_npi = self.config().addressNpi, 622 | interface_version = self.version 623 | ) 624 | return self.bind(pdu, SMPPSessionStates.BIND_RX_PENDING, SMPPSessionStates.BOUND_RX) 625 | 626 | def bindSucceeded(self, result, nextState): 627 | self.sessionState = nextState 628 | self.log.warning("Bind succeeded...now in state %s" % str(self.sessionState)) 629 | self.activateEnquireLinkTimer() 630 | return result 631 | 632 | def bindFailed(self, reason): 633 | self.log.error("Bind failed [%s]. Disconnecting..." % reason) 634 | self.disconnect() 635 | if reason.check(SMPPRequestTimoutError): 636 | raise SMPPSessionInitTimoutError(str(reason)) 637 | return reason 638 | 639 | def onPDU_outbind(self, pdu): 640 | if self.sessionState != SMPPSessionStates.OPEN: 641 | self.log.critical('Received outbind command in invalid state %s' % str(self.sessionState)) 642 | self.shutdown() 643 | return 644 | 645 | self.log.warning("Received outbind command") 646 | self.doBindAsReceiver() 647 | 648 | def onPDU_alert_notification(self, pdu): 649 | if self.sessionState == SMPPSessionStates.UNBIND_PENDING: 650 | self.log.info("Unbind is pending...Ignoring alert notification PDU %s" % pdu) 651 | return 652 | 653 | if not self.isBound(): 654 | errMsg = 'Received alert notification when not bound %s' % pdu 655 | self.cancelOutboundTransactions(SessionStateError(errMsg, CommandStatus.ESME_RINVBNDSTS)) 656 | self.log.critical(errMsg) 657 | self.shutdown() 658 | return 659 | 660 | if self.alertNotificationHandler: 661 | try: 662 | self.alertNotificationHandler(self, pdu) 663 | except Exception, e: 664 | self.log.critical('Alert handler threw exception: %s' % str(e)) 665 | self.log.exception(e) 666 | self.shutdown() 667 | 668 | ############################################################################ 669 | # Public command functions 670 | ############################################################################ 671 | def bindAsTransmitter(self): 672 | """Bind to SMSC as transmitter 673 | 674 | Result is a Deferred object 675 | """ 676 | self.log.warning('Requesting bind as transmitter') 677 | pdu = BindTransmitter( 678 | system_id = self.config().username, 679 | password = self.config().password, 680 | system_type = self.config().systemType, 681 | address_range = self.config().addressRange, 682 | addr_ton = self.config().addressTon, 683 | addr_npi = self.config().addressNpi, 684 | interface_version = self.version 685 | ) 686 | return self.bind(pdu, SMPPSessionStates.BIND_TX_PENDING, SMPPSessionStates.BOUND_TX) 687 | 688 | def bindAsReceiver(self, dataRequestHandler): 689 | """Bind to SMSC as receiver 690 | 691 | Result is a Deferred object 692 | """ 693 | self.setDataRequestHandler(dataRequestHandler) 694 | return self.doBindAsReceiver() 695 | 696 | def bindAsTransceiver(self, dataRequestHandler): 697 | """Bind to SMSC as transceiver 698 | 699 | Result is a Deferred object 700 | """ 701 | self.setDataRequestHandler(dataRequestHandler) 702 | self.log.warning('Requesting bind as transceiver') 703 | pdu = BindTransceiver( 704 | system_id = self.config().username, 705 | password = self.config().password, 706 | system_type = self.config().systemType, 707 | address_range = self.config().addressRange, 708 | addr_ton = self.config().addressTon, 709 | addr_npi = self.config().addressNpi, 710 | interface_version = self.version 711 | ) 712 | return self.bind(pdu, SMPPSessionStates.BIND_TRX_PENDING, SMPPSessionStates.BOUND_TRX) 713 | 714 | def setDataRequestHandler(self, handler): 715 | """Set handler to use for receiving data requests 716 | """ 717 | self.dataRequestHandler = handler 718 | 719 | def setAlertNotificationHandler(self, handler): 720 | """Set handler to use for receiving data requests 721 | """ 722 | self.alertNotificationHandler = handler 723 | 724 | 725 | class SMPPServerProtocol(SMPPProtocolBase): 726 | 727 | def __init__(self): 728 | SMPPProtocolBase.__init__(self) 729 | # Divert received messages to the handler defined in the config 730 | self.dataRequestHandler = lambda *args, **kwargs: self.config().msgHandler(self.system_id, *args, **kwargs) 731 | self.system_id = None 732 | self.log = logging.getLogger(LOG_CATEGORY) 733 | 734 | def onResponseTimeout(self, reqPDU, timeout): 735 | errMsg = 'Request timed out for system id %s after %s secs: %s' % (self.system_id, timeout, reqPDU) 736 | self.endOutboundTransactionErr(reqPDU, SMPPRequestTimoutError(errMsg)) 737 | self.shutdown() 738 | 739 | def connectionLost(self, reason): 740 | # Remove this connection from those stored in the factory 741 | self.factory.removeConnection(self) 742 | SMPPProtocolBase.connectionLost(self, reason) 743 | 744 | def PDUReceived( self, pdu ): 745 | """Dispatches incoming PDUs 746 | """ 747 | self.log.debug("SMPP Server received PDU to system '%s' [command: %s, sequence_number: %s, command_status: %s]" % (self.system_id, pdu.id, pdu.seqNum, pdu.status)) 748 | SMPPProtocolBase.PDUReceived(self, pdu) 749 | 750 | def onPDURequest_enquire_link(self, reqPDU): 751 | if self.isBound(): 752 | self.sendResponse(reqPDU) 753 | else: 754 | self.sendResponse(reqPDU, status=CommandStatus.ESME_RINVBNDSTS) 755 | 756 | def onPDURequest_bind_receiver(self, reqPDU): 757 | self.doBindRequest(reqPDU, SMPPSessionStates.BOUND_RX) 758 | 759 | def onPDURequest_bind_transmitter(self, reqPDU): 760 | self.doBindRequest(reqPDU, SMPPSessionStates.BOUND_TX) 761 | 762 | def onPDURequest_bind_transceiver(self, reqPDU): 763 | self.doBindRequest(reqPDU, SMPPSessionStates.BOUND_TRX) 764 | 765 | @inlineCallbacks 766 | def doBindRequest(self, reqPDU, sessionState): 767 | # Check the authentication 768 | system_id, password = reqPDU.params['system_id'], reqPDU.params['password'] 769 | 770 | # Authenticate system_id and password 771 | try: 772 | iface, auth_avatar, logout = yield self.factory.login(system_id, password, self.transport.getPeer().host) 773 | except error.UnauthorizedLogin: 774 | self.log.warning('SMPP Bind request failed for system_id: "%s", failed to authenticate' % system_id) 775 | self.sendErrorResponse(reqPDU, CommandStatus.ESME_RINVPASWD, system_id) 776 | return 777 | 778 | # Only a configured system_id can bind 779 | if system_id not in self.factory.config.systems.keys(): 780 | self.log.warning('SMPP Bind request failed for system_id: "%s", System ID not configured' % system_id) 781 | self.sendErrorResponse(reqPDU, CommandStatus.ESME_RINVSYSID, system_id) 782 | return 783 | 784 | # Check we're not already bound, and are open to being bound 785 | if self.sessionState != SMPPSessionStates.OPEN: 786 | self.log.warning('Duplicate SMPP bind request received from: %s' % system_id) 787 | self.sendErrorResponse(reqPDU, CommandStatus.ESME_RALYBND, system_id) 788 | return 789 | 790 | # Check that system_id hasn't exceeded number of allowed binds 791 | bind_type = reqPDU.commandId 792 | if not self.factory.canOpenNewConnection(system_id, bind_type): 793 | self.log.warning('SMPP System %s has exceeded maximum number of %s bindings' % (system_id, bind_type)) 794 | self.sendErrorResponse(reqPDU, CommandStatus.ESME_RBINDFAIL, system_id) 795 | return 796 | 797 | # If we get to here, bind successfully 798 | self.system_id = system_id 799 | self.sessionState = sessionState 800 | self.bind_type = bind_type 801 | 802 | self.factory.addBoundConnection(self) 803 | bound_cnxns = self.factory.getBoundConnections(system_id) 804 | self.log.info('Bind request succeeded for %s. %d active binds' % (system_id, bound_cnxns.getBindingCount() if bound_cnxns else 0)) 805 | self.sendResponse(reqPDU, system_id=system_id) 806 | 807 | def sendErrorResponse(self, reqPDU, status, system_id): 808 | """ Send an error response to reqPDU, with the specified command status.""" 809 | err_pdu = reqPDU.requireAck(seqNum=reqPDU.seqNum, status=status, system_id=system_id) 810 | self.sendPDU(err_pdu) 811 | 812 | -------------------------------------------------------------------------------- /smpp/twisted/server.py: -------------------------------------------------------------------------------- 1 | from smpp.twisted.protocol import SMPPServerProtocol 2 | from smpp.pdu import pdu_types 3 | 4 | from zope.interface import Interface 5 | 6 | from twisted.internet.protocol import ServerFactory 7 | from twisted import cred 8 | from twisted.cred import error 9 | from twisted.internet import defer 10 | 11 | import logging 12 | import collections 13 | 14 | 15 | LOG_CATEGORY="smpp.twisted.server" 16 | 17 | class IAuthenticatedSMPP(Interface): 18 | pass 19 | 20 | class UsernameAndPasswordAndIP(cred.credentials.UsernamePassword): 21 | def __init__(self, username, password, client_ip_address): 22 | self.username = username 23 | self.password = password 24 | self.client_ip_address = client_ip_address 25 | 26 | class SMPPServerFactory(ServerFactory): 27 | 28 | protocol = SMPPServerProtocol 29 | 30 | def __init__(self, config, auth_portal): 31 | self.config = config 32 | self.log = logging.getLogger(LOG_CATEGORY) 33 | # A dict of protocol instances for each of the current connections, 34 | # indexed by system_id 35 | self.bound_connections = {} 36 | self._auth_portal = auth_portal 37 | 38 | def getConfig(self): 39 | return self.config 40 | 41 | def getBoundConnectionCount(self, system_id): 42 | if self.bound_connections.has_key(system_id): 43 | return self.bound_connections[system_id].getMaxTransmitReceiveBindCount() 44 | else: 45 | return 0 46 | 47 | def getBoundConnectionCountsStr(self, system_id): 48 | if self.bound_connections.has_key(system_id): 49 | bind_counts = self.bound_connections[system_id].getBindingCountByType() 50 | bound_connections_count = [] 51 | for key, value in bind_counts.iteritems(): 52 | bound_connections_count.append("%s: %d" % (key, value)) 53 | bound_connections_str = ', '.join(bound_connections_count) 54 | return bound_connections_str 55 | else: 56 | return '0' 57 | 58 | def addBoundConnection(self, connection): 59 | """ 60 | Add a protocol instance to the list of current connections. 61 | @param connection: An instance of SMPPServerProtocol 62 | """ 63 | system_id = connection.system_id 64 | self.log.debug('Adding SMPP binding for %s' % system_id) 65 | if not system_id in self.bound_connections: 66 | self.bound_connections[system_id] = SMPPBindManager(system_id) 67 | self.bound_connections[system_id].addBinding(connection) 68 | bind_type = connection.bind_type 69 | self.log.info("Added %s bind for '%s'. Active binds: %s. Max binds: %s" % (bind_type, system_id, self.getBoundConnectionCountsStr(system_id), self.config.systems[system_id]['max_bindings'])) 70 | 71 | def removeConnection(self, connection): 72 | """ 73 | Remove a protocol instance (SMPP binding) from the list of current connections. 74 | @param connection: An instance of SMPPServerProtocol 75 | """ 76 | if connection.system_id is None: 77 | self.log.debug("SMPP connection attempt failed without binding.") 78 | else: 79 | system_id = connection.system_id 80 | bind_type = connection.bind_type 81 | self.bound_connections[system_id].removeBinding(connection) 82 | self.log.info("Dropped %s bind for '%s'. Active binds: %s. Max binds: %s" % (bind_type, system_id, self.getBoundConnectionCountsStr(system_id), self.config.systems[system_id]['max_bindings'])) 83 | # If this is the last binding for this service then remove the BindManager 84 | if self.bound_connections[system_id].getBindingCount() == 0: 85 | self.bound_connections.pop(system_id) 86 | 87 | def getBoundConnections(self, system_id): 88 | return self.bound_connections.get(system_id) 89 | 90 | def login(self, system_id, password, client_ip_address): 91 | if self._auth_portal is not None: 92 | return self._auth_portal.login( 93 | UsernameAndPasswordAndIP(system_id, password, client_ip_address), 94 | None, 95 | IAuthenticatedSMPP 96 | ) 97 | raise error.UnauthorizedLogin() 98 | 99 | def canOpenNewConnection(self, system_id, bind_type): 100 | """ 101 | Checks if the gateway with the specified system_id can open a new 102 | connection, as it hasn't exceeded its maximum number of bindings. 103 | @param bind_type: One of smpp.pdu.pdu_types.CommandId 104 | """ 105 | existing_bindings_for_id = self.getBoundConnections(system_id) 106 | if existing_bindings_for_id: 107 | connections_count = existing_bindings_for_id.getBindingCountForType(bind_type) 108 | return connections_count < self.config.systems[system_id]['max_bindings'] 109 | else: 110 | # No existing bindings for this system_id 111 | return self.config.systems[system_id]['max_bindings'] > 0 112 | 113 | def unbindGateway(self, system_id): 114 | """ Unbinds and disconnects all the bindings for the given system_id. """ 115 | bind_mgr = self.getBoundConnections(system_id) 116 | if bind_mgr: 117 | unbinds_list = [] 118 | for bind in bind_mgr: 119 | unbinds_list.append(bind.getDisconnectedDeferred()) 120 | bind.unbindAndDisconnect() 121 | d = defer.DeferredList(unbinds_list) 122 | else: 123 | d = defer.succeed(None) 124 | 125 | return d 126 | 127 | def unbindAndRemoveGateway(self, system_id): 128 | ''' 129 | Removes a running gateway from the config so they will be unable to rebind. 130 | Any attempt to bind while unbinding will receive a ESME_RBINDFAIL error. 131 | ''' 132 | self.config.systems[system_id]['max_bindings'] = 0 133 | d = self.unbindGateway(system_id) 134 | d.addCallback(self.removeGatewayFromConfig, system_id) 135 | return d 136 | 137 | def removeGatewayFromConfig(self, deferred_res, system_id): 138 | self.config.systems.pop(system_id) 139 | return deferred_res 140 | 141 | class SMPPBindManager(object): 142 | 143 | def __init__(self, system_id): 144 | self.system_id = system_id 145 | self._binds = {pdu_types.CommandId.bind_transceiver: [], 146 | pdu_types.CommandId.bind_transmitter: [], 147 | pdu_types.CommandId.bind_receiver: []} 148 | # A queue of the most recent bindings used for delivering messages 149 | self._delivery_binding_history = collections.deque() 150 | 151 | def addBinding(self, connection): 152 | """ @param connection: An instance of SMPPServerProtocol """ 153 | 154 | bind_type = connection.bind_type 155 | self._binds[bind_type].append(connection) 156 | 157 | def removeBinding(self, connection): 158 | """ @param connection: An instance of SMPPServerProtocol """ 159 | bind_type = connection.bind_type 160 | self._binds[bind_type].remove(connection) 161 | 162 | def getMaxTransmitReceiveBindCount(self): 163 | return len(self._binds[pdu_types.CommandId.bind_transceiver]) + \ 164 | max(len(self._binds[pdu_types.CommandId.bind_transmitter]), 165 | len(self._binds[pdu_types.CommandId.bind_receiver])) 166 | 167 | def getBindingCount(self): 168 | return sum(len(v) for v in self._binds.values()) 169 | 170 | def getBindingCountByType(self): 171 | ret = {} 172 | for key, value in self._binds.iteritems(): 173 | ret[key] = len(value) 174 | return ret 175 | 176 | def __len__(self): 177 | return self.getBindingCount() 178 | 179 | def __iter__(self): 180 | vals = [] 181 | [vals.extend(type) for type in self._binds.values()] 182 | return vals.__iter__() 183 | 184 | def getBindingCountForType(self, bind_type): 185 | """ 186 | Sum transceiver binds plus receiver or transmitter depending on this type 187 | @param bind_type: One of smpp.pdu.pdu_types.CommandId 188 | """ 189 | if bind_type == pdu_types.CommandId.bind_transceiver: 190 | # Sum of current transceiver binds plus greater of current transmitter or receiver binds 191 | connections_count = self.getMaxTransmitReceiveBindCount() 192 | else: 193 | # Sum of transceiver binds plus existing binds of this type 194 | connections_count = sum([len(self._binds[bt]) for bt in (pdu_types.CommandId.bind_transceiver, bind_type)]) 195 | return connections_count 196 | 197 | def getNextBindingForDelivery(self): 198 | """ 199 | Messages inbound (MO) that are to be forwarded to 200 | the client systems can be sent via transceiver and 201 | receiver bindings. Call this method to determine which 202 | binding to send down next so that traffic travels equally 203 | down the different binds. 204 | @return smpp protocol or None 205 | """ 206 | binding = None 207 | # If we now have more trx/rx bindings than have been used 208 | # then iterate through our trx/rx binds until we find one 209 | # that hasn't yet been used 210 | if len(self._delivery_binding_history) < self.getBindingCountForType(pdu_types.CommandId.bind_receiver): 211 | for binding in self._binds[pdu_types.CommandId.bind_receiver] + self._binds[pdu_types.CommandId.bind_transceiver]: 212 | if not binding in self._delivery_binding_history: 213 | break 214 | else: 215 | binding = None 216 | 217 | # Otherwise send on the last trx/rx binding delivered on, as 218 | # long as it is still bound 219 | while binding is None and self._delivery_binding_history: 220 | # get last binding used 221 | _binding = self._delivery_binding_history.popleft() 222 | # check it is still bound 223 | if _binding in self._binds[_binding.bind_type]: 224 | # If so then use it 225 | binding = _binding 226 | 227 | if binding is not None: 228 | self._delivery_binding_history.append(binding) 229 | return binding 230 | 231 | -------------------------------------------------------------------------------- /smpp/twisted/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ -------------------------------------------------------------------------------- /smpp/twisted/tests/smpp_client_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | import logging 17 | import functools 18 | from twisted.trial import unittest 19 | from twisted.internet import error, reactor, defer 20 | from twisted.internet.protocol import Factory 21 | from twisted.python import log 22 | import mock 23 | from smpp.twisted.protocol import SMPPClientProtocol 24 | from smpp.twisted.client import SMPPClientTransmitter, SMPPClientReceiver, SMPPClientTransceiver, DataHandlerResponse, SMPPClientService 25 | from smpp.twisted.tests.smsc_simulator import * 26 | from smpp.pdu.error import * 27 | from smpp.twisted.config import SMPPClientConfig 28 | from smpp.pdu.operations import * 29 | from smpp.pdu.pdu_types import * 30 | 31 | class SimulatorTestCase(unittest.TestCase): 32 | protocol = BlackHoleSMSC 33 | configArgs = {} 34 | 35 | def setUp(self): 36 | self.factory = Factory() 37 | self.factory.protocol = self.protocol 38 | self.port = reactor.listenTCP(0, self.factory) 39 | self.testPort = self.port.getHost().port 40 | 41 | args = self.configArgs.copy() 42 | args['host'] = self.configArgs.get('host', 'localhost') 43 | args['port'] = self.configArgs.get('port', self.testPort) 44 | args['username'] = self.configArgs.get('username', '') 45 | args['password'] = self.configArgs.get('password', '') 46 | 47 | self.config = SMPPClientConfig(**args) 48 | 49 | def tearDown(self): 50 | self.port.stopListening() 51 | 52 | class SessionInitTimeoutTestCase(SimulatorTestCase): 53 | configArgs = { 54 | 'sessionInitTimerSecs': 0.1, 55 | } 56 | 57 | def test_bind_transmitter_timeout(self): 58 | client = SMPPClientTransmitter(self.config) 59 | return self.assertFailure(client.connectAndBind(), SMPPSessionInitTimoutError) 60 | 61 | def test_bind_receiver_timeout(self): 62 | client = SMPPClientReceiver(self.config, lambda smpp, pdu: None) 63 | return self.assertFailure(client.connectAndBind(), SMPPSessionInitTimoutError) 64 | 65 | def test_bind_transceiver_timeout(self): 66 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 67 | return self.assertFailure(client.connectAndBind(), SMPPSessionInitTimoutError) 68 | 69 | class BindErrorTestCase(SimulatorTestCase): 70 | protocol = BindErrorSMSC 71 | 72 | def test_bind_error(self): 73 | client = SMPPClientTransmitter(self.config) 74 | return self.assertFailure(client.connectAndBind(), SMPPTransactionError) 75 | 76 | class BindErrorGenericNackTestCase(SimulatorTestCase): 77 | protocol = BindErrorGenericNackSMSC 78 | 79 | def test_bind_error_generic_nack(self): 80 | client = SMPPClientTransmitter(self.config) 81 | return self.assertFailure(client.connectAndBind(), SMPPGenericNackTransactionError) 82 | 83 | class UnbindTimeoutTestCase(SimulatorTestCase): 84 | protocol = UnbindNoResponseSMSC 85 | configArgs = { 86 | 'sessionInitTimerSecs': 0.1, 87 | } 88 | 89 | def setUp(self): 90 | SimulatorTestCase.setUp(self) 91 | self.unbindDeferred = defer.Deferred() 92 | 93 | def test_unbind_timeout(self): 94 | client = SMPPClientTransmitter(self.config) 95 | bindDeferred = client.connectAndBind().addCallback(self.do_unbind) 96 | return defer.DeferredList([ 97 | bindDeferred, #asserts that bind was successful 98 | self.assertFailure(self.unbindDeferred, SMPPSessionInitTimoutError), #asserts that unbind timed out 99 | ] 100 | ) 101 | 102 | def do_unbind(self, smpp): 103 | smpp.unbind().chainDeferred(self.unbindDeferred) 104 | return smpp 105 | 106 | class ResponseTimeoutTestCase(SimulatorTestCase): 107 | protocol = NoResponseOnSubmitSMSC 108 | configArgs = { 109 | 'responseTimerSecs': 0.1, 110 | } 111 | 112 | def setUp(self): 113 | SimulatorTestCase.setUp(self) 114 | self.disconnectDeferred = defer.Deferred() 115 | self.submitSMDeferred1 = defer.Deferred() 116 | self.submitSMDeferred2 = defer.Deferred() 117 | 118 | def test_response_timeout(self): 119 | client = SMPPClientTransmitter(self.config) 120 | bindDeferred = client.connectAndBind().addCallback(self.do_test_setup) 121 | self.disconnectDeferred.addCallback(self.verify) 122 | return defer.DeferredList([ 123 | bindDeferred, #asserts that bind was successful 124 | self.assertFailure(self.submitSMDeferred1, SMPPRequestTimoutError), #asserts that request1 timed out 125 | self.assertFailure(self.submitSMDeferred2, SMPPRequestTimoutError), #asserts that request2 timed out 126 | self.disconnectDeferred, #asserts that disconnect deferred was triggered 127 | ] 128 | ) 129 | 130 | def do_test_setup(self, smpp): 131 | self.smpp = smpp 132 | smpp.getDisconnectedDeferred().chainDeferred(self.disconnectDeferred) 133 | smpp.sendDataRequest(SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO')).chainDeferred(self.submitSMDeferred1) 134 | smpp.sendDataRequest(SubmitSM(source_addr='t2', destination_addr='1208230', short_message='HELLO')).chainDeferred(self.submitSMDeferred2) 135 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 136 | return smpp 137 | 138 | #Test unbind sent 139 | def verify(self, result): 140 | self.assertEquals(1, self.smpp.sendPDU.call_count) 141 | sent = self.smpp.sendPDU.call_args[0][0] 142 | self.assertTrue(isinstance(sent, Unbind)) 143 | 144 | class InactivityTimeoutTestCase(SimulatorTestCase): 145 | protocol = HappySMSC 146 | configArgs = { 147 | 'inactivityTimerSecs': 0.1, 148 | } 149 | 150 | def setUp(self): 151 | SimulatorTestCase.setUp(self) 152 | self.disconnectDeferred = defer.Deferred() 153 | 154 | def test_inactivity_timeout(self): 155 | client = SMPPClientTransmitter(self.config) 156 | bindDeferred = client.connectAndBind().addCallback(self.do_test_setup) 157 | self.disconnectDeferred.addCallback(self.verify) 158 | return defer.DeferredList([ 159 | bindDeferred, #asserts that bind was successful 160 | self.disconnectDeferred, #asserts that disconnect deferred was triggered 161 | ] 162 | ) 163 | 164 | def do_test_setup(self, smpp): 165 | self.smpp = smpp 166 | smpp.getDisconnectedDeferred().chainDeferred(self.disconnectDeferred) 167 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 168 | return smpp 169 | 170 | #Test unbind sent 171 | def verify(self, result): 172 | self.assertEquals(1, self.smpp.sendPDU.call_count) 173 | sent = self.smpp.sendPDU.call_args[0][0] 174 | self.assertTrue(isinstance(sent, Unbind)) 175 | 176 | class ServerInitiatedUnbindTestCase(SimulatorTestCase): 177 | protocol = UnbindOnSubmitSMSC 178 | 179 | def setUp(self): 180 | SimulatorTestCase.setUp(self) 181 | self.disconnectDeferred = defer.Deferred() 182 | self.submitSMDeferred = defer.Deferred() 183 | 184 | def test_server_unbind(self): 185 | client = SMPPClientTransmitter(self.config) 186 | bindDeferred = client.connectAndBind().addCallback(self.mock_stuff) 187 | self.disconnectDeferred.addCallback(self.verify) 188 | return defer.DeferredList([ 189 | bindDeferred, #asserts that bind was successful 190 | self.disconnectDeferred, #asserts that disconnect deferred was triggered, 191 | self.assertFailure(self.submitSMDeferred, SMPPClientSessionStateError), #asserts that outbound txn was canceled 192 | ] 193 | ) 194 | 195 | def mock_stuff(self, smpp): 196 | self.smpp = smpp 197 | smpp.getDisconnectedDeferred().chainDeferred(self.disconnectDeferred) 198 | smpp.sendDataRequest(SubmitSM(source_addr='mobileway', destination_addr='1208230', short_message='HELLO1')).chainDeferred(self.submitSMDeferred) 199 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 200 | return smpp 201 | 202 | def verify(self, result): 203 | self.assertEquals(1, self.smpp.sendPDU.call_count) 204 | sent = self.smpp.sendPDU.call_args[0][0] 205 | self.assertEquals(UnbindResp(1), sent) 206 | 207 | class EnquireLinkTestCase(SimulatorTestCase): 208 | protocol = EnquireLinkEchoSMSC 209 | configArgs = { 210 | 'enquireLinkTimerSecs': 0.1, 211 | } 212 | 213 | @defer.inlineCallbacks 214 | def test_enquire_link(self): 215 | client = SMPPClientTransmitter(self.config) 216 | smpp = yield client.connect() 217 | #Assert that enquireLinkTimer is not yet active on connection 218 | self.assertEquals(None, smpp.enquireLinkTimer) 219 | 220 | bindDeferred = client.bind(smpp) 221 | #Assert that enquireLinkTimer is not yet active until bind is complete 222 | self.assertEquals(None, smpp.enquireLinkTimer) 223 | yield bindDeferred 224 | #Assert that enquireLinkTimer is now active after bind is complete 225 | self.assertNotEquals(None, smpp.enquireLinkTimer) 226 | 227 | #Wrap functions for tracking 228 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 229 | smpp.PDUReceived = mock.Mock(wraps=smpp.PDUReceived) 230 | 231 | yield self.wait(0.25) 232 | 233 | self.verifyEnquireLink(smpp) 234 | 235 | #Assert that enquireLinkTimer is still active 236 | self.assertNotEquals(None, smpp.enquireLinkTimer) 237 | 238 | unbindDeferred = smpp.unbind() 239 | 240 | #Assert that enquireLinkTimer is no longer active after unbind is issued 241 | self.assertEquals(None, smpp.enquireLinkTimer) 242 | 243 | yield unbindDeferred 244 | #Assert that enquireLinkTimer is no longer active after unbind is complete 245 | self.assertEquals(None, smpp.enquireLinkTimer) 246 | yield smpp.disconnect() 247 | 248 | def wait(self, time_secs): 249 | finished = defer.Deferred() 250 | reactor.callLater(time_secs, finished.callback, None) 251 | return finished 252 | 253 | def verifyEnquireLink(self, smpp): 254 | self.assertEquals(4, smpp.sendPDU.call_count) 255 | self.assertEquals(4, smpp.PDUReceived.call_count) 256 | sent1 = smpp.sendPDU.call_args_list[0][0][0] 257 | sent2 = smpp.sendPDU.call_args_list[1][0][0] 258 | sent3 = smpp.sendPDU.call_args_list[2][0][0] 259 | sent4 = smpp.sendPDU.call_args_list[3][0][0] 260 | recv1 = smpp.PDUReceived.call_args_list[0][0][0] 261 | recv2 = smpp.PDUReceived.call_args_list[1][0][0] 262 | recv3 = smpp.PDUReceived.call_args_list[2][0][0] 263 | recv4 = smpp.PDUReceived.call_args_list[3][0][0] 264 | 265 | self.assertEquals(EnquireLink(2), sent1) 266 | self.assertEquals(EnquireLinkResp(2), recv1) 267 | 268 | self.assertEquals(EnquireLink(1), recv2) 269 | self.assertEquals(EnquireLinkResp(1), sent2) 270 | 271 | self.assertEquals(EnquireLink(3), sent3) 272 | self.assertEquals(EnquireLinkResp(3), recv3) 273 | 274 | self.assertEquals(EnquireLink(2), recv4) 275 | self.assertEquals(EnquireLinkResp(2), sent4) 276 | 277 | class TransmitterLifecycleTestCase(SimulatorTestCase): 278 | protocol = HappySMSC 279 | 280 | def setUp(self): 281 | SimulatorTestCase.setUp(self) 282 | self.unbindDeferred = defer.Deferred() 283 | self.submitSMDeferred = defer.Deferred() 284 | self.disconnectDeferred = defer.Deferred() 285 | 286 | def test_unbind(self): 287 | client = SMPPClientTransmitter(self.config) 288 | bindDeferred = client.connectAndBind().addCallback(self.do_lifecycle) 289 | return defer.DeferredList([ 290 | bindDeferred, #asserts that bind was successful 291 | self.submitSMDeferred, #asserts that submit was successful 292 | self.unbindDeferred, #asserts that unbind was successful 293 | self.disconnectDeferred, #asserts that disconnect was successful 294 | ] 295 | ) 296 | 297 | def do_lifecycle(self, smpp): 298 | smpp.getDisconnectedDeferred().chainDeferred(self.disconnectDeferred) 299 | smpp.sendDataRequest(SubmitSM()).chainDeferred(self.submitSMDeferred).addCallback(lambda result: smpp.unbindAndDisconnect().chainDeferred(self.unbindDeferred)) 300 | return smpp 301 | 302 | class AlertNotificationTestCase(SimulatorTestCase): 303 | protocol = AlertNotificationSMSC 304 | 305 | @defer.inlineCallbacks 306 | def test_alert_notification(self): 307 | client = SMPPClientTransmitter(self.config) 308 | smpp = yield client.connectAndBind() 309 | 310 | alertNotificationDeferred = defer.Deferred() 311 | alertHandler = mock.Mock(wraps=lambda smpp, pdu: alertNotificationDeferred.callback(None)) 312 | smpp.setAlertNotificationHandler(alertHandler) 313 | 314 | sendDataDeferred = smpp.sendDataRequest(DataSM()) 315 | 316 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 317 | smpp.PDUReceived = mock.Mock(wraps=smpp.PDUReceived) 318 | 319 | yield sendDataDeferred 320 | yield alertNotificationDeferred 321 | yield smpp.unbindAndDisconnect() 322 | 323 | self.assertEquals(1, alertHandler.call_count) 324 | self.assertEquals(smpp, alertHandler.call_args[0][0]) 325 | self.assertTrue(isinstance(alertHandler.call_args[0][1], AlertNotification)) 326 | 327 | self.assertEquals(1, smpp.sendPDU.call_count) 328 | self.assertEquals(3, smpp.PDUReceived.call_count) 329 | sent1 = smpp.sendPDU.call_args[0][0] 330 | recv1 = smpp.PDUReceived.call_args_list[0][0][0] 331 | recv2 = smpp.PDUReceived.call_args_list[1][0][0] 332 | recv3 = smpp.PDUReceived.call_args_list[2][0][0] 333 | self.assertTrue(isinstance(recv1, DataSMResp)) 334 | self.assertTrue(isinstance(recv2, AlertNotification)) 335 | self.assertTrue(isinstance(sent1, Unbind)) 336 | self.assertTrue(isinstance(recv3, UnbindResp)) 337 | 338 | class CommandLengthTooShortTestCase(SimulatorTestCase): 339 | protocol = CommandLengthTooShortSMSC 340 | 341 | def test_generic_nack_on_invalid_cmd_len(self): 342 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 343 | smpp = yield client.connectAndBind() 344 | 345 | msgSentDeferred = defer.Deferred() 346 | 347 | smpp.sendPDU = mock.Mock() 348 | smpp.sendPDU.side_effect = functools.partial(self.mock_side_effect, msgSentDeferred) 349 | 350 | yield msgSentDeferred 351 | 352 | yield smpp.disconnect() 353 | 354 | self.assertEquals(1, smpp.sendPDU.call_count) 355 | smpp.sendPDU.assert_called_with(GenericNack(status=CommandStatus.ESME_RINVMSGLEN)) 356 | 357 | def mock_side_effect(self, msgSentDeferred, pdu): 358 | msgSentDeferred.callback(None) 359 | return mock.DEFAULT 360 | 361 | class CommandLengthTooLongTestCase(SimulatorTestCase): 362 | protocol = CommandLengthTooLongSMSC 363 | configArgs = { 364 | 'pduReadTimerSecs': 0.1, 365 | } 366 | 367 | @defer.inlineCallbacks 368 | def test_generic_nack_on_invalid_cmd_len(self): 369 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 370 | smpp = yield client.connectAndBind() 371 | 372 | msgSentDeferred = defer.Deferred() 373 | 374 | smpp.sendPDU = mock.Mock() 375 | smpp.sendPDU.side_effect = functools.partial(self.mock_side_effect, msgSentDeferred) 376 | 377 | yield msgSentDeferred 378 | 379 | yield smpp.disconnect() 380 | 381 | self.assertEquals(1, smpp.sendPDU.call_count) 382 | smpp.sendPDU.assert_called_with(GenericNack(status=CommandStatus.ESME_RINVCMDLEN)) 383 | 384 | def mock_side_effect(self, msgSentDeferred, pdu): 385 | msgSentDeferred.callback(None) 386 | return mock.DEFAULT 387 | 388 | class InvalidCommandIdTestCase(SimulatorTestCase): 389 | protocol = InvalidCommandIdSMSC 390 | 391 | @defer.inlineCallbacks 392 | def test_generic_nack_on_invalid_cmd_id(self): 393 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 394 | smpp = yield client.connectAndBind() 395 | 396 | msgSentDeferred = defer.Deferred() 397 | 398 | smpp.sendPDU = mock.Mock() 399 | smpp.sendPDU.side_effect = functools.partial(self.mock_side_effect, msgSentDeferred) 400 | 401 | yield msgSentDeferred 402 | 403 | yield smpp.disconnect() 404 | 405 | self.assertEquals(1, smpp.sendPDU.call_count) 406 | smpp.sendPDU.assert_called_with(GenericNack(status=CommandStatus.ESME_RINVCMDID)) 407 | 408 | def mock_side_effect(self, msgSentDeferred, pdu): 409 | msgSentDeferred.callback(None) 410 | return mock.DEFAULT 411 | 412 | class NonFatalParseErrorTestCase(SimulatorTestCase): 413 | protocol = NonFatalParseErrorSMSC 414 | 415 | @defer.inlineCallbacks 416 | def test_nack_on_invalid_msg(self): 417 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 418 | smpp = yield client.connectAndBind() 419 | 420 | msgSentDeferred = defer.Deferred() 421 | 422 | smpp.sendPDU = mock.Mock() 423 | smpp.sendPDU.side_effect = functools.partial(self.mock_side_effect, msgSentDeferred) 424 | 425 | yield msgSentDeferred 426 | 427 | yield smpp.disconnect() 428 | 429 | self.assertEquals(1, smpp.sendPDU.call_count) 430 | smpp.sendPDU.assert_called_with(QuerySMResp(seqNum=self.protocol.seqNum, status=CommandStatus.ESME_RINVSRCTON)) 431 | 432 | def mock_side_effect(self, msgSentDeferred, pdu): 433 | msgSentDeferred.callback(None) 434 | return mock.DEFAULT 435 | 436 | class GenericNackNoSeqNumTestCase(SimulatorTestCase): 437 | protocol = GenericNackNoSeqNumOnSubmitSMSC 438 | 439 | @defer.inlineCallbacks 440 | def test_generic_nack_no_seq_num(self): 441 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 442 | smpp = yield client.connectAndBind() 443 | 444 | try: 445 | submitDeferred = smpp.sendDataRequest(SubmitSM()) 446 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 447 | yield submitDeferred 448 | except SMPPClientConnectionCorruptedError: 449 | pass 450 | else: 451 | self.assertTrue(False, "SMPPClientConnectionCorruptedError not raised") 452 | 453 | #for nack with no seq num, the connection is corrupt so don't unbind() 454 | self.assertEquals(0, smpp.sendPDU.call_count) 455 | 456 | class GenericNackWithSeqNumTestCase(SimulatorTestCase): 457 | protocol = GenericNackWithSeqNumOnSubmitSMSC 458 | 459 | @defer.inlineCallbacks 460 | def test_generic_nack_no_seq_num(self): 461 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 462 | smpp = yield client.connectAndBind() 463 | 464 | try: 465 | yield smpp.sendDataRequest(SubmitSM()) 466 | except SMPPGenericNackTransactionError: 467 | pass 468 | else: 469 | self.assertTrue(False, "SMPPGenericNackTransactionError not raised") 470 | finally: 471 | yield smpp.unbindAndDisconnect() 472 | 473 | class ErrorOnSubmitTestCase(SimulatorTestCase): 474 | protocol = ErrorOnSubmitSMSC 475 | 476 | @defer.inlineCallbacks 477 | def test_error_on_submit(self): 478 | client = SMPPClientTransceiver(self.config, lambda smpp, pdu: None) 479 | smpp = yield client.connectAndBind() 480 | try: 481 | yield smpp.sendDataRequest(SubmitSM()) 482 | except SMPPTransactionError: 483 | pass 484 | else: 485 | self.assertTrue(False, "SMPPTransactionError not raised") 486 | finally: 487 | yield smpp.unbindAndDisconnect() 488 | 489 | class ReceiverLifecycleTestCase(SimulatorTestCase): 490 | protocol = DeliverSMAndUnbindSMSC 491 | 492 | @defer.inlineCallbacks 493 | def test_receiver_lifecycle(self): 494 | client = SMPPClientReceiver(self.config, lambda smpp, pdu: None) 495 | smpp = yield client.connectAndBind() 496 | 497 | smpp.PDUReceived = mock.Mock(wraps=smpp.PDUReceived) 498 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 499 | 500 | yield smpp.getDisconnectedDeferred() 501 | 502 | self.assertEquals(2, smpp.PDUReceived.call_count) 503 | self.assertEquals(2, smpp.sendPDU.call_count) 504 | recv1 = smpp.PDUReceived.call_args_list[0][0][0] 505 | recv2 = smpp.PDUReceived.call_args_list[1][0][0] 506 | sent1 = smpp.sendPDU.call_args_list[0][0][0] 507 | sent2 = smpp.sendPDU.call_args_list[1][0][0] 508 | self.assertTrue(isinstance(recv1, DeliverSM)) 509 | self.assertEquals(recv1.requireAck(recv1.seqNum), sent1) 510 | self.assertTrue(isinstance(recv2, Unbind)) 511 | self.assertEquals(recv2.requireAck(recv2.seqNum), sent2) 512 | 513 | class ReceiverDataHandlerExceptionTestCase(SimulatorTestCase): 514 | protocol = DeliverSMSMSC 515 | 516 | @defer.inlineCallbacks 517 | def test_receiver_exception(self): 518 | client = SMPPClientReceiver(self.config, self.barf) 519 | smpp = yield client.connectAndBind() 520 | 521 | smpp.PDUReceived = mock.Mock(wraps=smpp.PDUReceived) 522 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 523 | 524 | yield smpp.getDisconnectedDeferred() 525 | 526 | self.assertEquals(2, smpp.PDUReceived.call_count) 527 | self.assertEquals(2, smpp.sendPDU.call_count) 528 | recv1 = smpp.PDUReceived.call_args_list[0][0][0] 529 | recv2 = smpp.PDUReceived.call_args_list[1][0][0] 530 | sent1 = smpp.sendPDU.call_args_list[0][0][0] 531 | sent2 = smpp.sendPDU.call_args_list[1][0][0] 532 | self.assertTrue(isinstance(recv1, DeliverSM)) 533 | self.assertEquals(recv1.requireAck(recv1.seqNum, CommandStatus.ESME_RX_T_APPN), sent1) 534 | self.assertTrue(isinstance(sent2, Unbind)) 535 | self.assertTrue(isinstance(recv2, UnbindResp)) 536 | 537 | def barf(self, smpp, pdu): 538 | raise ValueError('barf') 539 | 540 | class ReceiverDataHandlerBadResponseParamTestCase(SimulatorTestCase): 541 | protocol = DeliverSMSMSC 542 | 543 | @defer.inlineCallbacks 544 | def test_receiver_bad_resp_param(self): 545 | client = SMPPClientReceiver(self.config, self.respondBadParam) 546 | smpp = yield client.connectAndBind() 547 | 548 | smpp.PDUReceived = mock.Mock(wraps=smpp.PDUReceived) 549 | smpp.sendPDU = mock.Mock(wraps=smpp.sendPDU) 550 | 551 | yield smpp.getDisconnectedDeferred() 552 | 553 | self.assertEquals(2, smpp.PDUReceived.call_count) 554 | self.assertEquals(2, smpp.sendPDU.call_count) 555 | recv1 = smpp.PDUReceived.call_args_list[0][0][0] 556 | recv2 = smpp.PDUReceived.call_args_list[1][0][0] 557 | sent1 = smpp.sendPDU.call_args_list[0][0][0] 558 | sent2 = smpp.sendPDU.call_args_list[1][0][0] 559 | self.assertTrue(isinstance(recv1, DeliverSM)) 560 | self.assertEquals(recv1.requireAck(recv1.seqNum, CommandStatus.ESME_RX_T_APPN), sent1) 561 | self.assertTrue(isinstance(sent2, Unbind)) 562 | self.assertTrue(isinstance(recv2, UnbindResp)) 563 | 564 | def respondBadParam(self, smpp, pdu): 565 | return DataHandlerResponse(delivery_failure_reason=DeliveryFailureReason.PERMANENT_NETWORK_ERROR) 566 | 567 | class ReceiverUnboundErrorTestCase(SimulatorTestCase): 568 | protocol = DeliverSMBeforeBoundSMSC 569 | 570 | def setUp(self): 571 | SimulatorTestCase.setUp(self) 572 | 573 | def test_receiver_exception(self): 574 | client = SMPPClientReceiver(self.config, lambda smpp, pdu: None) 575 | bindDeferred = client.connectAndBind() 576 | return self.assertFailure(bindDeferred, SessionStateError) 577 | 578 | class OutbindTestCase(SimulatorTestCase): 579 | protocol = OutbindSMSC 580 | 581 | def msgHandler(self, smpp, pdu): 582 | smpp.unbindAndDisconnect() 583 | return None 584 | 585 | @defer.inlineCallbacks 586 | def test_outbind(self): 587 | client = SMPPClientReceiver(self.config, self.msgHandler) 588 | smpp = yield client.connect() 589 | yield smpp.getDisconnectedDeferred() 590 | 591 | class SMPPClientServiceBindTimeoutTestCase(SimulatorTestCase): 592 | configArgs = { 593 | 'sessionInitTimerSecs': 0.1, 594 | } 595 | 596 | def test_bind_transmitter_timeout(self): 597 | client = SMPPClientTransmitter(self.config) 598 | svc = SMPPClientService(client) 599 | stopDeferred = svc.getStopDeferred() 600 | startDeferred = svc.startService() 601 | return defer.DeferredList([ 602 | self.assertFailure(startDeferred, SMPPSessionInitTimoutError), 603 | self.assertFailure(stopDeferred, SMPPSessionInitTimoutError), 604 | ]) 605 | 606 | if __name__ == '__main__': 607 | observer = log.PythonLoggingObserver() 608 | observer.start() 609 | logging.basicConfig(level=logging.DEBUG) 610 | 611 | import sys 612 | from twisted.scripts import trial 613 | sys.argv.extend([sys.argv[0]]) 614 | trial.run() -------------------------------------------------------------------------------- /smpp/twisted/tests/smpp_protocol_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | import logging, binascii 17 | from twisted.trial import unittest 18 | from twisted.internet import error, reactor, defer 19 | from twisted.python import log 20 | from mock import Mock, sentinel 21 | from smpp.pdu.error import * 22 | from smpp.pdu.operations import * 23 | from smpp.pdu.pdu_types import * 24 | from smpp.twisted.config import SMPPClientConfig 25 | from smpp.twisted.protocol import SMPPClientProtocol, SMPPSessionStates, SMPPOutboundTxnResult, DataHandlerResponse 26 | 27 | class FakeClientError(SMPPClientError): 28 | pass 29 | 30 | class ProtocolTestCase(unittest.TestCase): 31 | 32 | def getProtocolObject(self): 33 | smpp = SMPPClientProtocol() 34 | config = SMPPClientConfig( 35 | host='localhost', 36 | port = 82, 37 | username = '', 38 | password = '', 39 | ) 40 | smpp.config = Mock(return_value=config) 41 | return smpp 42 | 43 | def test_corruptData(self): 44 | smpp = self.getProtocolObject() 45 | self.assertEquals('', smpp.recvBuffer) 46 | smpp.sendPDU = Mock() 47 | smpp.cancelOutboundTransactions = Mock() 48 | smpp.shutdown = Mock() 49 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 50 | smpp.corruptDataRecvd() 51 | #Assert that corrupt data 52 | #Triggers a shutdown call 53 | self.assertEquals(1, smpp.shutdown.call_count) 54 | #Causes outbound transactions to be canceled 55 | self.assertEquals(1, smpp.cancelOutboundTransactions.call_count) 56 | #Responds with a generic nack with invalid cmd len error status 57 | nackResp = smpp.sendPDU.call_args[0][0] 58 | self.assertEquals(GenericNack(seqNum=None, status=CommandStatus.ESME_RINVCMDLEN), nackResp) 59 | #Causes new data received to be ignored 60 | newDataHex = 'afc4' 61 | smpp.dataReceived(binascii.a2b_hex(newDataHex)) 62 | self.assertEquals(newDataHex, binascii.b2a_hex(smpp.recvBuffer)) 63 | #Causes new data requests to fail immediately 64 | submitPdu = SubmitSM( 65 | source_addr_ton=AddrTon.ALPHANUMERIC, 66 | source_addr='mobileway', 67 | dest_addr_ton=AddrTon.INTERNATIONAL, 68 | dest_addr_npi=AddrNpi.ISDN, 69 | destination_addr='1208230', 70 | short_message='HELLO', 71 | ) 72 | return self.assertFailure(smpp.sendDataRequest(submitPdu), SMPPClientConnectionCorruptedError) 73 | 74 | def test_cancelOutboundTransactions(self): 75 | smpp = self.getProtocolObject() 76 | smpp.sendPDU = Mock() 77 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 78 | #start two transactions 79 | submitPdu1 = SubmitSM( 80 | source_addr_ton=AddrTon.ALPHANUMERIC, 81 | source_addr='mobileway', 82 | dest_addr_ton=AddrTon.INTERNATIONAL, 83 | dest_addr_npi=AddrNpi.ISDN, 84 | destination_addr='1208230', 85 | short_message='HELLO1', 86 | ) 87 | submitPdu2 = SubmitSM( 88 | source_addr_ton=AddrTon.ALPHANUMERIC, 89 | source_addr='mobileway', 90 | dest_addr_ton=AddrTon.INTERNATIONAL, 91 | dest_addr_npi=AddrNpi.ISDN, 92 | destination_addr='1208230', 93 | short_message='HELLO2', 94 | ) 95 | d1 = smpp.sendDataRequest(submitPdu1) 96 | d2 = smpp.sendDataRequest(submitPdu2) 97 | self.assertEquals(2, len(smpp.outTxns)) 98 | smpp.cancelOutboundTransactions(FakeClientError('test')) 99 | return defer.DeferredList([ 100 | self.assertFailure(d1, FakeClientError), 101 | self.assertFailure(d2, FakeClientError), 102 | ]) 103 | 104 | def test_finish_txns(self): 105 | smpp = self.getProtocolObject() 106 | smpp.sendPDU = Mock() 107 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 108 | 109 | #setup outbound txns 110 | outPdu1 = SubmitSM( 111 | seqNum=98790, 112 | source_addr='mobileway', 113 | destination_addr='1208230', 114 | short_message='HELLO1', 115 | ) 116 | outRespPdu1 = outPdu1.requireAck(seqNum=outPdu1.seqNum) 117 | 118 | outPdu2 = SubmitSM( 119 | seqNum=875, 120 | source_addr='mobileway', 121 | destination_addr='1208230', 122 | short_message='HELLO1', 123 | ) 124 | outRespPdu2 = outPdu2.requireAck(seqNum=outPdu2.seqNum, status=CommandStatus.ESME_RINVSRCTON) 125 | 126 | outDeferred1 = smpp.startOutboundTransaction(outPdu1, 1) 127 | outDeferred2 = smpp.startOutboundTransaction(outPdu2, 1) 128 | 129 | finishOutTxns = smpp.finishOutboundTxns() 130 | 131 | #Simulate second txn having error 132 | smpp.endOutboundTransactionErr(outRespPdu2, FakeClientError('test')) 133 | #Assert txns not done yet 134 | self.assertFalse(finishOutTxns.called) 135 | 136 | #Simulate first txn finishing 137 | smpp.endOutboundTransaction(outRespPdu1) 138 | #Assert txns are all done 139 | self.assertTrue(finishOutTxns.called) 140 | 141 | return defer.DeferredList([ 142 | outDeferred1, 143 | self.assertFailure(outDeferred2, FakeClientError), 144 | finishOutTxns, 145 | ] 146 | ) 147 | 148 | def test_graceful_unbind(self): 149 | smpp = self.getProtocolObject() 150 | smpp.sendPDU = Mock() 151 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 152 | 153 | #setup outbound txn 154 | outPdu = SubmitSM( 155 | seqNum=98790, 156 | source_addr='mobileway', 157 | destination_addr='1208230', 158 | short_message='HELLO1', 159 | ) 160 | outRespPdu = outPdu.requireAck(seqNum=outPdu.seqNum) 161 | outDeferred = smpp.startOutboundTransaction(outPdu, 1) 162 | #setup inbound txn 163 | inPdu = DeliverSM( 164 | seqNum=764765, 165 | source_addr='mobileway', 166 | destination_addr='1208230', 167 | short_message='HELLO1', 168 | ) 169 | inDeferred = smpp.startInboundTransaction(inPdu) 170 | 171 | #Call unbind 172 | unbindDeferred = smpp.unbind() 173 | 174 | #Assert unbind request not sent and deferred not fired 175 | self.assertEquals(0, smpp.sendPDU.call_count) 176 | self.assertFalse(unbindDeferred.called) 177 | 178 | #Simulate inbound txn finishing 179 | smpp.endInboundTransaction(inPdu) 180 | 181 | #Assert unbind request not sent and deferred not fired 182 | self.assertEquals(0, smpp.sendPDU.call_count) 183 | self.assertFalse(unbindDeferred.called) 184 | 185 | #Simulate outbound txn finishing 186 | smpp.endOutboundTransaction(outRespPdu) 187 | 188 | #Assert unbind request was sent but deferred not yet fired 189 | self.assertEquals(1, smpp.sendPDU.call_count) 190 | sentPdu = smpp.sendPDU.call_args[0][0] 191 | self.assertTrue(isinstance(sentPdu, Unbind)) 192 | self.assertFalse(unbindDeferred.called) 193 | 194 | bindResp = UnbindResp(seqNum=sentPdu.seqNum) 195 | 196 | #Simulate unbind_resp 197 | smpp.endOutboundTransaction(bindResp) 198 | 199 | #Assert unbind deferred fired 200 | self.assertTrue(unbindDeferred.called) 201 | self.assertTrue(isinstance(unbindDeferred.result, SMPPOutboundTxnResult)) 202 | expectedResult = SMPPOutboundTxnResult(smpp, sentPdu, bindResp) 203 | self.assertEquals(expectedResult, unbindDeferred.result) 204 | 205 | def test_bind_when_not_in_open_state(self): 206 | smpp = self.getProtocolObject() 207 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 208 | return self.assertFailure(smpp.bindAsTransmitter(), SMPPClientSessionStateError) 209 | 210 | def test_unbind_when_not_bound(self): 211 | smpp = self.getProtocolObject() 212 | smpp.sessionState = SMPPSessionStates.BIND_TX_PENDING 213 | return self.assertFailure(smpp.unbind(), SMPPClientSessionStateError) 214 | 215 | def test_server_initiated_unbind_cancels_enquire_link_timer(self): 216 | smpp = self.getProtocolObject() 217 | smpp.sendResponse = Mock() 218 | smpp.disconnect = Mock() 219 | 220 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 221 | smpp.activateEnquireLinkTimer() 222 | self.assertNotEquals(None, smpp.enquireLinkTimer) 223 | smpp.onPDURequest_unbind(Unbind()) 224 | self.assertEquals(None, smpp.enquireLinkTimer) 225 | 226 | def test_sendDataRequest_when_not_bound(self): 227 | smpp = self.getProtocolObject() 228 | smpp.sessionState = SMPPSessionStates.BIND_TX_PENDING 229 | return self.assertFailure(smpp.sendDataRequest(SubmitSM()), SMPPClientSessionStateError) 230 | 231 | def test_sendDataRequest_invalid_pdu(self): 232 | smpp = self.getProtocolObject() 233 | smpp.sessionState = SMPPSessionStates.BOUND_TRX 234 | return self.assertFailure(smpp.sendDataRequest(Unbind()), SMPPClientError) 235 | 236 | def test_data_handler_return_none(self): 237 | smpp = self.getProtocolObject() 238 | smpp.sendPDU = Mock() 239 | reqPDU = DeliverSM(5) 240 | smpp.PDURequestSucceeded(None, reqPDU) 241 | self.assertEquals(1, smpp.sendPDU.call_count) 242 | sent = smpp.sendPDU.call_args[0][0] 243 | self.assertEquals(DeliverSMResp(5), sent) 244 | 245 | def test_data_handler_return_status(self): 246 | smpp = self.getProtocolObject() 247 | smpp.sendPDU = Mock() 248 | reqPDU = DeliverSM(5) 249 | smpp.PDURequestSucceeded(CommandStatus.ESME_RINVSRCTON, reqPDU) 250 | self.assertEquals(1, smpp.sendPDU.call_count) 251 | sent = smpp.sendPDU.call_args[0][0] 252 | self.assertEquals(DeliverSMResp(5, CommandStatus.ESME_RINVSRCTON), sent) 253 | 254 | def test_data_handler_return_resp(self): 255 | smpp = self.getProtocolObject() 256 | smpp.sendPDU = Mock() 257 | reqPDU = DataSM(6) 258 | smpp.PDURequestSucceeded(DataHandlerResponse(CommandStatus.ESME_RINVSRCTON, delivery_failure_reason=DeliveryFailureReason.PERMANENT_NETWORK_ERROR), reqPDU) 259 | self.assertEquals(1, smpp.sendPDU.call_count) 260 | sent = smpp.sendPDU.call_args[0][0] 261 | self.assertEquals(DataSMResp(6, CommandStatus.ESME_RINVSRCTON, delivery_failure_reason=DeliveryFailureReason.PERMANENT_NETWORK_ERROR), sent) 262 | 263 | def test_data_handler_return_junk(self): 264 | smpp = self.getProtocolObject() 265 | smpp.sendPDU = Mock() 266 | smpp.shutdown = Mock() 267 | reqPDU = DeliverSM(5) 268 | smpp.PDURequestSucceeded(3, reqPDU) 269 | self.assertEquals(1, smpp.shutdown.call_count) 270 | self.assertEquals(1, smpp.sendPDU.call_count) 271 | sent = smpp.sendPDU.call_args[0][0] 272 | self.assertEquals(DeliverSMResp(5, CommandStatus.ESME_RX_T_APPN), sent) 273 | 274 | if __name__ == '__main__': 275 | observer = log.PythonLoggingObserver() 276 | observer.start() 277 | logging.basicConfig(level=logging.DEBUG) 278 | 279 | import sys 280 | from twisted.scripts import trial 281 | sys.argv.extend([sys.argv[0]]) 282 | trial.run() -------------------------------------------------------------------------------- /smpp/twisted/tests/smsc_simulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2009-2010 Mozes, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | import logging, struct, StringIO, binascii 17 | from twisted.internet.protocol import Protocol, Factory 18 | from twisted.internet import reactor, protocol 19 | from smpp.pdu.operations import * 20 | from smpp.pdu.pdu_encoding import PDUEncoder 21 | from smpp.pdu.pdu_types import * 22 | 23 | LOG_CATEGORY="smpp.twisted.tests.smsc_simulator" 24 | 25 | class BlackHoleSMSC( protocol.Protocol ): 26 | 27 | responseMap = {} 28 | 29 | def __init__( self ): 30 | self.log = logging.getLogger(LOG_CATEGORY) 31 | self.recvBuffer = "" 32 | self.lastSeqNum = 0 33 | self.encoder = PDUEncoder() 34 | 35 | def dataReceived( self, data ): 36 | self.recvBuffer = self.recvBuffer + data 37 | 38 | while len( self.recvBuffer ) > 3: 39 | ( length, ) = struct.unpack( '!L', self.recvBuffer[:4] ) 40 | if len( self.recvBuffer ) < length: 41 | break 42 | message = self.recvBuffer[:length] 43 | self.recvBuffer = self.recvBuffer[length:] 44 | self.rawMessageReceived( message ) 45 | 46 | def rawMessageReceived( self, message ): 47 | return self.PDUReceived( self.encoder.decode( StringIO.StringIO(message) ) ) 48 | 49 | def PDUReceived( self, pdu ): 50 | if pdu.__class__ in self.responseMap: 51 | self.responseMap[pdu.__class__](pdu) 52 | 53 | def sendSuccessResponse(self, reqPDU): 54 | self.sendResponse(reqPDU, CommandStatus.ESME_ROK) 55 | 56 | def sendResponse(self, reqPDU, status): 57 | respPDU = reqPDU.requireAck(reqPDU.seqNum, status=status) 58 | self.sendPDU(respPDU) 59 | 60 | def sendPDU(self, pdu): 61 | if isinstance(pdu, PDURequest) and pdu.seqNum is None: 62 | self.lastSeqNum += 1 63 | pdu.seqNum = self.lastSeqNum 64 | # self.log.debug("Sending PDU: %s" % pdu) 65 | encoded = self.encoder.encode(pdu) 66 | # self.log.debug("Sending data [%s]" % binascii.b2a_hex(encoded)) 67 | self.transport.write( encoded ) 68 | 69 | class HappySMSC(BlackHoleSMSC): 70 | 71 | def __init__(self): 72 | BlackHoleSMSC.__init__(self) 73 | self.responseMap = { 74 | BindTransmitter: self.sendSuccessResponse, 75 | BindReceiver: self.sendSuccessResponse, 76 | BindTransceiver: self.sendSuccessResponse, 77 | EnquireLink: self.sendSuccessResponse, 78 | Unbind: self.sendSuccessResponse, 79 | SubmitSM: self.handleSubmit, 80 | DataSM: self.handleData, 81 | } 82 | 83 | def handleSubmit(self, reqPDU): 84 | self.sendSuccessResponse(reqPDU) 85 | 86 | def handleData(self, reqPDU): 87 | self.sendSuccessResponse(reqPDU) 88 | 89 | class AlertNotificationSMSC(HappySMSC): 90 | 91 | def handleData(self, reqPDU): 92 | HappySMSC.handleData(self, reqPDU) 93 | self.sendPDU(AlertNotification()) 94 | 95 | class EnquireLinkEchoSMSC(HappySMSC): 96 | 97 | def __init__( self ): 98 | HappySMSC.__init__(self) 99 | self.responseMap[EnquireLink] = self.echoEnquireLink 100 | 101 | def echoEnquireLink(self, reqPDU): 102 | self.sendSuccessResponse(reqPDU) 103 | self.sendPDU(EnquireLink()) 104 | 105 | class NoResponseOnSubmitSMSC(HappySMSC): 106 | 107 | def handleSubmit(self, reqPDU): 108 | pass 109 | 110 | class GenericNackNoSeqNumOnSubmitSMSC(HappySMSC): 111 | 112 | def handleSubmit(self, reqPDU): 113 | respPDU = GenericNack(status=CommandStatus.ESME_RINVCMDLEN) 114 | self.sendPDU(respPDU) 115 | 116 | class GenericNackWithSeqNumOnSubmitSMSC(HappySMSC): 117 | 118 | def handleSubmit(self, reqPDU): 119 | respPDU = GenericNack(seqNum=reqPDU.seqNum, status=CommandStatus.ESME_RINVCMDID) 120 | self.sendPDU(respPDU) 121 | 122 | class ErrorOnSubmitSMSC(HappySMSC): 123 | 124 | def handleSubmit(self, reqPDU): 125 | self.sendResponse(reqPDU, CommandStatus.ESME_RINVESMCLASS) 126 | 127 | class UnbindOnSubmitSMSC(HappySMSC): 128 | 129 | def handleSubmit(self, reqPDU): 130 | self.sendPDU(Unbind()) 131 | 132 | class UnbindNoResponseSMSC(HappySMSC): 133 | 134 | def __init__( self ): 135 | HappySMSC.__init__(self) 136 | del self.responseMap[Unbind] 137 | 138 | class BindErrorSMSC(BlackHoleSMSC): 139 | 140 | def __init__( self ): 141 | BlackHoleSMSC.__init__(self) 142 | self.responseMap = { 143 | BindTransmitter: self.bindError, 144 | BindReceiver: self.bindError, 145 | BindTransceiver: self.bindError, 146 | } 147 | 148 | def bindError(self, reqPDU): 149 | self.sendResponse(reqPDU, CommandStatus.ESME_RBINDFAIL) 150 | 151 | class BindErrorGenericNackSMSC(BlackHoleSMSC): 152 | 153 | def __init__( self ): 154 | BlackHoleSMSC.__init__(self) 155 | self.responseMap = { 156 | BindTransmitter: self.bindError, 157 | BindReceiver: self.bindError, 158 | BindTransceiver: self.bindError, 159 | } 160 | 161 | def bindError(self, reqPDU): 162 | respPDU = GenericNack(reqPDU.seqNum) 163 | respPDU.status = CommandStatus.ESME_RINVCMDID 164 | self.sendPDU(respPDU) 165 | 166 | class CommandLengthTooShortSMSC(BlackHoleSMSC): 167 | 168 | def __init__( self ): 169 | BlackHoleSMSC.__init__(self) 170 | self.responseMap = { 171 | BindTransmitter: self.sendInvalidCommandLengthPDUAfterBind, 172 | BindReceiver: self.sendInvalidCommandLengthPDUAfterBind, 173 | BindTransceiver: self.sendInvalidCommandLengthPDUAfterBind, 174 | } 175 | 176 | def sendInvalidCommandLengthPDUAfterBind(self, reqPDU): 177 | self.sendSuccessResponse(reqPDU) 178 | unbind = Unbind() 179 | encoded = self.encoder.encode(unbind) 180 | hexEncoded = binascii.b2a_hex(encoded) 181 | #Overwrite the command length (first octet) 182 | badCmdLenHex = '0000000f' 183 | badHexEncoded = badCmdLenHex + hexEncoded[len(badCmdLenHex):] 184 | self.log.debug("Sending PDU with cmd len too small [%s]" % badHexEncoded) 185 | badEncoded = binascii.a2b_hex(badHexEncoded) 186 | self.transport.write(badEncoded) 187 | 188 | class CommandLengthTooLongSMSC(BlackHoleSMSC): 189 | 190 | def __init__( self ): 191 | BlackHoleSMSC.__init__(self) 192 | self.responseMap = { 193 | BindTransmitter: self.sendInvalidCommandLengthPDUAfterBind, 194 | BindReceiver: self.sendInvalidCommandLengthPDUAfterBind, 195 | BindTransceiver: self.sendInvalidCommandLengthPDUAfterBind, 196 | } 197 | 198 | def sendInvalidCommandLengthPDUAfterBind(self, reqPDU): 199 | self.sendSuccessResponse(reqPDU) 200 | unbind = Unbind() 201 | encoded = self.encoder.encode(unbind) 202 | hexEncoded = binascii.b2a_hex(encoded) 203 | #Overwrite the command length (first octet) 204 | badCmdLenHex = '0000ffff' 205 | badHexEncoded = badCmdLenHex + hexEncoded[len(badCmdLenHex):] 206 | self.log.debug("Sending PDU with cmd len too large [%s]" % badHexEncoded) 207 | badEncoded = binascii.a2b_hex(badHexEncoded) 208 | self.transport.write(badEncoded) 209 | 210 | class InvalidCommandIdSMSC(BlackHoleSMSC): 211 | 212 | def __init__( self ): 213 | BlackHoleSMSC.__init__(self) 214 | self.responseMap = { 215 | BindTransmitter: self.sendInvalidCommandIdAfterBind, 216 | BindReceiver: self.sendInvalidCommandIdAfterBind, 217 | BindTransceiver: self.sendInvalidCommandIdAfterBind, 218 | } 219 | 220 | def sendInvalidCommandIdAfterBind(self, reqPDU): 221 | self.sendSuccessResponse(reqPDU) 222 | unbind = Unbind() 223 | encoded = self.encoder.encode(unbind) 224 | hexEncoded = binascii.b2a_hex(encoded) 225 | #Overwrite the command id (second octet) 226 | badCmdIdHex = 'f0000009' 227 | badHexEncoded = hexEncoded[:8] + badCmdIdHex + hexEncoded[8 + len(badCmdIdHex):] 228 | self.log.debug("Sending PDU with invalid cmd id [%s]" % badHexEncoded) 229 | badEncoded = binascii.a2b_hex(badHexEncoded) 230 | self.transport.write(badEncoded) 231 | 232 | class NonFatalParseErrorSMSC(BlackHoleSMSC): 233 | seqNum = 2654 234 | 235 | def __init__( self ): 236 | BlackHoleSMSC.__init__(self) 237 | self.responseMap = { 238 | BindTransmitter: self.sendInvalidMessageAfterBind, 239 | BindReceiver: self.sendInvalidMessageAfterBind, 240 | BindTransceiver: self.sendInvalidMessageAfterBind, 241 | } 242 | 243 | def sendInvalidMessageAfterBind(self, reqPDU): 244 | self.sendSuccessResponse(reqPDU) 245 | 246 | pdu = QuerySM(seqNum=self.seqNum, 247 | source_addr_ton=AddrTon.ABBREVIATED, 248 | source_addr='1234' 249 | ) 250 | encoded = self.encoder.encode(pdu) 251 | hexEncoded = binascii.b2a_hex(encoded) 252 | #Overwrite the source_addr_ton param (18th octet) 253 | badSrcAddrTonHex = '07' 254 | badIdx = 17*2 255 | badHexEncoded = hexEncoded[:badIdx] + badSrcAddrTonHex + hexEncoded[(badIdx + len(badSrcAddrTonHex)):] 256 | self.log.debug("Sending PDU with invalid source_addr_ton [%s]" % badHexEncoded) 257 | badEncoded = binascii.a2b_hex(badHexEncoded) 258 | self.transport.write(badEncoded) 259 | 260 | class DeliverSMBeforeBoundSMSC(BlackHoleSMSC): 261 | 262 | def connectionMade(self): 263 | pdu = DeliverSM( 264 | source_addr='1234', 265 | destination_addr='4567', 266 | short_message='test', 267 | ) 268 | self.sendPDU(pdu) 269 | 270 | class OutbindSMSC(HappySMSC): 271 | 272 | def __init__( self ): 273 | HappySMSC.__init__(self) 274 | self.responseMap[BindReceiver] = self.sendDeliverSM 275 | 276 | def connectionMade(self): 277 | self.sendPDU(Outbind()) 278 | 279 | def sendDeliverSM(self, reqPDU): 280 | self.sendSuccessResponse(reqPDU) 281 | 282 | pdu = DeliverSM( 283 | source_addr='1234', 284 | destination_addr='4567', 285 | short_message='test', 286 | ) 287 | self.sendPDU(pdu) 288 | 289 | class DeliverSMSMSC(HappySMSC): 290 | 291 | def __init__( self ): 292 | HappySMSC.__init__(self) 293 | self.responseMap[BindReceiver] = self.sendDeliverSM 294 | self.responseMap[BindTransceiver] = self.sendDeliverSM 295 | 296 | def sendDeliverSM(self, reqPDU): 297 | self.sendSuccessResponse(reqPDU) 298 | 299 | pdu = DeliverSM( 300 | source_addr='1234', 301 | destination_addr='4567', 302 | short_message='test', 303 | ) 304 | self.sendPDU(pdu) 305 | 306 | class DeliverSMAndUnbindSMSC(DeliverSMSMSC): 307 | 308 | def sendDeliverSM(self, reqPDU): 309 | DeliverSMSMSC.sendDeliverSM(self, reqPDU) 310 | self.sendPDU(Unbind()) 311 | 312 | if __name__ == '__main__': 313 | logging.basicConfig(level=logging.DEBUG) 314 | factory = Factory() 315 | factory.protocol = BlackHoleSMSC 316 | reactor.listenTCP(8007, factory) 317 | reactor.run() -------------------------------------------------------------------------------- /smpp/twisted/tests/test_smpp_server.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import logging 3 | from twisted.web import resource 4 | from twisted.internet import defer, reactor, task 5 | from twisted.application import internet 6 | from twisted.trial.unittest import TestCase 7 | from twisted.cred.portal import IRealm 8 | from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse 9 | from twisted.cred.portal import Portal 10 | from twisted.test import proto_helpers 11 | from zope.interface import implements 12 | 13 | from smpp.twisted.config import SMPPServerConfig, SMPPClientConfig 14 | from smpp.twisted.server import SMPPServerFactory, SMPPBindManager 15 | from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse 16 | from smpp.twisted.client import SMPPClientTransceiver 17 | 18 | from smpp.pdu import pdu_types, operations, pdu_encoding 19 | 20 | import mock 21 | 22 | logging.basicConfig(level = logging.DEBUG) 23 | 24 | def _makeMockServerConnection(key, bind_type): 25 | mk_server_cnxn = mock.Mock('mock svr cnxn') 26 | mk_server_cnxn.system_id = key 27 | mk_server_cnxn.bind_type = bind_type 28 | return mk_server_cnxn 29 | 30 | class SMPPServerFactoryTests(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.config = mock.Mock('mock smpp config') 34 | self.config.systems = {'lala': {'max_bindings': 2}} 35 | 36 | def tearDown(self): 37 | pass 38 | 39 | def test_canOpenNewConnection_transceiver(self): 40 | server = SMPPServerFactory(self.config, None) 41 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 42 | self.assertTrue(can_bind, 'Should be able to bind trx as none bound yet') 43 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 44 | self.assertTrue(can_bind, 'Should be able to bind tx as none bound yet') 45 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 46 | self.assertTrue(can_bind, 'Should be able to bind rx as none bound yet') 47 | 48 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transceiver) 49 | server.addBoundConnection(mk_server_cnxn) 50 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 51 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 52 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 53 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 54 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 55 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 56 | 57 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transceiver) 58 | server.addBoundConnection(mk_server_cnxn) 59 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 60 | self.assertFalse(can_bind, 'Should not be able to bind as two trx already bound') 61 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 62 | self.assertFalse(can_bind, 'Should not be able to bind as two trx already bound') 63 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 64 | self.assertFalse(can_bind, 'Should not be able to bind as two trx already bound') 65 | 66 | def test_canOpenNewConnection_transmitter(self): 67 | server = SMPPServerFactory(self.config, None) 68 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 69 | self.assertTrue(can_bind, 'Should be able to bind trx as none bound yet') 70 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 71 | self.assertTrue(can_bind, 'Should be able to bind tx as none bound yet') 72 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 73 | self.assertTrue(can_bind, 'Should be able to bind rx as none bound yet') 74 | 75 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transmitter) 76 | server.addBoundConnection(mk_server_cnxn) 77 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 78 | self.assertTrue(can_bind, 'Should still be able to bind as only one tx bound') 79 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 80 | self.assertTrue(can_bind, 'Should still be able to bind as only one tx bound') 81 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 82 | self.assertTrue(can_bind, 'Should still be able to bind only bindings are tx') 83 | 84 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transmitter) 85 | server.addBoundConnection(mk_server_cnxn) 86 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 87 | self.assertFalse(can_bind, 'Should not be able to bind as two tx already bound') 88 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 89 | self.assertFalse(can_bind, 'Should not be able to bind as two tx already bound') 90 | # NOTE this one different 91 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 92 | self.assertTrue(can_bind, 'Should still be able to bind only bindings are tx') 93 | 94 | def test_canOpenNewConnection_receiver(self): 95 | server = SMPPServerFactory(self.config, None) 96 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 97 | self.assertTrue(can_bind, 'Should be able to bind trx as none bound yet') 98 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 99 | self.assertTrue(can_bind, 'Should be able to bind tx as none bound yet') 100 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 101 | self.assertTrue(can_bind, 'Should be able to bind rx as none bound yet') 102 | 103 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_receiver) 104 | server.addBoundConnection(mk_server_cnxn) 105 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 106 | self.assertTrue(can_bind, 'Should still be able to bind as only one rx bound') 107 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 108 | self.assertTrue(can_bind, 'Should still be able to bind only bindings are rx') 109 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 110 | self.assertTrue(can_bind, 'Should still be able to bind as only one rx bound') 111 | 112 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_receiver) 113 | server.addBoundConnection(mk_server_cnxn) 114 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 115 | self.assertFalse(can_bind, 'Should not be able to bind as two rx already bound') 116 | # NOTE this one different 117 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 118 | self.assertTrue(can_bind, 'Should still be able to bind only bindings are rx') 119 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 120 | self.assertFalse(can_bind, 'Should not be able to bind as two rx already bound') 121 | 122 | def test_canOpenNewConnection_multitypes(self): 123 | server = SMPPServerFactory(self.config, None) 124 | 125 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 126 | self.assertTrue(can_bind, 'Should be able to bind trx as none bound yet') 127 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 128 | self.assertTrue(can_bind, 'Should be able to bind tx as none bound yet') 129 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 130 | self.assertTrue(can_bind, 'Should be able to bind rx as none bound yet') 131 | 132 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transceiver) 133 | server.addBoundConnection(mk_server_cnxn) 134 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 135 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 136 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 137 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 138 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 139 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx bound') 140 | 141 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_transmitter) 142 | server.addBoundConnection(mk_server_cnxn) 143 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 144 | self.assertFalse(can_bind, 'Should not be able to bind as one trx and one tx already bound') 145 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 146 | self.assertFalse(can_bind, 'Should not be able to bind as one trx and one tx already bound') 147 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 148 | self.assertTrue(can_bind, 'Should still be able to bind as only one trx and one tx bound') 149 | 150 | mk_server_cnxn = _makeMockServerConnection('lala', pdu_types.CommandId.bind_receiver) 151 | server.addBoundConnection(mk_server_cnxn) 152 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transceiver) 153 | self.assertFalse(can_bind, 'Should not be able to bind as one trx, one tx, and one rx already bound') 154 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_transmitter) 155 | self.assertFalse(can_bind, 'Should not be able to bind as one trx, one tx, and one rx already bound') 156 | can_bind = server.canOpenNewConnection('lala', pdu_types.CommandId.bind_receiver) 157 | self.assertFalse(can_bind, 'Should not be able to bind as one trx, one tx, and one rx already bound') 158 | 159 | 160 | class SMPPBindManagerTests(unittest.TestCase): 161 | 162 | def test_getNextBindingForDelivery(self): 163 | bm = SMPPBindManager('blah') 164 | 165 | # add an initial rx 166 | mk_server_cnxn_rx1 = _makeMockServerConnection('blah', pdu_types.CommandId.bind_receiver) 167 | bm.addBinding(mk_server_cnxn_rx1) 168 | 169 | # Get the only rx 170 | deliverer = bm.getNextBindingForDelivery() 171 | self.assertEqual(mk_server_cnxn_rx1, deliverer) 172 | 173 | # Get the only rx again 174 | deliverer = bm.getNextBindingForDelivery() 175 | self.assertEqual(mk_server_cnxn_rx1, deliverer) 176 | 177 | # Add a new rx 178 | mk_server_cnxn_rx2 = _makeMockServerConnection('blah', pdu_types.CommandId.bind_receiver) 179 | bm.addBinding(mk_server_cnxn_rx2) 180 | 181 | # Get the new rx 182 | deliverer = bm.getNextBindingForDelivery() 183 | self.assertEqual(mk_server_cnxn_rx2, deliverer) 184 | 185 | # Expect the original rx again 186 | deliverer = bm.getNextBindingForDelivery() 187 | self.assertEqual(mk_server_cnxn_rx1, deliverer) 188 | 189 | # Add a new tx - shouldn't affect deliverer selection at all 190 | mk_server_cnxn_tx1 = _makeMockServerConnection('blah', pdu_types.CommandId.bind_transmitter) 191 | bm.addBinding(mk_server_cnxn_tx1) 192 | 193 | # Expect the 2nd rx again 194 | deliverer = bm.getNextBindingForDelivery() 195 | self.assertEqual(mk_server_cnxn_rx2, deliverer) 196 | 197 | # Add a new trx 198 | mk_server_cnxn_trx1 = _makeMockServerConnection('blah', pdu_types.CommandId.bind_transceiver) 199 | bm.addBinding(mk_server_cnxn_trx1) 200 | 201 | # Expect the new trx 202 | deliverer = bm.getNextBindingForDelivery() 203 | self.assertEqual(mk_server_cnxn_trx1, deliverer) 204 | 205 | # Remove the 1st rx 206 | bm.removeBinding(mk_server_cnxn_rx1) 207 | 208 | # Expect the 2nd rx again 209 | deliverer = bm.getNextBindingForDelivery() 210 | self.assertEqual(mk_server_cnxn_rx2, deliverer) 211 | 212 | # Expect the new trx 213 | deliverer = bm.getNextBindingForDelivery() 214 | self.assertEqual(mk_server_cnxn_trx1, deliverer) 215 | 216 | # Expect the 2nd rx again 217 | deliverer = bm.getNextBindingForDelivery() 218 | self.assertEqual(mk_server_cnxn_rx2, deliverer) 219 | 220 | class SMPPServerBaseTest(TestCase): 221 | def _serviceHandler(self, system_id, smpp, pdu): 222 | self.service_calls.append((system_id, smpp, pdu)) 223 | return pdu_types.CommandStatus.ESME_ROK 224 | 225 | class SmppRealm(object): 226 | implements(IRealm) 227 | 228 | def requestAvatar(self, avatarId, mind, *interfaces): 229 | return ('SMPP', avatarId, lambda: None) 230 | 231 | def _bind(self): 232 | self.proto.bind_type = pdu_types.CommandId.bind_transceiver 233 | self.proto.sessionState = SMPPSessionStates.BOUND_TRX 234 | self.proto.system_id = 'userA' 235 | self.factory.addBoundConnection(self.proto) 236 | 237 | class SMPPServerTestCase(SMPPServerBaseTest): 238 | 239 | def setUp(self): 240 | self.service_calls = [] 241 | self.encoder = pdu_encoding.PDUEncoder() 242 | self.smpp_config = SMPPServerConfig(msgHandler=self._serviceHandler, 243 | systems={'userA': {"max_bindings": 2}} 244 | ) 245 | portal = Portal(self.SmppRealm()) 246 | credential_checker = InMemoryUsernamePasswordDatabaseDontUse() 247 | credential_checker.addUser('userA', 'valid') 248 | portal.registerChecker(credential_checker) 249 | self.factory = SMPPServerFactory(self.smpp_config, auth_portal=portal) 250 | self.proto = self.factory.buildProtocol(('127.0.0.1', 0)) 251 | self.tr = proto_helpers.StringTransport() 252 | self.proto.makeConnection(self.tr) 253 | 254 | def tearDown(self): 255 | self.proto.connectionLost('test end') 256 | 257 | def testTRXBindRequest(self): 258 | pdu = operations.BindTransceiver( 259 | system_id = 'userA', 260 | password = 'valid', 261 | seqNum = 1 262 | ) 263 | self.proto.dataReceived(self.encoder.encode(pdu)) 264 | expected_pdu = operations.BindTransceiverResp(system_id='userA', seqNum=1) 265 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 266 | connection = self.factory.getBoundConnections('userA') 267 | self.assertEqual(connection.system_id, 'userA') 268 | self.assertEqual(connection._binds[pdu_types.CommandId.bind_transceiver][0], self.proto) 269 | 270 | def testTRXBindRequestInvalidSysId(self): 271 | pdu = operations.BindTransceiver( 272 | system_id = 'userB', 273 | password = 'valid', 274 | seqNum = 1 275 | ) 276 | self.proto.dataReceived(self.encoder.encode(pdu)) 277 | expected_pdu = operations.BindTransceiverResp(system_id='userB', seqNum=1, status=pdu_types.CommandStatus.ESME_RINVPASWD) 278 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 279 | connection = self.factory.getBoundConnections('userA') 280 | self.assertEqual(connection, None) 281 | connection = self.factory.getBoundConnections('userB') 282 | self.assertEqual(connection, None) 283 | 284 | def testTRXBindRequestInvalidPassword(self): 285 | pdu = operations.BindTransceiver( 286 | system_id = 'userA', 287 | password = 'invalid', 288 | seqNum = 1 289 | ) 290 | self.proto.dataReceived(self.encoder.encode(pdu)) 291 | expected_pdu = operations.BindTransceiverResp(system_id='userA', seqNum=1, status=pdu_types.CommandStatus.ESME_RINVPASWD) 292 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 293 | connection = self.factory.getBoundConnections('userA') 294 | self.assertEqual(connection, None) 295 | 296 | def testTransmitterBindRequest(self): 297 | system_id = 'userA' 298 | pdu = operations.BindTransmitter( 299 | system_id = system_id, 300 | password = 'valid', 301 | seqNum = 1 302 | ) 303 | self.proto.dataReceived(self.encoder.encode(pdu)) 304 | expected_pdu = operations.BindTransmitterResp(system_id='userA', seqNum=1) 305 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 306 | self.tr.clear() 307 | connection = self.factory.getBoundConnections(system_id) 308 | self.assertEqual(connection.system_id, system_id) 309 | self.assertEqual(connection._binds[pdu_types.CommandId.bind_transmitter][0], self.proto) 310 | bind_manager = self.factory.getBoundConnections(system_id) 311 | delivery_binding = bind_manager.getNextBindingForDelivery() 312 | self.assertTrue(delivery_binding is None) 313 | 314 | def testReceiverBindRequest(self): 315 | system_id = 'userA' 316 | pdu = operations.BindReceiver( 317 | system_id = system_id, 318 | password = 'valid', 319 | seqNum = 1 320 | ) 321 | self.proto.dataReceived(self.encoder.encode(pdu)) 322 | expected_pdu = operations.BindReceiverResp(system_id='userA', seqNum=1) 323 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 324 | self.tr.clear() 325 | connection = self.factory.getBoundConnections(system_id) 326 | self.assertEqual(connection.system_id, system_id) 327 | self.assertEqual(connection._binds[pdu_types.CommandId.bind_receiver][0], self.proto) 328 | bind_manager = self.factory.getBoundConnections(system_id) 329 | delivery_binding = bind_manager.getNextBindingForDelivery() 330 | self.assertTrue(delivery_binding is not None) 331 | # TODO Identify what should be returned here 332 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=6) 333 | self.proto.dataReceived(self.encoder.encode(pdu)) 334 | expected_pdu = operations.SubmitSMResp(seqNum=6) 335 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 336 | 337 | 338 | def testUnboundSubmitRequest(self): 339 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=1) 340 | self.proto.dataReceived(self.encoder.encode(pdu)) 341 | expected_pdu = operations.SubmitSMResp(status=pdu_types.CommandStatus.ESME_RINVBNDSTS, seqNum=1) 342 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 343 | 344 | def testUnboundSubmitRequest(self): 345 | pdu = operations.EnquireLink(seqNum = 576) 346 | self.proto.dataReceived(self.encoder.encode(pdu)) 347 | expected_pdu = operations.EnquireLinkResp(status=pdu_types.CommandStatus.ESME_RINVBNDSTS, seqNum=576) 348 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 349 | 350 | def testTRXUnbindRequest(self): 351 | self._bind() 352 | pdu = operations.Unbind(seqNum = 346) 353 | self.proto.dataReceived(self.encoder.encode(pdu)) 354 | expected_pdu = operations.UnbindResp(seqNum=346) 355 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 356 | connection = self.factory.getBoundConnections('userA') 357 | self.assertEqual(connection.system_id, 'userA') 358 | # Still in list of binds as the connection has not been closed yet. is removed after test tearDown 359 | self.assertEqual(connection._binds[pdu_types.CommandId.bind_transceiver][0].sessionState, SMPPSessionStates.UNBOUND) 360 | 361 | def testTRXSubmitSM(self): 362 | self._bind() 363 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=6) 364 | self.proto.dataReceived(self.encoder.encode(pdu)) 365 | expected_pdu = operations.SubmitSMResp(seqNum=6) 366 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 367 | system_id, smpp, pdu_notified = self.service_calls.pop() 368 | self.assertEqual(system_id, self.proto.system_id) 369 | self.assertEqual(pdu.params['short_message'], pdu_notified.params['short_message']) 370 | self.assertEqual(pdu.params['source_addr'], pdu_notified.params['source_addr']) 371 | self.assertEqual(pdu.params['destination_addr'], pdu_notified.params['destination_addr']) 372 | 373 | def testTRXDataSM(self): 374 | self._bind() 375 | pdu = operations.DataSM(source_addr='t1', destination_addr='1208230', short_message='tests', seqNum=6) 376 | self.proto.dataReceived(self.encoder.encode(pdu)) 377 | expected_pdu = operations.DataSMResp(seqNum=6) 378 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 379 | system_id, smpp, pdu_notified = self.service_calls.pop() 380 | self.assertEqual(system_id, self.proto.system_id) 381 | 382 | def testTRXQuerySM(self): 383 | def _serviceHandler(system_id, smpp, pdu): 384 | self.service_calls.append((system_id, smpp, pdu)) 385 | return DataHandlerResponse(status=pdu_types.CommandStatus.ESME_ROK, 386 | message_id='tests', 387 | final_date=None, 388 | message_state=pdu_types.MessageState.ACCEPTED, 389 | error_code=0) 390 | self.proto.dataRequestHandler = lambda *args, **kwargs: _serviceHandler(self.proto.system_id, *args, **kwargs) 391 | self._bind() 392 | pdu = operations.QuerySM(message_id='tests', source_addr='t1', seqNum=23) 393 | self.proto.dataReceived(self.encoder.encode(pdu)) 394 | expected_pdu = operations.QuerySMResp(message_id='tests', error_code=0, final_date=None, message_state=pdu_types.MessageState.ACCEPTED ,seqNum=23) 395 | # Does not work as application using library must reply correctly. 396 | print self.tr.value() 397 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 398 | system_id, smpp, pdu_notified = self.service_calls.pop() 399 | self.assertEqual(system_id, self.proto.system_id) 400 | 401 | def testRecievePduWhileUnbindPending(self): 402 | self._bind() 403 | self.proto.unbind() 404 | expected_pdu = operations.Unbind(seqNum=1) 405 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 406 | self.tr.clear() 407 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=6) 408 | self.proto.dataReceived(self.encoder.encode(pdu)) 409 | expected_pdu = operations.SubmitSMResp(seqNum=6) 410 | self.assertEqual(self.tr.value(), '') 411 | pdu = operations.UnbindResp(seqNum=1) 412 | self.proto.dataReceived(self.encoder.encode(pdu)) 413 | 414 | def testTRXClientUnbindRequestAfterSubmit(self): 415 | d = defer.Deferred() 416 | def _serviceHandler(system_id, smpp, pdu): 417 | logging.debug("%s, %s, %s", system_id, smpp, pdu) 418 | return d 419 | self.proto.dataRequestHandler = lambda *args, **kwargs: _serviceHandler(self.proto.system_id, *args, **kwargs) 420 | self._bind() 421 | 422 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=1) 423 | self.proto.dataReceived(self.encoder.encode(pdu)) 424 | pdu = operations.Unbind(seqNum = 52) 425 | self.proto.dataReceived(self.encoder.encode(pdu)) 426 | 427 | #All PDU requests should fail now. 428 | #Once we fire this we should get our Submit Resp and the unbind Resp 429 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='goodbye', seqNum=5) 430 | self.proto.dataReceived(self.encoder.encode(pdu)) 431 | 432 | unbind_resp_pdu = operations.UnbindResp(seqNum=52) 433 | submit_fail_pdu = operations.SubmitSMResp(status=pdu_types.CommandStatus.ESME_RINVBNDSTS, seqNum=5) 434 | # We should have a reply here as our service handler should not be called 435 | self.assertEqual(self.tr.value(), self.encoder.encode(submit_fail_pdu)) 436 | self.tr.clear() 437 | 438 | d.callback(pdu_types.CommandStatus.ESME_ROK) 439 | #Then we should get our initial message response and the unbind response 440 | expected_pdu = operations.SubmitSMResp(seqNum=1) 441 | self.assertEqual(self.tr.value(), '%s%s' % (self.encoder.encode(expected_pdu), self.encoder.encode(unbind_resp_pdu))) 442 | 443 | def testTRXServerUnbindRequestAfterSubmit(self): 444 | deferreds = [] 445 | def _serviceHandler(system_id, smpp, pdu): 446 | d = defer.Deferred() 447 | deferreds.append(d) 448 | logging.debug("%s, %s, %s", system_id, smpp, pdu) 449 | return d 450 | self.proto.dataRequestHandler = lambda *args, **kwargs: _serviceHandler(self.proto.system_id, *args, **kwargs) 451 | self._bind() 452 | 453 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=1) 454 | self.proto.dataReceived(self.encoder.encode(pdu)) 455 | unbind_d = self.proto.unbind() 456 | print self.tr.value() 457 | 458 | pdu2 = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO2', seqNum=2) 459 | self.proto.dataReceived(self.encoder.encode(pdu)) 460 | 461 | self.assertEqual(1, len(deferreds)) 462 | self.assertEqual(self.tr.value(), '') 463 | self.tr.clear() 464 | deferreds[-1].callback(pdu_types.CommandStatus.ESME_ROK) 465 | deferreds = deferreds[:-1] 466 | submit_resp_pdu = operations.SubmitSMResp(seqNum=1) 467 | 468 | unbind_pdu = operations.Unbind(seqNum=1) 469 | # We should have a reply here as our service handler should not be called 470 | self.assertEqual(self.tr.value(), '%s%s' % (self.encoder.encode(submit_resp_pdu), self.encoder.encode(unbind_pdu))) 471 | self.tr.clear() 472 | pdu = operations.UnbindResp(seqNum=1) 473 | self.proto.dataReceived(self.encoder.encode(pdu)) 474 | 475 | class SMPPServerTimeoutTestCase(SMPPServerBaseTest): 476 | 477 | def setUp(self): 478 | self.service_calls = [] 479 | self.clock = task.Clock() 480 | self.encoder = pdu_encoding.PDUEncoder() 481 | self.smpp_config = SMPPServerConfig(msgHandler=self._serviceHandler, 482 | systems={'userA': {"max_bindings": 2}}, 483 | enquireLinkTimerSecs=0.1, 484 | responseTimerSecs=0.1 485 | ) 486 | portal = Portal(self.SmppRealm()) 487 | credential_checker = InMemoryUsernamePasswordDatabaseDontUse() 488 | credential_checker.addUser('userA', 'valid') 489 | portal.registerChecker(credential_checker) 490 | self.factory = SMPPServerFactory(self.smpp_config, auth_portal=portal) 491 | self.proto = self.factory.buildProtocol(('127.0.0.1', 0)) 492 | self.proto.callLater = self.clock.callLater 493 | self.tr = proto_helpers.StringTransport() 494 | self.proto.makeConnection(self.tr) 495 | 496 | def testEnquireTimeout(self): 497 | self._bind() 498 | pdu = operations.SubmitSM(source_addr='t1', destination_addr='1208230', short_message='HELLO', seqNum=6) 499 | self.proto.dataReceived(self.encoder.encode(pdu)) 500 | expected_pdu = operations.SubmitSMResp(seqNum=6) 501 | self.assertEqual(self.tr.value(), self.encoder.encode(expected_pdu)) 502 | self.service_calls.pop() 503 | self.tr.clear() 504 | self.clock.advance(0.1) 505 | self.clock.advance(0.1) 506 | self.assertEqual(self.proto.sessionState, SMPPSessionStates.UNBIND_PENDING) 507 | --------------------------------------------------------------------------------