Exemple #1
0
class SMPPProtocolBase(protocol.Protocol):
    """Short Message Peer to Peer Protocol v3.4 implementing ESME (client)"""
    version = 0x34

    def __init__(self):
        self.recvBuffer = ""
        self.connectionCorrupted = False
        self.pduReadTimer = None
        self.enquireLinkTimer = None
        self.inactivityTimer = None
        self.dataRequestHandler = None
        self.lastSeqNum = 0
        self.inTxns = {}
        self.outTxns = {}
        self.sessionState = SMPPSessionStates.NONE
        self.encoder = PDUEncoder()
        self.disconnectedDeferred = defer.Deferred()
        # Overriden in tests
        self.callLater = reactor.callLater
        self.port = None

    def config(self):
        return self.factory.getConfig()

    def connectionMade(self):
        """When TCP connection is made
        """
        protocol.Protocol.connectionMade(self)
        self.port = self.transport.getHost().port
        # Start the inactivity timer the connection is dropped if we receive no data
        self.activateInactivityTimer()
        self.sessionState = SMPPSessionStates.OPEN
        self.log.warning("SMPP connection established from %s to port %s", self.transport.getPeer().host, self.port)

    def connectionLost(self, reason):
        protocol.Protocol.connectionLost(self, reason)
        self.log.warning("SMPP %s disconnected from port %s: %s", self.transport.getPeer().host, self.port, reason)

        self.sessionState = SMPPSessionStates.NONE

        self.cancelEnquireLinkTimer()
        self.cancelInactivityTimer()

        self.disconnectedDeferred.callback(None)

    def dataReceived(self, data):
        """ Looks for a full PDU (protocol data unit) and passes it from
        rawMessageReceived.
        """
        # if self.log.isEnabledFor(logging.DEBUG):
        #     self.log.debug("Received data [%s]" % _safelylogOutPdu(data))

        self.recvBuffer = self.recvBuffer + data

        while True:
            if self.connectionCorrupted:
                return
            msg = self.readMessage()
            if msg is None:
                break
            self.endPDURead()
            self.rawMessageReceived(msg)

        if len(self.recvBuffer) > 0:
            self.incompletePDURead()

    def incompletePDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            return
        self.pduReadTimer = self.callLater(self.config().pduReadTimerSecs, self.onPDUReadTimeout)

    def endPDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            self.pduReadTimer.cancel()

    def readMessage(self):
        pduLen = self.getMessageLength()
        if pduLen is None:
            return None
        return self.getMessage(pduLen)

    def getMessageLength(self):
        if len(self.recvBuffer) < 4:
            return None
        return struct.unpack('!L', self.recvBuffer[:4])[0]

    def getMessage(self, pduLen):
        if len(self.recvBuffer) < pduLen:
            return None

        message = self.recvBuffer[:pduLen]
        self.recvBuffer = self.recvBuffer[pduLen:]
        return message

    def corruptDataRecvd(self, status=CommandStatus.ESME_RINVCMDLEN):
        self.sendPDU(GenericNack(status=status))
        self.onCorruptConnection()

    def onCorruptConnection(self):
        """ Once the connection is corrupt, the PDU boundaries are lost and it's impossible to
            continue processing messages.
                - Set a flag to indicate corrupt connection
                    - no more parse attempts should be made for inbound data
                    - no more outbound requests should be attempted (they should errback immediately)
                - Cancel outstanding outbound requests (which have not yet been ack'ed)
                    (removed from the list and errback called)
                - Shutdown
        """
        self.log.critical("Connection is corrupt!!! Shutting down...")
        self.connectionCorrupted = True
        self.cancelOutboundTransactions(SMPPClientConnectionCorruptedError())
        self.shutdown()

    def getHeader(self, message):
        try:
            return self.encoder.decodeHeader(StringIO.StringIO(message[:self.encoder.HEADER_LEN]))
        except:
            return {}

    def onPDUReadTimeout(self):
        self.log.critical('PDU read timed out. Buffer is now considered corrupt')
        self.corruptDataRecvd()

    def rawMessageReceived(self, message):
        """Called once a PDU (protocol data unit) boundary is identified.

        Creates an SMPP PDU class from the data and calls PDUReceived dispatcher
        """
        pdu = None
        try:
            pdu = self.encoder.decode(StringIO.StringIO(message))
        except PDUCorruptError as e:
            self.log.exception(e)
            self.log.critical("Received corrupt PDU %s" % _safelylogOutPdu(message))
            self.corruptDataRecvd(status=e.status)
        except PDUParseError as e:
            self.log.exception(e)
            self.log.critical("Received unparsable PDU %s" % _safelylogOutPdu(message))
            header = self.getHeader(message)
            seqNum = header.get('sequence_number', None)
            commandId = header.get('command_id', None)
            self.sendPDU(getPDUClass(commandId).requireAck(seqNum=seqNum, status=e.status))
        else:
            self.PDUReceived(pdu)

    def PDUReceived(self, pdu):
        """Dispatches incoming PDUs
        """
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Received PDU: %s" % pdu)

        encoded = self.encoder.encode(pdu)

        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Receiving data [%s]" % _safelylogOutPdu(encoded))

        # Signal SMPP operation
        self.onSMPPOperation()

        if isinstance(pdu, PDURequest):
            self.PDURequestReceived(pdu)
        elif isinstance(pdu, PDUResponse):
            self.PDUResponseReceived(pdu)
        else:
            getattr(self, "onPDU_%s" % str(pdu.id))(pdu)

    def PDURequestReceived(self, reqPDU):
        """Handle incoming request PDUs
        """
        if isinstance(reqPDU, PDUDataRequest):
            self.PDUDataRequestReceived(reqPDU)
            return

        getattr(self, "onPDURequest_%s" % str(reqPDU.id))(reqPDU)

    def onPDURequest_enquire_link(self, reqPDU):
        self.sendResponse(reqPDU)

    def onPDURequest_unbind(self, reqPDU):
        # Allow no more outbound data requests
        # Accept no more inbound requests
        self.sessionState = SMPPSessionStates.UNBIND_RECEIVED
        self.cancelEnquireLinkTimer()
        # Cancel outbound requests
        self.cancelOutboundTransactions(SMPPClientSessionStateError('Unbind received'))
        # Wait for inbound requests to finish then ack and disconnect
        self.finishInboundTxns().addCallback(lambda r: (self.sendResponse(reqPDU) or True) and self.disconnect())

    def sendResponse(self, reqPDU, status=CommandStatus.ESME_ROK, **params):
        self.sendPDU(reqPDU.requireAck(reqPDU.seqNum, status, **params))

    def PDUDataRequestReceived(self, reqPDU):
        if self.sessionState == SMPPSessionStates.UNBIND_PENDING:
            self.log.info("Unbind is pending...Ignoring data request PDU %s" % reqPDU)
            return

        if not self.isBound():
            errMsg = 'Received data request when not bound %s' % reqPDU
            self.cancelOutboundTransactions(SessionStateError(errMsg, CommandStatus.ESME_RINVBNDSTS))
            return self.fatalErrorOnRequest(reqPDU, errMsg, CommandStatus.ESME_RINVBNDSTS)

        if self.dataRequestHandler is None:
            return self.fatalErrorOnRequest(reqPDU, 'Missing dataRequestHandler', CommandStatus.ESME_RX_T_APPN)

        self.doPDURequest(reqPDU, self.dataRequestHandler)

    def fatalErrorOnRequest(self, reqPDU, errMsg, status):
        self.log.critical(errMsg)
        self.sendResponse(reqPDU, status)
        self.shutdown()

    def doPDURequest(self, reqPDU, handler):
        self.startInboundTransaction(reqPDU)

        handlerCall = defer.maybeDeferred(handler, self, reqPDU)
        handlerCall.addCallback(self.PDURequestSucceeded, reqPDU)
        handlerCall.addErrback(self.PDURequestFailed, reqPDU)
        handlerCall.addBoth(self.PDURequestFinished, reqPDU)

    def PDURequestSucceeded(self, dataHdlrResp, reqPDU):
        if reqPDU.requireAck:
            status = CommandStatus.ESME_ROK
            params = {}
            if dataHdlrResp:
                if dataHdlrResp in CommandStatus:
                    status = dataHdlrResp
                elif isinstance(dataHdlrResp, DataHandlerResponse):
                    status = dataHdlrResp.status
                    params = dataHdlrResp.params
                else:
                    self.log.critical("Invalid response type returned from data handler %s" % type(dataHdlrResp))
                    status = CommandStatus.ESME_RX_T_APPN
                    self.shutdown()

            self.sendResponse(reqPDU, status, **params)

    def PDURequestFailed(self, error, reqPDU):
        if error.check(SMPPProtocolError):
            # Get the original error
            try:
                error.raiseException()
            except SMPPProtocolError as validation_error:
                self.log.debug("Application raised error '%s', forwarding to client. Inbound PDU was [%s], hex[%s]" % (
                validation_error, reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU))))
                return_cmd_status = validation_error.commandStatusName
                shutdown = False
        else:
            self.log.critical('Exception raised handling inbound PDU [%s] hex[%s]: %s' % (
            reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU)), error))
            return_cmd_status = CommandStatus.ESME_RX_T_APPN
            shutdown = True

        if reqPDU.requireAck:
            self.sendResponse(reqPDU, return_cmd_status)

        if shutdown:
            self.shutdown()

    def PDURequestFinished(self, result, reqPDU):
        self.endInboundTransaction(reqPDU)
        return result

    def finishTxns(self):
        return defer.DeferredList([self.finishInboundTxns(), self.finishOutboundTxns()])

    def finishInboundTxns(self):
        return defer.DeferredList(self.inTxns.values())

    def finishOutboundTxns(self):
        return defer.DeferredList([txn.ackDeferred for txn in self.outTxns.values()])

    def PDUResponseReceived(self, pdu):
        """Handle incoming response PDUs
        """
        if isinstance(pdu, GenericNack):
            self.log.critical("Recevied generic_nack %s" % pdu)
            if pdu.seqNum is None:
                self.onCorruptConnection()
                return

        if pdu.seqNum not in self.outTxns:
            self.log.critical('Response PDU received with unknown outbound transaction sequence number %s' % pdu)
            return

        self.endOutboundTransaction(pdu)

    def sendPDU(self, pdu):
        """Send a SMPP PDU
        """
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Sending PDU: %s" % pdu)
        encoded = self.encoder.encode(pdu)

        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Sending data [%s]" % _safelylogOutPdu(encoded))

        self.transport.write(encoded)
        self.onSMPPOperation()

    def sendBindRequest(self, pdu):
        return self.sendRequest(pdu, self.config().sessionInitTimerSecs)

    def sendRequest(self, pdu, timeout):
        return defer.maybeDeferred(self.doSendRequest, pdu, timeout)

    def doSendRequest(self, pdu, timeout):
        if self.connectionCorrupted:
            raise SMPPClientConnectionCorruptedError()
        if not isinstance(pdu, PDURequest) or pdu.requireAck is None:
            raise SMPPClientError("Invalid PDU to send: %s" % pdu)

        pdu.seqNum = self.claimSeqNum()
        self.sendPDU(pdu)
        return self.startOutboundTransaction(pdu, timeout)

    def onSMPPOperation(self):
        """Called whenever an SMPP PDU is sent or received
        """
        if self.isBound():
            self.activateEnquireLinkTimer()

        self.activateInactivityTimer()

    def activateEnquireLinkTimer(self):
        if self.enquireLinkTimer and self.enquireLinkTimer.active():
            self.enquireLinkTimer.reset(self.config().enquireLinkTimerSecs)
        elif self.config().enquireLinkTimerSecs:
            self.enquireLinkTimer = self.callLater(self.config().enquireLinkTimerSecs, self.enquireLinkTimerExpired)

    def activateInactivityTimer(self):
        if self.inactivityTimer and self.inactivityTimer.active():
            self.inactivityTimer.reset(self.config().inactivityTimerSecs)
        elif self.config().inactivityTimerSecs:
            self.inactivityTimer = self.callLater(self.config().inactivityTimerSecs, self.inactivityTimerExpired)

    def cancelEnquireLinkTimer(self):
        if self.enquireLinkTimer and self.enquireLinkTimer.active():
            self.enquireLinkTimer.cancel()
            self.enquireLinkTimer = None

    def cancelInactivityTimer(self):
        if self.inactivityTimer and self.inactivityTimer.active():
            self.inactivityTimer.cancel()
            self.inactivityTimer = None

    def enquireLinkTimerExpired(self):
        txn = self.sendRequest(EnquireLink(), self.config().responseTimerSecs)
        txn.addErrback(self.enquireLinkErr)

    def enquireLinkErr(self, failure):
        # Unbinding already anyway. No need to raise another error
        failure.trap(SMPPError)

    def inactivityTimerExpired(self):
        self.log.critical("Inactivity timer expired...shutting down")
        self.shutdown()

    def isBound(self):
        return self.sessionState in (
        SMPPSessionStates.BOUND_TX, SMPPSessionStates.BOUND_RX, SMPPSessionStates.BOUND_TRX)

    def shutdown(self):
        """ Unbind if appropriate and disconnect """

        if self.isBound() and not self.connectionCorrupted:
            self.log.warning("Shutdown requested...unbinding")
            self.unbind().addBoth(lambda result: self.disconnect())
        elif self.sessionState not in (SMPPSessionStates.UNBIND_RECEIVED, SMPPSessionStates.UNBIND_PENDING):
            self.log.warning("Shutdown requested...disconnecting")
            self.disconnect()
        else:
            self.log.debug("Shutdown already in progress")

    def startInboundTransaction(self, reqPDU):
        if reqPDU.seqNum in self.inTxns:
            raise SMPPProtocolError('Duplicate message id [%s] received.  Already in progess.' % reqPDU.seqNum,
                                    CommandStatus.ESME_RUNKNOWNERR)
        txnDeferred = defer.Deferred()
        self.inTxns[reqPDU.seqNum] = txnDeferred
        self.log.debug("Inbound transaction started with message id %s" % reqPDU.seqNum)
        return txnDeferred

    def endInboundTransaction(self, reqPDU):
        if not reqPDU.seqNum in self.inTxns:
            raise ValueError('Unknown inbound sequence number in transaction for request PDU %s' % reqPDU)

        self.log.debug("Inbound transaction finished with message id %s" % reqPDU.seqNum)
        self.inTxns[reqPDU.seqNum].callback(reqPDU)
        del self.inTxns[reqPDU.seqNum]

    def startOutboundTransaction(self, reqPDU, timeout):
        if reqPDU.seqNum in self.outTxns:
            raise ValueError('Seq number [%s] is already in progess.' % reqPDU.seqNum)

        # Create callback deferred
        ackDeferred = defer.Deferred()
        # Create response timer
        timer = self.callLater(timeout, self.onResponseTimeout, reqPDU, timeout)
        # Save transaction
        self.outTxns[reqPDU.seqNum] = SMPPOutboundTxn(reqPDU, timer, ackDeferred)
        self.log.debug("Outbound transaction started with message id %s" % reqPDU.seqNum)
        return ackDeferred

    def closeOutboundTransaction(self, seqNum):
        self.log.debug("Outbound transaction finished with message id %s" % seqNum)

        txn = self.outTxns[seqNum]
        # Remove txn
        del self.outTxns[seqNum]
        # Cancel response timer
        if txn.timer.active():
            txn.timer.cancel()
        return txn

    def endOutboundTransaction(self, respPDU):
        txn = self.closeOutboundTransaction(respPDU.seqNum)

        if respPDU.status == CommandStatus.ESME_ROK:
            if not isinstance(respPDU, txn.request.requireAck):
                txn.ackDeferred.errback(SMPPProtocolError(
                    "Invalid PDU response type [%s] returned for request type [%s]" % (
                    type(respPDU), type(txn.request))))
                return
            # Do callback
            txn.ackDeferred.callback(SMPPOutboundTxnResult(self, txn.request, respPDU))
            return

        if isinstance(respPDU, GenericNack):
            txn.ackDeferred.errback(SMPPGenericNackTransactionError(respPDU, txn.request))
            return

        errCode = respPDU.status
        txn.ackDeferred.errback(SMPPTransactionError(respPDU, txn.request))

    def endOutboundTransactionErr(self, reqPDU, error):
        self.log.error(error)
        txn = self.closeOutboundTransaction(reqPDU.seqNum)
        # Do errback
        txn.ackDeferred.errback(error)

    def cancelOutboundTransactions(self, error):
        for txn in self.outTxns.values():
            self.endOutboundTransactionErr(txn.request, error)

    def onResponseTimeout(self, reqPDU, timeout):
        errMsg = 'Request timed out after %s secs: %s' % (timeout, reqPDU)
        self.endOutboundTransactionErr(reqPDU, SMPPRequestTimoutError(errMsg))
        self.shutdown()

    def claimSeqNum(self):
        self.lastSeqNum += 1
        return self.lastSeqNum

    def unbindSucceeded(self, result):
        self.sessionState = SMPPSessionStates.UNBOUND
        self.log.warning("Unbind succeeded")
        return result

    def unbindFailed(self, reason):
        self.log.error("Unbind failed [%s]. Disconnecting..." % reason)
        self.disconnect()
        if reason.check(SMPPRequestTimoutError):
            raise SMPPSessionInitTimoutError(str(reason))
        return reason

    def unbindAfterInProgressTxnsFinished(self, result, unbindDeferred):
        self.log.warning('Issuing unbind request')
        self.sendBindRequest(Unbind()).addCallbacks(self.unbindSucceeded, self.unbindFailed).chainDeferred(
            unbindDeferred)

    ############################################################################
    # Public command functions
    ############################################################################
    def unbind(self):
        """Unbind from SMSC
        
        Result is a Deferred object
        """
        if not self.isBound():
            return defer.fail(
                SMPPClientSessionStateError('unbind called with illegal session state: %s' % self.sessionState))

        self.cancelEnquireLinkTimer()

        self.log.info('Waiting for in-progress transactions to finish...')

        # Signal that
        #   - no new data requests should be sent
        #   - no new incoming data requests should be accepted
        self.sessionState = SMPPSessionStates.UNBIND_PENDING

        unbindDeferred = defer.Deferred()
        # Wait for any in-progress txns to finish
        self.finishTxns().addCallback(self.unbindAfterInProgressTxnsFinished, unbindDeferred)
        # Result is the deferred for the unbind txn
        return unbindDeferred

    def unbindAndDisconnect(self):
        """Unbind from SMSC and disconnect
        
        Result is a Deferred object
        """
        return self.unbind().addBoth(lambda result: self.disconnect())

    def disconnect(self):
        """Disconnect from SMSC
        """
        if self.isBound():
            self.log.warning("Disconnecting while bound to SMSC...")
        else:
            self.log.warning("Disconnecting...")
        self.sessionState = SMPPSessionStates.UNBOUND
        self.transport.loseConnection()

    def getDisconnectedDeferred(self):
        """Get a Deferred so you can be notified on disconnect
        """
        return self.disconnectedDeferred

    def sendDataRequest(self, pdu):
        """Send a SMPP Request Message

        Argument is an SMPP PDUDataRequest (protocol data unit).
        Result is a Deferred object
        """
        if not isinstance(pdu, PDUDataRequest):
            return defer.fail(SMPPClientError("Invalid PDU passed to sendDataRequest(): %s" % pdu))
        if not self.isBound():
            return defer.fail(SMPPClientSessionStateError('Not bound'))
        return self.sendRequest(pdu, self.config().responseTimerSecs)
Exemple #2
0
class SMPPClientProtocol(protocol.Protocol):
    """Short Message Peer to Peer Protocol v3.4 implementing ESME (client)"""
    version = 0x34

    def __init__(self):
        self.log = logging.getLogger(LOG_CATEGORY)
        self.recvBuffer = ""
        self.connectionCorrupted = False
        self.pduReadTimer = None
        self.enquireLinkTimer = None
        self.inactivityTimer = None
        self.lastSeqNum = 0
        self.dataRequestHandler = None
        self.alertNotificationHandler = None
        self.inTxns = {}
        self.outTxns = {}
        self.sessionState = SMPPSessionStates.NONE
        self.encoder = PDUEncoder()
        self.disconnectedDeferred = defer.Deferred()

    def config(self):
        return self.factory.getConfig()

    def connectionMade(self):
        """When TCP connection is made
        """
        protocol.Protocol.connectionMade(self)

        self.sessionState = SMPPSessionStates.OPEN
        self.log.warning("Connection established")

    def connectionLost(self, reason):
        protocol.Protocol.connectionLost(self, reason)
        self.log.warning("Disconnected: %s" % reason)

        self.sessionState = SMPPSessionStates.NONE

        self.cancelEnquireLinkTimer()
        self.cancelInactivityTimer()

        self.disconnectedDeferred.callback(None)

    def dataReceived(self, data):
        """ Looks for a full PDU (protocol data unit) and passes it from
        rawMessageReceived.
        """
        # if self.log.isEnabledFor(logging.DEBUG):
        #     self.log.debug("Received data [%s]" % binascii.b2a_hex(data))

        self.recvBuffer = self.recvBuffer + data

        while True:
            if self.connectionCorrupted:
                return
            msg = self.readMessage()
            if msg is None:
                break
            self.endPDURead()
            self.rawMessageReceived(msg)

        if len(self.recvBuffer) > 0:
            self.incompletePDURead()

    def incompletePDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            return
        self.pduReadTimer = reactor.callLater(self.config().pduReadTimerSecs,
                                              self.onPDUReadTimeout)

    def endPDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            self.pduReadTimer.cancel()

    def readMessage(self):
        pduLen = self.getMessageLength()
        if pduLen is None:
            return None
        return self.getMessage(pduLen)

    def getMessageLength(self):
        if len(self.recvBuffer) < 4:
            return None
        return struct.unpack('!L', self.recvBuffer[:4])[0]

    def getMessage(self, pduLen):
        if len(self.recvBuffer) < pduLen:
            return None

        message = self.recvBuffer[:pduLen]
        self.recvBuffer = self.recvBuffer[pduLen:]
        return message

    def corruptDataRecvd(self, status=CommandStatus.ESME_RINVCMDLEN):
        self.sendPDU(GenericNack(status=status))
        self.onCorruptConnection()

    def onCorruptConnection(self):
        """ Once the connection is corrupt, the PDU boundaries are lost and it's impossible to
            continue processing messages.
                - Set a flag to indicate corrupt connection
                    - no more parse attempts should be made for inbound data
                    - no more outbound requests should be attempted (they should errback immediately)
                - Cancel outstanding outbound requests (which have not yet been ack'ed)
                    (removed from the list and errback called)
                - Shutdown
        """
        self.log.critical("Connection is corrupt!!! Shutting down...")
        self.connectionCorrupted = True
        self.cancelOutboundTransactions(SMPPClientConnectionCorruptedError())
        self.shutdown()

    def getHeader(self, message):
        try:
            return self.encoder.decodeHeader(
                StringIO.StringIO(message[:self.encoder.HEADER_LEN]))
        except:
            return {}

    def onPDUReadTimeout(self):
        self.log.critical(
            'PDU read timed out. Buffer is now considered corrupt')
        self.corruptDataRecvd()

    def rawMessageReceived(self, message):
        """Called once a PDU (protocol data unit) boundary is identified.

        Creates an SMPP PDU class from the data and calls PDUReceived dispatcher
        """
        pdu = None
        try:
            pdu = self.encoder.decode(StringIO.StringIO(message))
        except PDUCorruptError, e:
            self.log.exception(e)
            self.log.critical("Received corrupt PDU %s" %
                              binascii.b2a_hex(message))
            self.corruptDataRecvd(status=e.status)
        except PDUParseError, e:
            self.log.exception(e)
            self.log.critical("Received unparsable PDU %s" %
                              binascii.b2a_hex(message))
            header = self.getHeader(message)
            seqNum = header.get('sequence_number', None)
            commandId = header.get('command_id', None)
            self.sendPDU(
                getPDUClass(commandId).requireAck(seqNum=seqNum,
                                                  status=e.status))
Exemple #3
0
class SMPPClientProtocol( protocol.Protocol ):
    """Short Message Peer to Peer Protocol v3.4 implementing ESME (client)"""
    version = 0x34

    def __init__( self ):
        self.log = logging.getLogger(LOG_CATEGORY)
        self.recvBuffer = ""
        self.connectionCorrupted = False
        self.pduReadTimer = None
        self.enquireLinkTimer = None
        self.inactivityTimer = None
        self.lastSeqNum = 0
        self.dataRequestHandler = None
        self.alertNotificationHandler = None
        self.inTxns = {}
        self.outTxns = {}
        self.sessionState = SMPPSessionStates.NONE
        self.encoder = PDUEncoder()
        self.disconnectedDeferred = defer.Deferred()

    def config(self):
        return self.factory.getConfig()

    def connectionMade(self):
        """When TCP connection is made
        """
        protocol.Protocol.connectionMade(self)

        self.sessionState = SMPPSessionStates.OPEN
        self.log.warning("Connection established")

    def connectionLost( self, reason ):
        protocol.Protocol.connectionLost( self, reason )
        self.log.warning("Disconnected: %s" % reason)
        
        self.sessionState = SMPPSessionStates.NONE

        self.cancelEnquireLinkTimer()
        self.cancelInactivityTimer()

        self.disconnectedDeferred.callback(None)

    def dataReceived( self, data ):
        """ Looks for a full PDU (protocol data unit) and passes it from
        rawMessageReceived.
        """
        # if self.log.isEnabledFor(logging.DEBUG):
        #     self.log.debug("Received data [%s]" % binascii.b2a_hex(data))
        
        self.recvBuffer = self.recvBuffer + data
        
        while True:
            if self.connectionCorrupted:
                return
            msg = self.readMessage()
            if msg is None:
                break
            self.endPDURead()
            self.rawMessageReceived(msg)
            
        if len(self.recvBuffer) > 0:
            self.incompletePDURead()
                  
    def incompletePDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            return
        self.pduReadTimer = reactor.callLater(self.config().pduReadTimerSecs, self.onPDUReadTimeout)

    def endPDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            self.pduReadTimer.cancel()

    def readMessage(self):
        pduLen = self.getMessageLength()
        if pduLen is None:
            return None
        return self.getMessage(pduLen)

    def getMessageLength(self):
        if len(self.recvBuffer) < 4:
            return None
        return struct.unpack('!L', self.recvBuffer[:4])[0]
    
    def getMessage(self, pduLen):
        if len(self.recvBuffer) < pduLen:
            return None
        
        message = self.recvBuffer[:pduLen]
        self.recvBuffer = self.recvBuffer[pduLen:]
        return message        
        
    def corruptDataRecvd(self, status=CommandStatus.ESME_RINVCMDLEN):
        self.sendPDU(GenericNack(status=status))
        self.onCorruptConnection()
        
    def onCorruptConnection(self):
        """ Once the connection is corrupt, the PDU boundaries are lost and it's impossible to
            continue processing messages.
                - Set a flag to indicate corrupt connection
                    - no more parse attempts should be made for inbound data
                    - no more outbound requests should be attempted (they should errback immediately)
                - Cancel outstanding outbound requests (which have not yet been ack'ed)
                    (removed from the list and errback called)
                - Shutdown
        """
        self.log.critical("Connection is corrupt!!! Shutting down...")
        self.connectionCorrupted = True
        self.cancelOutboundTransactions(SMPPClientConnectionCorruptedError())
        self.shutdown()

    def getHeader(self, message):
        try:            
            return self.encoder.decodeHeader(StringIO.StringIO(message[:self.encoder.HEADER_LEN]))
        except:
            return {}
    
    def onPDUReadTimeout(self):
        self.log.critical('PDU read timed out. Buffer is now considered corrupt')
        self.corruptDataRecvd()

    def rawMessageReceived( self, message ):
        """Called once a PDU (protocol data unit) boundary is identified.

        Creates an SMPP PDU class from the data and calls PDUReceived dispatcher
        """
        pdu = None
        try:
            pdu = self.encoder.decode(StringIO.StringIO(message))
        except PDUCorruptError, e:
            self.log.exception(e)
            self.log.critical("Received corrupt PDU %s" % binascii.b2a_hex(message))
            self.corruptDataRecvd(status=e.status)
        except PDUParseError, e:
            self.log.exception(e)
            self.log.critical("Received unparsable PDU %s" % binascii.b2a_hex(message))
            header = self.getHeader(message)
            seqNum = header.get('sequence_number', None)
            commandId = header.get('command_id', None)
            self.sendPDU(getPDUClass(commandId).requireAck(seqNum=seqNum, status=e.status))
Exemple #4
0
class SMPPProtocolBase(Protocol):
    """Short Message Peer to Peer Protocol v3.4 implementing ESME (client)"""
    version = 0x34

    def __init__(self):
        self.recvBuffer = b""
        self.connectionCorrupted = False
        self.pduReadTimer = None
        self.enquireLinkTimer = None
        self.inactivityTimer = None
        self.dataRequestHandler = None
        self.lastSeqNum = 0
        self.inTxns = {}
        self.outTxns = {}
        self.sessionState = SMPPSessionStates.NONE
        self.encoder = PDUEncoder()
        self.disconnectedDeferred = defer.Deferred()
        # Overriden in tests
        #pylint: disable=no-member
        self.callLater = reactor.callLater
        self.port = None
        self.log = logging.getLogger(LOG_CATEGORY)

    def config(self):
        #pylint: disable=no-member
        return self.factory.getConfig()

    def connectionMade(self):
        """When TCP connection is made
        """
        Protocol.connectionMade(self)
        self.port = self.transport.getHost().port
        # Start the inactivity timer the connection is dropped if we receive no data
        self.activateInactivityTimer()
        self.sessionState = SMPPSessionStates.OPEN
        self.log.warning("SMPP connection established from %s to port %s", self.transport.getPeer().host, self.port)

    def connectionLost(self, reason):
        Protocol.connectionLost(self, reason)
        self.log.warning("SMPP %s disconnected from port %s: %s", self.transport.getPeer().host, self.port, reason)

        self.sessionState = SMPPSessionStates.NONE

        self.cancelEnquireLinkTimer()
        self.cancelInactivityTimer()

        self.disconnectedDeferred.callback(None)

    def dataReceived(self, data):
        """ Looks for a full PDU (protocol data unit) and passes it from
        rawMessageReceived.
        """
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Received data [%s]" % _safelylogOutPdu(data))

        self.recvBuffer = self.recvBuffer + data

        while True:
            if self.connectionCorrupted:
                return
            msg = self.readMessage()
            if msg is None:
                break
            self.endPDURead()
            self.rawMessageReceived(msg)

        if len(self.recvBuffer) > 0:
            self.incompletePDURead()

    def incompletePDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            return
        self.pduReadTimer = self.callLater(self.config().pduReadTimerSecs, self.onPDUReadTimeout)

    def endPDURead(self):
        if self.pduReadTimer and self.pduReadTimer.active():
            self.pduReadTimer.cancel()

    def readMessage(self):
        pduLen = self.getMessageLength()
        if pduLen is None:
            return None
        return self.getMessage(pduLen)

    def getMessageLength(self):
        if len(self.recvBuffer) < 4:
            return None
        return struct.unpack('!L', self.recvBuffer[:4])[0]

    def getMessage(self, pduLen):
        if len(self.recvBuffer) < pduLen:
            return None

        message = self.recvBuffer[:pduLen]
        self.recvBuffer = self.recvBuffer[pduLen:]
        return message

    def corruptDataRecvd(self, status=CommandStatus.ESME_RINVCMDLEN):
        self.sendPDU(GenericNack(status=status))
        self.onCorruptConnection()

    def onCorruptConnection(self):
        """ Once the connection is corrupt, the PDU boundaries are lost and it's impossible to
            continue processing messages.
                - Set a flag to indicate corrupt connection
                    - no more parse attempts should be made for inbound data
                    - no more outbound requests should be attempted (they should errback immediately)
                - Cancel outstanding outbound requests (which have not yet been ack'ed)
                    (removed from the list and errback called)
                - Shutdown
        """
        self.log.critical("Connection is corrupt!!! Shutting down...")
        self.connectionCorrupted = True
        self.cancelOutboundTransactions(SMPPClientConnectionCorruptedError())
        self.shutdown()

    def getHeader(self, message):
        try:
            return self.encoder.decodeHeader(BytesIO(message[:self.encoder.HEADER_LEN]))
        except:
            return {}

    def onPDUReadTimeout(self):
        self.log.critical('PDU read timed out. Buffer is now considered corrupt')
        self.corruptDataRecvd()

    def rawMessageReceived(self, message):
        """Called once a PDU (protocol data unit) boundary is identified.

        Creates an SMPP PDU class from the data and calls PDUReceived dispatcher
        """
        pdu = None
        try:
            pdu = self.encoder.decode(BytesIO(message))
        except PDUCorruptError as e:
            self.log.exception(e)
            self.log.critical("Received corrupt PDU %s" % _safelylogOutPdu(message))
            self.corruptDataRecvd(status=e.status)
        except PDUParseError as e:
            self.log.exception(e)
            self.log.critical("Received unparsable PDU %s" % _safelylogOutPdu(message))
            header = self.getHeader(message)
            seqNum = header.get('sequence_number', None)
            commandId = header.get('command_id', None)
            self.sendPDU(getPDUClass(commandId).requireAck(seqNum=seqNum, status=e.status))
        else:
            self.PDUReceived(pdu)

    def PDUReceived(self, pdu):
        """Dispatches incoming PDUs
        """
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Received PDU: %s" % pdu)

        encoded = self.encoder.encode(pdu)

        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Receiving data [%s]" % _safelylogOutPdu(encoded))

        # Signal SMPP operation
        self.onSMPPOperation()

        if isinstance(pdu, PDURequest):
            self.PDURequestReceived(pdu)
        elif isinstance(pdu, PDUResponse):
            self.PDUResponseReceived(pdu)
        else:
            getattr(self, "onPDU_%s" % pdu.id.name)(pdu)

    def PDURequestReceived(self, reqPDU):
        """Handle incoming request PDUs
        """
        if isinstance(reqPDU, PDUDataRequest):
            self.PDUDataRequestReceived(reqPDU)
            return

        getattr(self, "onPDURequest_%s" % reqPDU.id.name)(reqPDU)

    def onPDURequest_enquire_link(self, reqPDU):
        self.sendResponse(reqPDU)

    def onPDURequest_unbind(self, reqPDU):
        # Allow no more outbound data requests
        # Accept no more inbound requests
        self.sessionState = SMPPSessionStates.UNBIND_RECEIVED
        self.cancelEnquireLinkTimer()
        # Cancel outbound requests
        self.cancelOutboundTransactions(SMPPClientSessionStateError('Unbind received'))
        # Wait for inbound requests to finish then ack and disconnect
        self.finishInboundTxns().addCallback(lambda r: (self.sendResponse(reqPDU) or True) and self.disconnect())

    def sendResponse(self, reqPDU, status=CommandStatus.ESME_ROK, **params):
        self.sendPDU(reqPDU.requireAck(reqPDU.seqNum, status, **params))

    def PDUDataRequestReceived(self, reqPDU):
        if self.sessionState == SMPPSessionStates.UNBIND_PENDING:
            self.log.info("Unbind is pending...Ignoring data request PDU %s" % reqPDU)
            return

        if not self.isBound():
            errMsg = 'Received data request when not bound %s' % reqPDU
            self.cancelOutboundTransactions(SessionStateError(errMsg, CommandStatus.ESME_RINVBNDSTS))
            return self.fatalErrorOnRequest(reqPDU, errMsg, CommandStatus.ESME_RINVBNDSTS)

        if self.dataRequestHandler is None:
            return self.fatalErrorOnRequest(reqPDU, 'Missing dataRequestHandler', CommandStatus.ESME_RX_T_APPN)

        self.doPDURequest(reqPDU, self.dataRequestHandler)

    def fatalErrorOnRequest(self, reqPDU, errMsg, status):
        self.log.critical(errMsg)
        self.sendResponse(reqPDU, status)
        self.shutdown()

    def doPDURequest(self, reqPDU, handler):
        self.startInboundTransaction(reqPDU)

        handlerCall = defer.maybeDeferred(handler, self, reqPDU)
        handlerCall.addCallback(self.PDURequestSucceeded, reqPDU)
        handlerCall.addErrback(self.PDURequestFailed, reqPDU)
        handlerCall.addBoth(self.PDURequestFinished, reqPDU)

    def PDURequestSucceeded(self, dataHdlrResp, reqPDU):
        if reqPDU.requireAck:
            status = CommandStatus.ESME_ROK
            params = {}
            if dataHdlrResp:
                if dataHdlrResp in list(CommandStatus):
                    status = dataHdlrResp
                elif isinstance(dataHdlrResp, DataHandlerResponse):
                    status = dataHdlrResp.status
                    params = dataHdlrResp.params
                else:
                    self.log.critical("Invalid response type returned from data handler %s" % type(dataHdlrResp))
                    status = CommandStatus.ESME_RX_T_APPN
                    self.shutdown()

            self.sendResponse(reqPDU, status, **params)

    def PDURequestFailed(self, error, reqPDU):
        if error.check(SMPPProtocolError):
            # Get the original error
            try:
                error.raiseException()
            except SMPPProtocolError as validation_error:
                self.log.info("Application raised error '%s', forwarding to client. Inbound PDU was [%s], hex[%s]" % (
                validation_error, reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU))))
                # Jasmin update: validation_error have attribute named commandStatusName
                # return_cmd_status = validation_error.commandStatusName
                return_cmd_status = validation_error.status
                shutdown = False
        else:
            self.log.critical('Exception raised handling inbound PDU [%s] hex[%s]: %s' % (
            reqPDU, _safelylogOutPdu(self.encoder.encode(reqPDU)), error))
            return_cmd_status = CommandStatus.ESME_RX_T_APPN
            shutdown = True

        if reqPDU.requireAck:
            self.sendResponse(reqPDU, return_cmd_status)

        if shutdown:
            self.shutdown()

    def PDURequestFinished(self, result, reqPDU):
        self.endInboundTransaction(reqPDU)
        return result

    def finishTxns(self):
        return defer.DeferredList([self.finishInboundTxns(), self.finishOutboundTxns()])

    def finishInboundTxns(self):
        return defer.DeferredList(self.inTxns.values())

    def finishOutboundTxns(self):
        return defer.DeferredList([txn.ackDeferred for txn in list(self.outTxns.values())])

    def PDUResponseReceived(self, pdu):
        """Handle incoming response PDUs
        """
        if isinstance(pdu, GenericNack):
            self.log.critical("Recevied generic_nack %s" % pdu)
            if pdu.seqNum is None:
                self.onCorruptConnection()
                return

        if pdu.seqNum not in self.outTxns:
            self.log.critical('Response PDU received with unknown outbound transaction sequence number %s' % pdu)
            return

        self.endOutboundTransaction(pdu)

    def sendPDU(self, pdu):
        """Send a SMPP PDU
        """
        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Sending PDU: %s" % pdu)
        encoded = self.encoder.encode(pdu)

        if self.log.isEnabledFor(logging.DEBUG):
            self.log.debug("Sending data [%s]" % _safelylogOutPdu(encoded))

        self.transport.write(encoded)
        self.onSMPPOperation()

    def sendBindRequest(self, pdu):
        return self.sendRequest(pdu, self.config().sessionInitTimerSecs)

    def sendRequest(self, pdu, timeout):
        return defer.maybeDeferred(self.doSendRequest, pdu, timeout)

    def doSendRequest(self, pdu, timeout):
        if self.connectionCorrupted:
            raise SMPPClientConnectionCorruptedError()
        if not isinstance(pdu, PDURequest) or pdu.requireAck is None:
            raise SMPPClientError("Invalid PDU to send: %s" % pdu)

        pdu.seqNum = self.claimSeqNum()
        self.sendPDU(pdu)
        return self.startOutboundTransaction(pdu, timeout)

    def onSMPPOperation(self):
        """Called whenever an SMPP PDU is sent or received
        """
        if self.isBound():
            self.activateEnquireLinkTimer()

        self.activateInactivityTimer()

    def activateEnquireLinkTimer(self):
        if self.enquireLinkTimer and self.enquireLinkTimer.active():
            self.enquireLinkTimer.reset(self.config().enquireLinkTimerSecs)
        elif self.config().enquireLinkTimerSecs:
            self.enquireLinkTimer = self.callLater(self.config().enquireLinkTimerSecs, self.enquireLinkTimerExpired)

    def activateInactivityTimer(self):
        if self.inactivityTimer and self.inactivityTimer.active():
            self.inactivityTimer.reset(self.config().inactivityTimerSecs)
        elif self.config().inactivityTimerSecs:
            self.inactivityTimer = self.callLater(self.config().inactivityTimerSecs, self.inactivityTimerExpired)

    def cancelEnquireLinkTimer(self):
        if self.enquireLinkTimer and self.enquireLinkTimer.active():
            self.enquireLinkTimer.cancel()
            self.enquireLinkTimer = None

    def cancelInactivityTimer(self):
        if self.inactivityTimer and self.inactivityTimer.active():
            self.inactivityTimer.cancel()
            self.inactivityTimer = None

    def enquireLinkTimerExpired(self):
        txn = self.sendRequest(EnquireLink(), self.config().responseTimerSecs)
        txn.addErrback(self.enquireLinkErr)

    def enquireLinkErr(self, failure):
        # Unbinding already anyway. No need to raise another error
        failure.trap(SMPPError)

    def inactivityTimerExpired(self):
        self.log.critical("Inactivity timer expired...shutting down")
        self.shutdown()

    def isBound(self):
        return self.sessionState in (
        SMPPSessionStates.BOUND_TX, SMPPSessionStates.BOUND_RX, SMPPSessionStates.BOUND_TRX)

    def shutdown(self):
        """ Unbind if appropriate and disconnect """

        if self.isBound() and not self.connectionCorrupted:
            self.log.warning("Shutdown requested...unbinding")
            self.unbind().addBoth(lambda result: self.disconnect())
        elif self.sessionState not in (SMPPSessionStates.UNBIND_RECEIVED, SMPPSessionStates.UNBIND_PENDING):
            self.log.warning("Shutdown requested...disconnecting")
            self.disconnect()
        else:
            self.log.debug("Shutdown already in progress")

    def startInboundTransaction(self, reqPDU):
        if reqPDU.seqNum in self.inTxns:
            raise SMPPProtocolError('Duplicate message id [%s] received.  Already in progess.' % reqPDU.seqNum,
                                    CommandStatus.ESME_RUNKNOWNERR)
        txnDeferred = defer.Deferred()
        self.inTxns[reqPDU.seqNum] = txnDeferred
        self.log.debug("Inbound transaction started with message id %s" % reqPDU.seqNum)
        return txnDeferred

    def endInboundTransaction(self, reqPDU):
        if not reqPDU.seqNum in self.inTxns:
            raise ValueError('Unknown inbound sequence number in transaction for request PDU %s' % reqPDU)

        self.log.debug("Inbound transaction finished with message id %s" % reqPDU.seqNum)
        self.inTxns[reqPDU.seqNum].callback(reqPDU)
        del self.inTxns[reqPDU.seqNum]

    def startOutboundTransaction(self, reqPDU, timeout):
        if reqPDU.seqNum in self.outTxns:
            raise ValueError('Seq number [%s] is already in progess.' % reqPDU.seqNum)

        # Create callback deferred
        ackDeferred = defer.Deferred()
        # Create response timer
        timer = self.callLater(timeout, self.onResponseTimeout, reqPDU, timeout)
        # Save transaction
        self.outTxns[reqPDU.seqNum] = SMPPOutboundTxn(reqPDU, timer, ackDeferred)
        self.log.debug("Outbound transaction started with message id %s" % reqPDU.seqNum)
        return ackDeferred

    def closeOutboundTransaction(self, seqNum):
        self.log.debug("Outbound transaction finished with message id %s" % seqNum)

        if seqNum in self.outTxns:
            txn = self.outTxns[seqNum]
            # Remove txn
            del self.outTxns[seqNum]
            # Cancel response timer
            if txn.timer.active():
                txn.timer.cancel()
            return txn
        else:
            self.log.critical('Cannot close outbound transaction: trx id [%s] not found !', seqNum)
            return None

    def endOutboundTransaction(self, respPDU):
        txn = self.closeOutboundTransaction(respPDU.seqNum)

        if txn is not None:
            if respPDU.status == CommandStatus.ESME_ROK:
                if not isinstance(respPDU, txn.request.requireAck):
                    txn.ackDeferred.errback(SMPPProtocolError(respPDU,
                        "Invalid PDU response type [%s] returned for request type [%s]" % (
                        type(respPDU), type(txn.request))))
                    return
                # Do callback
                txn.ackDeferred.callback(SMPPOutboundTxnResult(self, txn.request, respPDU))
                return

            if isinstance(respPDU, GenericNack):
                txn.ackDeferred.errback(SMPPGenericNackTransactionError(respPDU, txn.request))
                return

            errCode = respPDU.status
            txn.ackDeferred.errback(SMPPTransactionError(respPDU, txn.request))

    def endOutboundTransactionErr(self, reqPDU, error):
        self.log.error(error)
        txn = self.closeOutboundTransaction(reqPDU.seqNum)

        if txn is not None:
            # Do errback
            txn.ackDeferred.errback(error)

    def cancelOutboundTransactions(self, error):
        for txn in list(self.outTxns.values()):
            self.endOutboundTransactionErr(txn.request, error)

    def onResponseTimeout(self, reqPDU, timeout):
        errMsg = 'Request timed out after %s secs: %s' % (timeout, reqPDU)
        self.endOutboundTransactionErr(reqPDU, SMPPRequestTimoutError(errMsg))
        self.shutdown()

    def claimSeqNum(self):
        self.lastSeqNum += 1
        return self.lastSeqNum

    def unbindSucceeded(self, result):
        self.sessionState = SMPPSessionStates.UNBOUND
        self.log.warning("Unbind succeeded")
        return result

    def unbindFailed(self, reason):
        self.log.error("Unbind failed [%s]. Disconnecting..." % reason)
        self.disconnect()
        if reason.check(SMPPRequestTimoutError):
            raise SMPPSessionInitTimoutError(str(reason))
        return reason

    def unbindAfterInProgressTxnsFinished(self, result, unbindDeferred):
        self.log.warning('Issuing unbind request')
        self.sendBindRequest(Unbind()).addCallbacks(self.unbindSucceeded, self.unbindFailed).chainDeferred(
            unbindDeferred)

    ############################################################################
    # Public command functions
    ############################################################################
    def unbind(self):
        """Unbind from SMSC

        Result is a Deferred object
        """
        if not self.isBound():
            return defer.fail(
                SMPPClientSessionStateError('unbind called with illegal session state: %s' % self.sessionState))

        self.cancelEnquireLinkTimer()

        self.log.info('Waiting for in-progress transactions to finish...')

        # Signal that
        #   - no new data requests should be sent
        #   - no new incoming data requests should be accepted
        self.sessionState = SMPPSessionStates.UNBIND_PENDING

        unbindDeferred = defer.Deferred()
        # Wait for any in-progress txns to finish
        self.finishTxns().addCallback(self.unbindAfterInProgressTxnsFinished, unbindDeferred)
        # Result is the deferred for the unbind txn
        return unbindDeferred

    def unbindAndDisconnect(self):
        """Unbind from SMSC and disconnect

        Result is a Deferred object
        """
        return self.unbind().addBoth(lambda result: self.disconnect())

    def disconnect(self):
        """Disconnect from SMSC
        """
        if self.isBound():
            self.log.warning("Disconnecting while bound to SMSC...")
        else:
            self.log.warning("Disconnecting...")
        self.sessionState = SMPPSessionStates.UNBOUND
        self.transport.loseConnection()

    def getDisconnectedDeferred(self):
        """Get a Deferred so you can be notified on disconnect
        """
        return self.disconnectedDeferred

    def sendDataRequest(self, pdu):
        """Send a SMPP Request Message

        Argument is an SMPP PDUDataRequest (protocol data unit).
        Result is a Deferred object
        """
        if not isinstance(pdu, PDUDataRequest):
            return defer.fail(SMPPClientError("Invalid PDU passed to sendDataRequest(): %s" % pdu))
        if not self.isBound():
            return defer.fail(SMPPClientSessionStateError('Not bound'))
        return self.sendRequest(pdu, self.config().responseTimerSecs)