def __init__(self, SMPPClientSMListenerConfig, SMPPClientFactory, amqpBroker, redisClient): self.config = SMPPClientSMListenerConfig self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory( self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig( self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = logging.FileHandler(filename=self.config.log_file) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False
def setUp(self): LongSubmitSmWithSARTestCase.setUp(self) # Reset opFactory with long_content_max_parts = 8 self.opFactory = SMPPOperationFactory(self.config, long_content_max_parts=8) self.long_content_max_parts = self.opFactory.long_content_max_parts
def __init__(self, config, SMPPClientFactory, amqpBroker, redisClient, RouterPB=None, interceptorpb_client=None): self.config = config self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory(self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.RouterPB = RouterPB self.interceptorpb_client = interceptorpb_client self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.submit_retrials = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig(self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = TimedRotatingFileHandler(filename=self.config.log_file, when=self.config.log_rotate) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False
def setUp(self): self.opFactory = SMPPOperationFactory(SMPPClientConfig(id='test-id')) self.user = User(1, Group(100), 'username', 'password') self.pdu = self.opFactory.SubmitSM( source_addr=b'2', destination_addr=b'3', short_message=b'sunny day !', )
def send_long_submit_sm(self, long_content_split): """Reference to #27: When sending a long SMS, logger must write concatenated content """ lc = LogCapture("jasmin-sm-listener") yield self.connect('127.0.0.1', self.pbPort) yield self.add(self.defaultConfig) yield self.start(self.defaultConfig.id) # Wait for 'BOUND_TRX' state yield waitFor(2) # Build a long submit_sm assertionKey = str(randint(10, 99)) * 100 + 'EOF' # 203 chars config = SMPPClientConfig(id='defaultId') opFactory = SMPPOperationFactory(config, long_content_split = long_content_split) SubmitSmPDU = opFactory.SubmitSM( source_addr='1423', destination_addr='98700177', short_message=assertionKey, ) # Send submit_sm yield self.submit_sm(self.defaultConfig.id, SubmitSmPDU) # Wait 2 seconds yield waitFor(2) yield self.stop(self.defaultConfig.id) # Wait for unbound state yield waitFor(2) # Assertions # Take the lastClient (and unique one) and assert received messages self.assertEqual(len(self.SMSCPort.factory.lastClient.submitRecords), 2) if long_content_split == 'udh': concatenatedShortMessage = self.SMSCPort.factory.lastClient.submitRecords[0].params['short_message'][6:] concatenatedShortMessage+= self.SMSCPort.factory.lastClient.submitRecords[1].params['short_message'][6:] else: concatenatedShortMessage = self.SMSCPort.factory.lastClient.submitRecords[0].params['short_message'] concatenatedShortMessage+= self.SMSCPort.factory.lastClient.submitRecords[1].params['short_message'] self.assertEqual(concatenatedShortMessage, assertionKey) # Logged concatenated message loggedSms = False for record in lc.records: if record.getMessage()[:6] == 'SMS-MT': loggedSms = True # Will raise ValueError if concatenatedShortMessage is not logged record.getMessage().index('[content:%s]' % concatenatedShortMessage) break # This will assert if we had a SMS-MT logged self.assertTrue(loggedSms) # There were a connection to the SMSC self.assertTrue(self.SMSCPort.factory.buildProtocol.called) self.assertEqual(self.SMSCPort.factory.buildProtocol.call_count, 1)
def __init__(self, config): self.log_category = "jasmin-dlr-thrower" self.exchangeName = 'messaging' self.consumerTag = 'DLRThrower' self.routingKey = 'dlr_thrower.*' self.queueName = 'dlr_thrower' self.callback = self.dlr_throwing_callback self.opFactory = SMPPOperationFactory() Thrower.__init__(self, config)
def __init__(self, HTTPApiConfig, RouterPB, stats, log): Resource.__init__(self) self.RouterPB = RouterPB self.log = log self.stats = stats # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory(long_content_max_parts = HTTPApiConfig.long_content_max_parts, long_content_split = HTTPApiConfig.long_content_split)
def setUp(self): yield SMSCSimulatorRecorder.setUp(self) self.SMSCPort.factory.buildProtocol = mock.Mock(wraps=self.SMSCPort.factory.buildProtocol) config = SMPPClientConfig(id='defaultId') opFactory = SMPPOperationFactory(config) self.SubmitSmPDU = opFactory.SubmitSM( source_addr='1423', destination_addr='98700177', short_message='Hello world !', )
def setUp(self): yield SMSCSimulatorRecorder.setUp(self) self.SMSCPort.factory.buildProtocol = mock.Mock(wraps=self.SMSCPort.factory.buildProtocol) config = SMPPClientConfig(id='defaultId') opFactory = SMPPOperationFactory(config) self.SubmitSmPDU = opFactory.SubmitSM( source_addr='1423', destination_addr='06155423', short_message='Hello world !', ) self.SubmitSmBill = SubmitSmBill(User('test_user', Group('test_group'), 'test_username', 'pwd'))
def setUp(self): LongSubmitSmTestCase.setUp(self) # Reset opFactory with long_content_split = 'udh' self.opFactory = SMPPOperationFactory(self.config, long_content_split='udh') self.long_content_max_parts = self.opFactory.long_content_max_parts
def __init__(self, config, SMPPClientFactory, amqpBroker, redisClient, RouterPB=None, interceptorpb_client=None): self.config = config self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory(self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.RouterPB = RouterPB self.interceptorpb_client = interceptorpb_client self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.submit_retrials = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig(self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = TimedRotatingFileHandler(filename=self.config.log_file, when=self.config.log_rotate) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False
def __init__(self, config): self.log_category = "jasmin-dlr-thrower" self.exchangeName = 'messaging' self.consumerTag = 'DLRThrower' self.routingKey = 'dlr_thrower.*' self.queueName = 'dlr_thrower' self.callback = self.dlr_throwing_callback self.opFactory = SMPPOperationFactory() Thrower.__init__(self, config)
def setUp(self): self.factory = Factory() self.factory.protocol = self.protocol self.port = reactor.listenTCP(9001, self.factory) self.testPort = self.port.getHost().port args = self.configArgs.copy() args['host'] = self.configArgs.get('host', 'localhost') args['port'] = self.configArgs.get('port', self.testPort) args['username'] = self.configArgs.get('username', 'anyusername') args['password'] = self.configArgs.get('password', '') args['log_level'] = self.configArgs.get('log_level', logging.DEBUG) self.config = SMPPClientConfig(**args) self.opFactory = SMPPOperationFactory(self.config)
def setUp(self): self.factory = Factory() self.factory.protocol = self.protocol args = self.configArgs.copy() args['host'] = self.configArgs.get('host', 'localhost') args['port'] = self.configArgs.get('port', '2775') args['username'] = self.configArgs.get('username', 'anyusername') args['password'] = self.configArgs.get('password', '') args['log_level'] = self.configArgs.get('log_level', logging.DEBUG) self.config = SMPPClientConfig(**args) self.opFactory = SMPPOperationFactory(self.config) # Start listening 5 seconds later, the client shall successfully reconnect reactor.callLater(5, self.startListening, self.config.port)
def __init__(self, SMPPClientSMListenerConfig, SMPPClientFactory, amqpBroker, redisClient): self.config = SMPPClientSMListenerConfig self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory(self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig(self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = logging.FileHandler(filename=self.config.log_file) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler)
class VeryLongSubmitSmUsingSARTestCase(LongSubmitSmWithSARTestCase): configArgs = { 'id': 'test-id', 'sessionInitTimerSecs': 0.1, 'reconnectOnConnectionFailure': False, 'reconnectOnConnectionLoss': False, 'username': '******', } def setUp(self): LongSubmitSmWithSARTestCase.setUp(self) # Reset opFactory with long_content_max_parts = 8 self.opFactory = SMPPOperationFactory(self.config, long_content_max_parts=8) self.long_content_max_parts = self.opFactory.long_content_max_parts @defer.inlineCallbacks def test_long_submit_sm_7bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm content = self.composeMessage(GSM0338, 1224) # 1224 = 153 * 8 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=0, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.runAsserts(smpp, content, len(content) / 153) @defer.inlineCallbacks def test_very_long_submit_sm_7bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm content = self.composeMessage(GSM0338, 1530) # 1530 = 153 * 10 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=0, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.assertEquals(self.long_content_max_parts + 1, smpp.PDUReceived.call_count) self.assertEquals(self.long_content_max_parts + 1, smpp.sendPDU.call_count) @defer.inlineCallbacks def test_long_submit_sm_8bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm content = self.composeMessage(ISO8859_1, 1072) # 1072 = 134 * 8 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=3, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.runAsserts(smpp, content, len(content) / 134) @defer.inlineCallbacks def test_very_long_submit_sm_8bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm content = self.composeMessage(ISO8859_1, 1340) # 1340 = 134 * 10 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=3, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.assertEquals(self.long_content_max_parts + 1, smpp.PDUReceived.call_count) self.assertEquals(self.long_content_max_parts + 1, smpp.sendPDU.call_count) @defer.inlineCallbacks def test_long_submit_sm_16bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm UCS2 = {'\x0623', '\x0631', '\x0646', '\x0628'} content = self.composeMessage(UCS2, 536) # 536 = 67 * 8 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=8, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.runAsserts(smpp, content, len(content) / 67) @defer.inlineCallbacks def test_very_long_submit_sm_16bit(self): client = SMPPClientFactory(self.config) # Connect and bind yield client.connectAndBind() smpp = client.smpp self.prepareMocks(smpp) # Send submit_sm UCS2 = {'\x0623', '\x0631', '\x0646', '\x0628'} content = self.composeMessage(UCS2, 3350) # 3350 = 67 * 10 SubmitSmPDU = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=content, data_coding=8, ) yield smpp.sendDataRequest(SubmitSmPDU) # Unbind & Disconnect yield smpp.unbindAndDisconnect() ############## # Assertions : self.assertEquals(self.long_content_max_parts + 1, smpp.PDUReceived.call_count) self.assertEquals(self.long_content_max_parts + 1, smpp.sendPDU.call_count)
class SMPPClientSMListener: debug_it = {'rejectCount': 0} ''' This is a listener object instanciated for every new SMPP connection, it is responsible of handling SubmitSm, DeliverSm and SubmitSm PDUs for a given SMPP connection ''' def __init__(self, SMPPClientSMListenerConfig, SMPPClientFactory, amqpBroker, redisClient): self.config = SMPPClientSMListenerConfig self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory(self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig(self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = logging.FileHandler(filename=self.config.log_file) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False def setSubmitSmQ(self, queue): self.log.debug('Setting a new submit_sm_q: %s' % queue) self.submit_sm_q = queue def clearRejectTimer(self, msgid): if msgid in self.rejectTimers: t = self.rejectTimers[msgid] if t.active(): t.cancel() del self.rejectTimers[msgid] def clearRejectTimers(self): for msgid, timer in self.rejectTimers.items(): if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearQosTimer(self): if self.qosTimer is not None and self.qosTimer.called == False: self.qosTimer.cancel() self.qosTimer = None def clearAllTimers(self): self.clearQosTimer() self.clearRejectTimers() @defer.inlineCallbacks def rejectAndRequeueMessage(self, message, delay = True): msgid = message.content.properties['message-id'] if delay != False: self.log.debug("Requeuing SubmitSmPDU[%s] in %s seconds" % (msgid, self.SMPPClientFactory.config.requeue_delay)) # Use configured requeue_delay or specific one if delay is not bool: requeue_delay = delay else: requeue_delay = self.SMPPClientFactory.config.requeue_delay # Requeue the message with a delay t = reactor.callLater(requeue_delay, self.rejectMessage, message = message, requeue = 1) # If any, clear timer before setting a new one self.clearRejectTimer(msgid) self.rejectTimers[msgid] = t defer.returnValue(t) else: self.log.debug("Requeuing SubmitSmPDU[%s] without delay" % msgid) yield self.rejectMessage(message, requeue = 1) @defer.inlineCallbacks def rejectMessage(self, message, requeue = 0): yield self.amqpBroker.chan.basic_reject(delivery_tag=message.delivery_tag, requeue = requeue) @defer.inlineCallbacks def ackMessage(self, message): yield self.amqpBroker.chan.basic_ack(message.delivery_tag) @defer.inlineCallbacks def setKeyExpiry(self, callbackArg, key, expiry): yield self.redisClient.expire(key, expiry) @defer.inlineCallbacks def submit_sm_callback(self, message): """This callback is a queue listener it is called whenever a message was consumed from queue c.f. test_amqp.ConsumeTestCase for use cases """ msgid = message.content.properties['message-id'] SubmitSmPDU = pickle.loads(message.content.body) self.submit_sm_q.get().addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) self.log.debug("Callbacked a submit_sm with a SubmitSmPDU[%s] (?): %s" % (msgid, SubmitSmPDU)) if self.qos_last_submit_sm_at is None: self.qos_last_submit_sm_at = datetime(1970, 1, 1) if self.SMPPClientFactory.config.submit_sm_throughput > 0: # QoS throttling qos_throughput_second = 1 / float(self.SMPPClientFactory.config.submit_sm_throughput) qos_throughput_ysecond_td = timedelta( microseconds = qos_throughput_second * 1000000) qos_delay = datetime.now() - self.qos_last_submit_sm_at if qos_delay < qos_throughput_ysecond_td: qos_slow_down = float((qos_throughput_ysecond_td - qos_delay).microseconds) / 1000000 # We're faster than submit_sm_throughput, slow down before taking a new message from the queue self.log.debug("QoS: submit_sm_callback is faster (%s) than fixed throughput (%s), slowing down by %s seconds (message will be requeued)." % ( qos_delay, qos_throughput_ysecond_td, qos_slow_down )) # Relaunch queue callbacking after qos_slow_down seconds #self.qosTimer = task.deferLater(reactor, qos_slow_down, self.submit_sm_q.get) #self.qosTimer.addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) # Requeue the message yield self.rejectAndRequeueMessage(message, delay = qos_slow_down) defer.returnValue(False) self.qos_last_submit_sm_at = datetime.now() # Verify if message is a SubmitSm PDU if isinstance(SubmitSmPDU, SubmitSM) == False: self.log.error("Received an object[%s] which is not an instance of SubmitSm: discarding this unkown object from the queue" % msgid) yield self.rejectMessage(message) defer.returnValue(False) # If the message has expired in the queue if 'headers' in message.content.properties and 'expiration' in message.content.properties['headers']: expiration_datetime = parser.parse(message.content.properties['headers']['expiration']) if expiration_datetime < datetime.now(): self.log.info("Discarding expired message[%s]: expiration is %s" % (msgid, expiration_datetime)) yield self.rejectMessage(message) defer.returnValue(False) # SMPP Client should be already connected if self.SMPPClientFactory.smpp == None: self.log.error("SMPP Client is not connected: requeuing SubmitSmPDU[%s]" % msgid) yield self.rejectAndRequeueMessage(message) defer.returnValue(False) # SMPP Client should be already bound as transceiver or transmitter if self.SMPPClientFactory.smpp.isBound() == False: self.log.error("SMPP Client is not bound: Requeuing SubmitSmPDU[%s]" % msgid) yield self.rejectAndRequeueMessage(message) defer.returnValue(False) self.log.debug("Sending SubmitSmPDU through SMPPClientFactory") yield self.SMPPClientFactory.smpp.sendDataRequest( SubmitSmPDU ).addCallback(self.submit_sm_resp_event, message) @defer.inlineCallbacks def submit_sm_resp_event(self, r, amqpMessage): msgid = amqpMessage.content.properties['message-id'] total_bill_amount = None if ('headers' not in amqpMessage.content.properties or 'submit_sm_resp_bill' not in amqpMessage.content.properties['headers']): submit_sm_resp_bill = None else: submit_sm_resp_bill = pickle.loads(amqpMessage.content.properties['headers']['submit_sm_resp_bill']) if r.response.status == CommandStatus.ESME_ROK: # Get bill information total_bill_amount = 0.0 if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts() > 0: total_bill_amount = submit_sm_resp_bill.getTotalAmounts() # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(r.request.params['esm_class'], 'gsmFeatures'): for gsmFeature in r.request.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break # What type of splitting ? splitMethod = None if 'sar_msg_ref_num' in r.request.params: splitMethod = 'sar' elif UDHI_INDICATOR_SET and r.request.params['short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' # Concatenate short_message if splitMethod is not None: _pdu = r.request if splitMethod == 'sar': short_message = _pdu.params['short_message'] else: short_message = _pdu.params['short_message'][6:] while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu if splitMethod == 'sar': short_message += _pdu.params['short_message'] else: short_message += _pdu.params['short_message'][6:] # Increase bill amount for each submit_sm_resp if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts() > 0: total_bill_amount+= submit_sm_resp_bill.getTotalAmounts() else: short_message = r.request.params['short_message'] self.log.info("SMS-MT [cid:%s] [queue-msgid:%s] [smpp-msgid:%s] [status:%s] [prio:%s] [dlr:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % ( self.SMPPClientFactory.config.id, msgid, r.response.params['message_id'], r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], short_message )) else: self.log.info("SMS-MT [cid:%s] [queue-msgid:%s] [status:ERROR/%s] [prio:%s] [dlr:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % ( self.SMPPClientFactory.config.id, msgid, r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], r.request.params['short_message'] )) # Cancel any mapped rejectTimer to this message (in case this message was rejected in the past) self.clearRejectTimer(msgid) self.log.debug("ACKing amqpMessage [%s] having routing_key [%s]", msgid, amqpMessage.routing_key) # ACK the message in queue, this will remove it from the queue yield self.ackMessage(amqpMessage) # Redis client is connected ? if self.redisClient is not None: # Check for HTTP DLR request from redis 'dlr' key # If there's a pending delivery receipt request then serve it # back by publishing a DLRContentForHttpapi to the messaging exchange pickledDlr = None pickledSmppsMap = None pickledDlr = yield self.redisClient.get("dlr:%s" % msgid) if pickledDlr is None: pickledSmppsMap = yield self.redisClient.get("smppsmap:%s" % msgid) if pickledDlr is not None: self.log.debug('There is a HTTP DLR request for msgid[%s] ...' % (msgid)) dlr = pickle.loads(pickledDlr) dlr_url = dlr['url'] dlr_level = dlr['level'] dlr_method = dlr['method'] dlr_expiry = dlr['expiry'] if dlr_level in [1, 3]: self.log.debug('Got DLR information for msgid[%s], url:%s, level:%s' % (msgid, dlr_url, dlr_level)) content = DLRContentForHttpapi(str(r.response.status), msgid, dlr_url, # The dlr_url in DLRContentForHttpapi indicates the level # of the actual delivery receipt (1) and not the requested # one (maybe 1 or 3) dlr_level = 1, method = dlr_method) routing_key = 'dlr_thrower.http' self.log.debug("Publishing DLRContentForHttpapi[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # DLR request is removed if: # - If level 1 is requested (SMSC level only) # - SubmitSmResp returned an error (no more delivery will be tracked) # # When level 3 is requested, the DLR will be removed when # receiving a deliver_sm (terminal receipt) if dlr_level == 1 or r.response.status != CommandStatus.ESME_ROK: self.log.debug('Removing DLR request for msgid[%s]' % msgid) yield self.redisClient.delete("dlr:%s" % msgid) else: self.log.debug('Terminal level receipt is requested, will not send any DLR receipt at this level.') if dlr_level in [2, 3]: # Map received submit_sm_resp's message_id to the msg for later rceipt handling self.log.debug('Mapping smpp msgid: %s to queue msgid: %s, expiring in %s' % ( r.response.params['message_id'], msgid, dlr_expiry ) ) hashKey = "queue-msgid:%s" % r.response.params['message_id'] hashValues = {'msgid': msgid, 'connector_type': 'httpapi',} self.redisClient.set(hashKey, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.setKeyExpiry, hashKey, dlr_expiry) elif pickledSmppsMap is not None: self.log.debug('There is a SMPPs mapping for msgid[%s] ...' % (msgid)) smpps_map = pickle.loads(pickledSmppsMap) system_id = smpps_map['system_id'] source_addr = smpps_map['source_addr'] destination_addr = smpps_map['destination_addr'] registered_delivery = smpps_map['registered_delivery'] smpps_map_expiry = smpps_map['expiry'] # Do we need to forward the receipt to the original sender ? if ((r.response.status == CommandStatus.ESME_ROK and str(registered_delivery.receipt) in ['SMSC_DELIVERY_RECEIPT_REQUESTED', 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE']) or (r.response.status != CommandStatus.ESME_ROK and str(registered_delivery.receipt) == 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE')): self.log.debug('Got DLR information for msgid[%s], registered_deliver%s, system_id:%s' % (msgid, registered_delivery, system_id)) content = DLRContentForSmpps(str(r.response.status), msgid, system_id, source_addr, destination_addr) routing_key = 'dlr_thrower.smpps' self.log.debug("Publishing DLRContentForSmpps[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # Map received submit_sm_resp's message_id to the msg for later rceipt handling self.log.debug('Mapping smpp msgid: %s to queue msgid: %s, expiring in %s' % ( r.response.params['message_id'], msgid, smpps_map_expiry ) ) hashKey = "queue-msgid:%s" % r.response.params['message_id'] hashValues = {'msgid': msgid, 'connector_type': 'smpps',} self.redisClient.set(hashKey, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.setKeyExpiry, hashKey, smpps_map_expiry) else: self.log.warn('No valid RC were found while checking msg[%s] !' % msgid) # Bill will be charged by bill_request.submit_sm_resp.UID queue consumer if total_bill_amount > 0: pubQueueName = 'bill_request.submit_sm_resp.%s' % submit_sm_resp_bill.user.uid content = SubmitSmRespBillContent(submit_sm_resp_bill.bid, submit_sm_resp_bill.user.uid, total_bill_amount) self.log.debug("Requesting a SubmitSmRespBillContent from a bill [bid:%s] with routing_key[%s]: %s" % (submit_sm_resp_bill.bid, pubQueueName, total_bill_amount)) yield self.amqpBroker.publish(exchange='billing', routing_key=pubQueueName, content=content) if self.config.publish_submit_sm_resp: # Send back submit_sm_resp to submit.sm.resp.CID queue # There's no actual listeners on this queue, it can be used to # track submit_sm_resp messages from a 3rd party app content = SubmitSmRespContent(r.response, msgid, pickleProtocol = self.pickleProtocol) self.log.debug("Sending back SubmitSmRespContent[%s] with routing_key[%s]" % (msgid, amqpMessage.content.properties['reply-to'])) yield self.amqpBroker.publish(exchange='messaging', routing_key=amqpMessage.content.properties['reply-to'], content=content) def submit_sm_errback(self, error): """It appears that when closing a queue with the close() method it errbacks with a txamqp.queue.Closed exception, didnt find a clean way to stop consuming a queue without errbacking here so this is a workaround to make it clean, it can be considered as a @TODO requiring knowledge of the queue api behaviour """ if error.check(Closed) == None: #@todo: implement this errback # For info, this errback is called whenever: # - an error has occured inside submit_sm_callback # - the qosTimer has been cancelled (self.clearQosTimer()) self.log.error("Error in submit_sm_errback: %s" % error.getErrorMessage()) @defer.inlineCallbacks def concatDeliverSMs(self, HSetReturn, splitMethod, total_segments, msg_ref_num, segment_seqnum): hashKey = "longDeliverSm:%s" % (msg_ref_num) if HSetReturn != 1: self.log.warn('Error (%s) when trying to set hashKey %s' % (HSetReturn, hashKey)) return # @TODO: longDeliverSm part expiry must be configurable yield self.redisClient.expire(hashKey, 300) # This is the last part if segment_seqnum == total_segments: hvals = yield self.redisClient.hvals(hashKey) if len(hvals) != total_segments: self.log.warn('Received the last part (msg_ref_num:%s) and did not find all parts in redis, data lost !' % msg_ref_num) return # Get PDUs pdus = {} for pickledValue in hvals: value = pickle.loads(pickledValue) pdus[value['segment_seqnum']] = value['pdu'] # Build short_message short_message = '' for i in range(total_segments): if splitMethod == 'sar': short_message += pdus[i+1].params['short_message'] else: short_message += pdus[i+1].params['short_message'][6:] # Build the final pdu and return it back to deliver_sm_event pdu = pdus[1] # Take the first part as a base of work # 1. Remove message splitting information from pdu if splitMethod == 'sar': del(pdu.params['sar_segment_seqnum']) del(pdu.params['sar_total_segments']) del(pdu.params['sar_msg_ref_num']) else: pdu.params['esm_class'] = None # 2. Set the new short_message pdu.params['short_message'] = short_message yield self.deliver_sm_event(smpp = None, pdu = pdu, concatenated = True) @defer.inlineCallbacks def deliver_sm_event(self, smpp, pdu, concatenated = False): """This event is called whenever a deliver_sm pdu is received through a SMPPc It will hand the pdu to the router or a dlr thrower (depending if its a DLR or not). """ pdu.dlr = self.SMPPOperationFactory.isDeliveryReceipt(pdu) content = DeliverSmContent(pdu, self.SMPPClientFactory.config.id, pickleProtocol = self.pickleProtocol, concatenated = concatenated) msgid = content.properties['message-id'] if pdu.dlr is None: # We have a SMS-MO # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(pdu.params['esm_class'], 'gsmFeatures'): for gsmFeature in pdu.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break splitMethod = None # Is it a part of a long message ? if 'sar_msg_ref_num' in pdu.params: splitMethod = 'sar' total_segments = pdu.params['sar_total_segments'] segment_seqnum = pdu.params['sar_segment_seqnum'] msg_ref_num = pdu.params['sar_msg_ref_num'] self.log.debug('Received a part of SMS-MO [queue-msgid:%s] using SAR options: total_segments=%s, segmen_seqnum=%s, msg_ref_num=%s' % (msgid, total_segments, segment_seqnum, msg_ref_num)) elif UDHI_INDICATOR_SET and pdu.params['short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' total_segments = struct.unpack('!B', pdu.params['short_message'][4])[0] segment_seqnum = struct.unpack('!B', pdu.params['short_message'][5])[0] msg_ref_num = struct.unpack('!B', pdu.params['short_message'][3])[0] self.log.debug('Received a part of SMS-MO [queue-msgid:%s] using UDH options: total_segments=%s, segmen_seqnum=%s, msg_ref_num=%s' % (msgid, total_segments, segment_seqnum, msg_ref_num)) if splitMethod is None: # It's a simple short message or a part of a concatenated message routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug("Publishing DeliverSmContent[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) self.log.info("SMS-MO [cid:%s] [queue-msgid:%s] [status:%s] [prio:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % ( self.SMPPClientFactory.config.id, msgid, pdu.status, pdu.params['priority_flag'], pdu.params['validity_period'], pdu.params['source_addr'], pdu.params['destination_addr'], pdu.params['short_message'] )) else: # Long message part received if self.redisClient is None: self.warn('No valid RC were found while receiving a part of a long DeliverSm [queue-msgid:%s], MESSAGE IS LOST !' % msgid) # Save it to redis hashKey = "longDeliverSm:%s" % (msg_ref_num) hashValues = {'pdu': pdu, 'total_segments':total_segments, 'msg_ref_num':msg_ref_num, 'segment_seqnum':segment_seqnum} self.redisClient.hset(hashKey, segment_seqnum, pickle.dumps(hashValues, self.pickleProtocol ) ).addCallback(self.concatDeliverSMs, splitMethod, total_segments, msg_ref_num, segment_seqnum) self.log.info("DeliverSmContent[%s] is a part of a long message of %s parts, will be sent to queue after concatenation." % (msgid, total_segments)) # Flag it as "will_be_concatenated" and publish it to router routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug("Publishing DeliverSmContent[%s](flagged:wbc) with routing_key[%s]" % (msgid, routing_key)) content.properties['headers']['will_be_concatenated'] = True yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) else: # This is a DLR ! # Check for DLR request if self.redisClient is not None: q = yield self.redisClient.get("queue-msgid:%s" % pdu.dlr['id']) submit_sm_queue_id = None connector_type = None if q is not None: q = pickle.loads(q) submit_sm_queue_id = q['msgid'] connector_type = q['connector_type'] if submit_sm_queue_id is not None and connector_type == 'httpapi': pickledDlr = yield self.redisClient.get("dlr:%s" % submit_sm_queue_id) if pickledDlr is not None: dlr = pickle.loads(pickledDlr) dlr_url = dlr['url'] dlr_level = dlr['level'] dlr_method = dlr['method'] if dlr_level in [2, 3]: self.log.debug('Got DLR information for msgid[%s], url:%s, level:%s' % (submit_sm_queue_id, dlr_url, dlr_level)) content = DLRContentForHttpapi(pdu.dlr['stat'], submit_sm_queue_id, dlr_url, # The dlr_url in DLRContentForHttpapi indicates the level # of the actual delivery receipt (2) and not the # requested one (maybe 2 or 3) dlr_level = 2, id_smsc = pdu.dlr['id'], sub = pdu.dlr['sub'], dlvrd = pdu.dlr['dlvrd'], subdate = pdu.dlr['sdate'], donedate = pdu.dlr['ddate'], err = pdu.dlr['err'], text = pdu.dlr['text'], method = dlr_method) routing_key = 'dlr_thrower.http' self.log.debug("Publishing DLRContentForHttpapi[%s] with routing_key[%s]" % (submit_sm_queue_id, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) self.log.debug('Removing DLR request for msgid[%s]' % submit_sm_queue_id) yield self.redisClient.delete('dlr:%s' % submit_sm_queue_id) else: self.log.debug('SMS-C receipt is requested, will not send any DLR receipt at this level.') else: self.log.warn('Got invalid DLR information for msgid[%s], url:%s, level:%s' % (submit_sm_queue_id, dlr_url, dlr_level)) elif submit_sm_queue_id is not None and connector_type == 'smpps': pickledSmppsMap = yield self.redisClient.get("smppsmap:%s" % submit_sm_queue_id) if pickledSmppsMap is not None: smpps_map = pickle.loads(pickledSmppsMap) system_id = smpps_map['system_id'] source_addr = smpps_map['source_addr'] destination_addr = smpps_map['destination_addr'] registered_delivery = smpps_map['registered_delivery'] smpps_map_expiry = smpps_map['expiry'] success_states = ['ACCEPTD', 'DELIVRD'] final_states = ['DELIVRD', 'EXPIRED', 'DELETED', 'UNDELIV', 'REJECTD'] # Do we need to forward the receipt to the original sender ? if ((pdu.dlr['stat'] in success_states and str(registered_delivery.receipt) in ['SMSC_DELIVERY_RECEIPT_REQUESTED', 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE']) or (pdu.dlr['stat'] not in success_states and str(registered_delivery.receipt) == 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE')): self.log.debug('Got DLR information for msgid[%s], registered_deliver%s, system_id:%s' % (submit_sm_queue_id, registered_delivery, system_id)) content = DLRContentForSmpps(pdu.dlr['stat'], submit_sm_queue_id, system_id, source_addr, destination_addr) routing_key = 'dlr_thrower.smpps' self.log.debug("Publishing DLRContentForSmpps[%s] with routing_key[%s]" % (submit_sm_queue_id, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) if pdu.dlr['stat'] in final_states: self.log.debug('Removing SMPPs map for msgid[%s]' % submit_sm_queue_id) yield self.redisClient.delete('smppsmap:%s' % submit_sm_queue_id) else: self.log.warn('Got a DLR for an unknown message id: %s' % pdu.dlr['id']) else: self.log.warn('DLR for msgid[%s] is not checked, no valid RC were found' % msgid) self.log.info("DLR [cid:%s] [smpp-msgid:%s] [status:%s] [submit date:%s] [done date:%s] [submitted/delivered messages:%s/%s] [err:%s] [content:%s]" % ( self.SMPPClientFactory.config.id, pdu.dlr['id'], pdu.dlr['stat'], pdu.dlr['sdate'], pdu.dlr['ddate'], pdu.dlr['sub'], pdu.dlr['dlvrd'], pdu.dlr['err'], pdu.dlr['text'], ))
class Rate(Resource): isleaf = True def __init__(self, HTTPApiConfig, RouterPB, stats, log, interceptorpb_client): Resource.__init__(self) self.RouterPB = RouterPB self.stats = stats self.log = log self.interceptorpb_client = interceptorpb_client # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) @defer.inlineCallbacks def route_routable(self, request): try: # Authentication user = self.RouterPB.authenticateUser( username=request.args['username'][0], password=request.args['password'][0]) if user is None: self.stats.inc('auth_error_count') self.log.debug( "Authentication failure for username:%s and password:%s", request.args['username'][0], request.args['password'][0]) self.log.error("Authentication failure for username:%s", request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % request.args['username'][0]) # Update CnxStatus user.getCnxStatus().httpapi['connects_count'] += 1 user.getCnxStatus().httpapi['rate_request_count'] += 1 user.getCnxStatus().httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_add=None if 'from' not in request.args else request.args['from'][0], destination_addr=request.args['to'][0], short_message=request.args['content'][0], data_coding=int(request.args['coding'][0]), ) self.log.debug("Built base SubmitSmPDU: %s", SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Rate', user, request, submit_sm=SubmitSmPDU) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Prepare for interception than routing routable = RoutableSubmitSm(SubmitSmPDU, user) self.log.debug("Built Routable %s for SubmitSmPDU: %s", routable, SubmitSmPDU) # Should we tag the routable ? tags = [] if 'tags' in request.args: tags = request.args['tags'][0].split(',') for tag in tags: routable.addTag(tag) self.log.debug('Tagged routable %s: +%s', routable, tag) # Intercept interceptor = self.RouterPB.getMTInterceptionTable( ).getInterceptorFor(routable) if interceptor is not None: self.log.debug( "RouterPB selected %s interceptor for this SubmitSmPDU", interceptor) if self.interceptorpb_client is None: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError( 'InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! r = yield self.interceptorpb_client.run_script( script, routable) if isinstance(r, dict) and r['http_status'] != 200: self.stats.inc('interceptor_error_count') self.log.error( 'Interceptor script returned %s http_status error.', r['http_status']) raise InterceptorRunError( code=r['http_status'], message='Interception specific error code %s' % r['http_status']) elif isinstance(r, str): self.stats.inc('interceptor_count') routable = pickle.loads(r) else: self.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', r) raise InterceptorRunError( message= 'Failed running interception script, check log for details' ) # Routing route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.log.error( "No route matched from user %s for SubmitSmPDU: %s", user, SubmitSmPDU) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s for this SubmitSmPDU", route) # Get number of PDUs to be sent (for billing purpose) _pdu = SubmitSmPDU submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Get the bill bill = route.getBillFor(user) response = { 'return': { 'unit_rate': bill.getTotalAmounts(), 'submit_sm_count': submit_sm_count }, 'status': 200 } except Exception, e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally:
class DLRThrower(Thrower): name = 'DLRThrower' def __init__(self, config): self.log_category = "jasmin-dlr-thrower" self.exchangeName = 'messaging' self.consumerTag = 'DLRThrower' self.routingKey = 'dlr_thrower.*' self.queueName = 'dlr_thrower' self.callback = self.dlr_throwing_callback self.opFactory = SMPPOperationFactory() Thrower.__init__(self, config) @defer.inlineCallbacks def http_dlr_callback(self, message): msgid = message.content.properties['message-id'] url = message.content.properties['headers']['url'] method = message.content.properties['headers']['method'] level = message.content.properties['headers']['level'] self.log.debug('Got one message (msgid:%s) to throw', msgid) # If any, clear requeuing timer self.clearRequeueTimer(msgid) # Build mandatory arguments args = { 'id': msgid, 'level': level, 'message_status': message.content.properties['headers']['message_status'] } # Level 2 extra args if level in [2, 3]: args['id_smsc'] = message.content.properties['headers']['id_smsc'] args['sub'] = message.content.properties['headers']['sub'] args['dlvrd'] = message.content.properties['headers']['dlvrd'] args['subdate'] = message.content.properties['headers']['subdate'] args['donedate'] = message.content.properties['headers'][ 'donedate'] args['err'] = message.content.properties['headers']['err'] args['text'] = message.content.properties['headers']['text'] try: # Throw the message to http endpoint encodedArgs = urllib.urlencode(args) postdata = None baseurl = url if method == 'GET': baseurl += '?%s' % encodedArgs else: postdata = encodedArgs self.log.debug('Calling %s with args %s using %s method.', baseurl, encodedArgs, method) content = yield getPage(baseurl, method=method, postdata=postdata, timeout=self.config.timeout, agent='Jasmin gateway/1.0 %s' % self.name, headers={ 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain' }) self.log.info('Throwed DLR [msgid:%s] to %s.', msgid, baseurl) self.log.debug('Destination end replied to message [msgid:%s]: %r', msgid, content) # Check for acknowledgement if content.strip() != 'ACK/Jasmin': raise MessageAcknowledgementError( 'Destination end did not acknowledge receipt of the DLR message.' ) # Everything is okay ? then: yield self.ackMessage(message) except Exception as e: self.log.error('Throwing HTTP/DLR [msgid:%s] to (%s): %r.', msgid, baseurl, e) # List of errors after which, no further retrying shall be made noRetryErrors = ['404 Not Found'] # Requeue message for later retry if (str(e) not in noRetryErrors and self.getThrowingRetrials(message) <= self.config.max_retries): self.log.debug('Message try-count is %s [msgid:%s]: requeuing', self.getThrowingRetrials(message), msgid) yield self.rejectAndRequeueMessage(message) elif str(e) in noRetryErrors: self.log.warn( 'Message is no more processed after receiving "%s" error', str(e)) yield self.rejectMessage(message) else: self.log.warn( 'Message try-count is %s [msgid:%s]: purged from queue', self.getThrowingRetrials(message), msgid) yield self.rejectMessage(message) @defer.inlineCallbacks def smpp_dlr_callback(self, message): msgid = message.content.properties['message-id'] system_id = message.content.properties['headers']['system_id'] message_status = message.content.properties['headers'][ 'message_status'] source_addr = '%s' % message.content.properties['headers'][ 'source_addr'] destination_addr = '%s' % message.content.properties['headers'][ 'destination_addr'] sub_date = message.content.properties['headers']['sub_date'] source_addr_ton = message.content.properties['headers'][ 'source_addr_ton'] source_addr_npi = message.content.properties['headers'][ 'source_addr_npi'] dest_addr_ton = message.content.properties['headers']['dest_addr_ton'] dest_addr_npi = message.content.properties['headers']['dest_addr_npi'] self.log.debug('Got one message (msgid:%s) to throw', msgid) # If any, clear requeuing timer self.clearRequeueTimer(msgid) try: if self.smpps is None or self.smpps_access is None: raise SmppsNotSetError() # Get bound connections (or systemids) if self.smpps_access == 'direct': bound_systemdids = self.smpps.bound_connections else: bound_systemdids = yield self.smpps.list_bound_systemids() if system_id not in bound_systemdids: raise SystemIdNotBound(system_id) # Build the Receipt PDU (data_sm) pdu = self.opFactory.getReceipt(dlr_pdu=self.config.dlr_pdu, msgid=msgid, source_addr=source_addr, destination_addr=destination_addr, message_status=message_status, sub_date=sub_date, source_addr_ton=source_addr_ton, source_addr_npi=source_addr_npi, dest_addr_ton=dest_addr_ton, dest_addr_npi=dest_addr_npi) # Pick a deliverer and sendRequest if self.smpps_access == 'direct': deliverer = bound_systemdids[ system_id].getNextBindingForDelivery() if deliverer is None: raise NoDelivererForSystemId(system_id) yield deliverer.sendRequest( pdu, deliverer.config().responseTimerSecs) else: r = yield self.smpps.deliverer_send_request(system_id, pdu) if not r: raise DeliveringFailed( 'Delivering failed, check %s smpps logs for more details' % system_id) except Exception as e: self.log.error('Throwing SMPP/DLR [msgid:%s] to (%s): %r.', msgid, system_id, e) # List of exceptions after which, no further retrying shall be made noRetryExceptions = [SmppsNotSetError] retry = True for noRetryException in noRetryExceptions: if isinstance(e, noRetryException): retry = False break # Requeue message for later retry if retry and self.getThrowingRetrials( message) <= self.config.max_retries: self.log.debug('Message try-count is %s [msgid:%s]: requeuing', self.getThrowingRetrials(message), msgid) yield self.rejectAndRequeueMessage(message) elif retry and self.getThrowingRetrials( message) > self.config.max_retries: self.log.warn( 'Message is no more processed after receiving "%s" error', str(e)) yield self.rejectMessage(message) else: self.log.warn( 'Message try-count is %s [msgid:%s]: purged from queue', self.getThrowingRetrials(message), msgid) yield self.rejectMessage(message) else: # Everything is okay ? then: yield self.ackMessage(message) @defer.inlineCallbacks def dlr_throwing_callback(self, message): Thrower.throwing_callback(self, message) if message.routing_key == 'dlr_thrower.http': yield self.http_dlr_callback(message) elif message.routing_key == 'dlr_thrower.smpps': yield self.smpp_dlr_callback(message) else: self.log.error('Unknown routing_key in dlr_throwing_callback: %s', message.routing_key) yield self.rejectMessage(message)
class OperationsTest(TestCase): source_addr = '20203060' destination_addr = '98700177' latin1_sm = '6162636465666768696a6b6c6d6e6f707172737475767778797a' latin1_long_sm = '6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e6162636465666768696a6b6c6d6e6f707172737475767778797a2e' def setUp(self): self.opFactory = SMPPOperationFactory(SMPPClientConfig(id='test-id')) def buildSubmitSmTest(self, sm): """ Build a SubmitSm pdu and test if: - command_id is correct - command_status is ESME_ROCK (default value) - destination_addr is the same as self.destination_addr - source_addr is the same as self.source_addr """ pdu = self.opFactory.SubmitSM( source_addr=self.source_addr, destination_addr=self.destination_addr, short_message=sm, ) self.assertEquals(pdu.id, CommandId.submit_sm) self.assertEquals(pdu.status, CommandStatus.ESME_ROK) self.assertEquals(pdu.params['destination_addr'], self.destination_addr) self.assertEquals(pdu.params['source_addr'], self.source_addr) return pdu def test_encode_latin1(self): """ Test that a latin1 short message text remain the same after it's getting encoded in a PDU object. """ sm = binascii.a2b_hex(self.latin1_sm) pdu = self.buildSubmitSmTest(sm) # SM shall not be altered since it is not sliced (not too long) self.assertEquals(pdu.params['short_message'], sm) def test_encode_latin1_long(self): """ Test that a latin1 short message long text gets successfully sliced into multiple PDUs (parts) """ sm = binascii.a2b_hex(self.latin1_long_sm) pdu = self.buildSubmitSmTest(sm) # The first PDU shall have a next one self.assertTrue(isinstance(pdu.nextPdu, SubmitSM)) # These UDH parameters shall be present in all PDUs self.assertTrue(pdu.params['sar_msg_ref_num'] > 0) self.assertTrue(pdu.params['sar_total_segments'] > 0) self.assertTrue(pdu.params['sar_segment_seqnum'] > 0) # Iterating through sliced PDUs partedSmPdu = pdu assembledSm= '' lastSeqNum = 0 while True: assembledSm += partedSmPdu.params['short_message'] self.assertTrue(partedSmPdu.params['sar_msg_ref_num'] == pdu.params['sar_msg_ref_num']) self.assertTrue(partedSmPdu.params['sar_total_segments'] == pdu.params['sar_total_segments']) self.assertTrue(partedSmPdu.params['sar_segment_seqnum'] > lastSeqNum) lastSeqNum = partedSmPdu.params['sar_segment_seqnum'] try: partedSmPdu = partedSmPdu.nextPdu except AttributeError: break # Assembled SM shall be equal to the original SM self.assertEquals(assembledSm, sm) # The last seqNum shall be equal to total segments self.assertEquals(lastSeqNum, pdu.params['sar_total_segments']) def test_is_delivery(self): pdu = DeliverSM( source_addr='1234', destination_addr='4567', short_message='id:1891273321 sub:001 dlvrd:001 submit date:1305050826 done date:1305050826 stat:DELIVRD err:000 text:DLVRD TO MOBILE', ) isDlr = self.opFactory.isDeliveryReceipt(pdu) self.assertTrue(isDlr is not None) self.assertEquals(isDlr['id'], '1891273321') self.assertEquals(isDlr['sub'], '001') self.assertEquals(isDlr['dlvrd'], '001') self.assertEquals(isDlr['sdate'], '1305050826') self.assertEquals(isDlr['ddate'], '1305050826') self.assertEquals(isDlr['stat'], 'DELIVRD') self.assertEquals(isDlr['err'], '000') self.assertEquals(isDlr['text'], 'DLVRD TO MOBILE')
class DLRThrower(Thrower): name = 'DLRThrower' def __init__(self, config): self.log_category = "jasmin-dlr-thrower" self.exchangeName = 'messaging' self.consumerTag = 'DLRThrower' self.routingKey = 'dlr_thrower.*' self.queueName = 'dlr_thrower' self.callback = self.dlr_throwing_callback self.opFactory = SMPPOperationFactory() Thrower.__init__(self, config) @defer.inlineCallbacks def http_dlr_callback(self, message): msgid = message.content.properties['message-id'] url = message.content.properties['headers']['url'] method = message.content.properties['headers']['method'] level = message.content.properties['headers']['level'] self.log.debug('Got one message (msgid:%s) to throw', msgid) # If any, clear requeuing timer self.clearRequeueTimer(msgid) # Build mandatory arguments args = {'id': msgid, 'level': level, 'message_status': message.content.properties['headers']['message_status']} # Level 2 extra args if level in [2, 3]: args['id_smsc'] = message.content.properties['headers']['id_smsc'] args['sub'] = message.content.properties['headers']['sub'] args['dlvrd'] = message.content.properties['headers']['dlvrd'] args['subdate'] = message.content.properties['headers']['subdate'] args['donedate'] = message.content.properties['headers']['donedate'] args['err'] = message.content.properties['headers']['err'] args['text'] = message.content.properties['headers']['text'] try: # Throw the message to http endpoint encodedArgs = urllib.urlencode(args) postdata = None baseurl = url if method == 'GET': baseurl += '?%s' % encodedArgs else: postdata = encodedArgs self.log.debug('Calling %s with args %s using %s method.', baseurl, encodedArgs, method) if '127.0.0.1' not in baseurl: content = yield getPage( baseurl, method=method, postdata=postdata, timeout=self.config.timeout, agent='Jasmin gateway/1.0 %s' % self.name, headers={'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}) else: content = 'ACK/Jasmin' self.log.info('Throwed DLR [msgid:%s] to %s.', msgid, baseurl) self.log.debug('Destination end replied to message [msgid:%s]: %r', msgid, content) # Check for acknowledgement if content.strip() != 'ACK/Jasmin': raise MessageAcknowledgementError( 'Destination end did not acknowledge receipt of the DLR message.') # Everything is okay ? then: yield self.ackMessage(message) except Exception as e: self.log.error('Throwing HTTP/DLR [msgid:%s] to (%s): %r.', msgid, baseurl, e) # List of errors after which, no further retrying shall be made noRetryErrors = ['404 Not Found'] # Requeue message for later retry if (str(e) not in noRetryErrors and self.getThrowingRetrials(message) <= self.config.max_retries): self.log.debug('Message try-count is %s [msgid:%s]: requeuing', self.getThrowingRetrials(message), msgid) yield self.rejectAndRequeueMessage(message) elif str(e) in noRetryErrors: self.log.warn('Message is no more processed after receiving "%s" error', str(e)) yield self.rejectMessage(message) else: self.log.warn('Message try-count is %s [msgid:%s]: purged from queue', self.getThrowingRetrials(message), msgid) yield self.rejectMessage(message) @defer.inlineCallbacks def smpp_dlr_callback(self, message): msgid = message.content.properties['message-id'] system_id = message.content.properties['headers']['system_id'] message_status = message.content.properties['headers']['message_status'] source_addr = '%s' % message.content.properties['headers']['source_addr'] destination_addr = '%s' % message.content.properties['headers']['destination_addr'] sub_date = message.content.properties['headers']['sub_date'] source_addr_ton = message.content.properties['headers']['source_addr_ton'] source_addr_npi = message.content.properties['headers']['source_addr_npi'] dest_addr_ton = message.content.properties['headers']['dest_addr_ton'] dest_addr_npi = message.content.properties['headers']['dest_addr_npi'] self.log.debug('Got one message (msgid:%s) to throw', msgid) # If any, clear requeuing timer self.clearRequeueTimer(msgid) try: if self.smpps is None or self.smpps_access is None: raise SmppsNotSetError() # Get bound connections (or systemids) if self.smpps_access == 'direct': bound_systemdids = self.smpps.bound_connections else: bound_systemdids = yield self.smpps.list_bound_systemids() if system_id not in bound_systemdids: raise SystemIdNotBound(system_id) # Build the Receipt PDU (data_sm) pdu = self.opFactory.getReceipt(dlr_pdu=self.config.dlr_pdu, msgid=msgid, source_addr=source_addr, destination_addr=destination_addr, message_status=message_status, sub_date=sub_date, source_addr_ton=source_addr_ton, source_addr_npi=source_addr_npi, dest_addr_ton=dest_addr_ton, dest_addr_npi=dest_addr_npi) # Pick a deliverer and sendRequest if self.smpps_access == 'direct': deliverer = bound_systemdids[system_id].getNextBindingForDelivery() if deliverer is None: raise NoDelivererForSystemId(system_id) yield deliverer.sendRequest(pdu, deliverer.config().responseTimerSecs) else: r = yield self.smpps.deliverer_send_request(system_id, pdu) if not r: raise DeliveringFailed('Delivering failed, check %s smpps logs for more details' % system_id) except Exception as e: self.log.error('Throwing SMPP/DLR [msgid:%s] to (%s): %r.', msgid, system_id, e) # List of exceptions after which, no further retrying shall be made noRetryExceptions = [SmppsNotSetError] retry = True for noRetryException in noRetryExceptions: if isinstance(e, noRetryException): retry = False break # Requeue message for later retry if retry and self.getThrowingRetrials(message) <= self.config.max_retries: self.log.debug('Message try-count is %s [msgid:%s]: requeuing', self.getThrowingRetrials(message), msgid) yield self.rejectAndRequeueMessage(message) elif retry and self.getThrowingRetrials(message) > self.config.max_retries: self.log.warn('Message is no more processed after receiving "%s" error', str(e)) yield self.rejectMessage(message) else: self.log.warn('Message try-count is %s [msgid:%s]: purged from queue', self.getThrowingRetrials(message), msgid) yield self.rejectMessage(message) else: # Everything is okay ? then: yield self.ackMessage(message) @defer.inlineCallbacks def dlr_throwing_callback(self, message): Thrower.throwing_callback(self, message) if message.routing_key == 'dlr_thrower.http': yield self.http_dlr_callback(message) elif message.routing_key == 'dlr_thrower.smpps': yield self.smpp_dlr_callback(message) else: self.log.error('Unknown routing_key in dlr_throwing_callback: %s', message.routing_key) yield self.rejectMessage(message)
class SMPPClientSMListener(object): """ This is a listener object instantiated for every new SMPP connection, it is responsible of handling SubmitSm, DeliverSm and SubmitSm PDUs for a given SMPP connection """ def __init__(self, config, SMPPClientFactory, amqpBroker, redisClient, RouterPB=None, interceptorpb_client=None): self.config = config self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory(self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.RouterPB = RouterPB self.interceptorpb_client = interceptorpb_client self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.submit_retrials = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig(self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = TimedRotatingFileHandler(filename=self.config.log_file, when=self.config.log_rotate) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False def setSubmitSmQ(self, queue): self.log.debug('Setting a new submit_sm_q: %s', queue) self.submit_sm_q = queue def clearRejectTimer(self, msgid): if msgid in self.rejectTimers: timer = self.rejectTimers[msgid] if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearRejectTimers(self): for msgid, timer in self.rejectTimers.items(): if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearQosTimer(self): if self.qosTimer is not None and self.qosTimer.called is False: self.qosTimer.cancel() self.qosTimer = None def clearAllTimers(self): self.clearQosTimer() self.clearRejectTimers() @defer.inlineCallbacks def rejectAndRequeueMessage(self, message, delay=True): msgid = message.content.properties['message-id'] if delay: # Use configured requeue_delay or specific one if not isinstance(delay, bool): requeue_delay = delay else: requeue_delay = self.SMPPClientFactory.config.requeue_delay self.log.debug("Requeuing SubmitSmPDU[%s] in %s seconds", msgid, requeue_delay) # Requeue the message with a delay timer = reactor.callLater(requeue_delay, self.rejectMessage, message=message, requeue=1) # If any, clear timer before setting a new one self.clearRejectTimer(msgid) self.rejectTimers[msgid] = timer defer.returnValue(timer) else: self.log.debug("Requeuing SubmitSmPDU[%s] without delay", msgid) yield self.rejectMessage(message, requeue=1) @defer.inlineCallbacks def rejectMessage(self, message, requeue=0): yield self.amqpBroker.chan.basic_reject(delivery_tag=message.delivery_tag, requeue=requeue) @defer.inlineCallbacks def ackMessage(self, message): yield self.amqpBroker.chan.basic_ack(message.delivery_tag) @defer.inlineCallbacks def submit_sm_callback(self, message): """This callback is a queue listener it is called whenever a message was consumed from queue c.f. test_amqp.ConsumeTestCase for use cases """ msgid = None try: msgid = message.content.properties['message-id'] SubmitSmPDU = pickle.loads(message.content.body) self.submit_sm_q.get().addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) self.log.debug("Callbacked a submit_sm with a SubmitSmPDU[%s] (?): %s", msgid, SubmitSmPDU) # Update submit_sm retrial tracker if msgid in self.submit_retrials: self.submit_retrials[msgid] += 1 else: self.submit_retrials[msgid] = 1 if self.qos_last_submit_sm_at is None: self.qos_last_submit_sm_at = datetime(1970, 1, 1) if self.SMPPClientFactory.config.submit_sm_throughput > 0: # QoS throttling qos_throughput_second = 1 / float(self.SMPPClientFactory.config.submit_sm_throughput) qos_throughput_ysecond_td = timedelta(microseconds=qos_throughput_second * 1000000) qos_delay = datetime.now() - self.qos_last_submit_sm_at if qos_delay < qos_throughput_ysecond_td: qos_slow_down = float((qos_throughput_ysecond_td - qos_delay).microseconds) / 1000000 # We're faster than submit_sm_throughput, # slow down before taking a new message from the queue self.log.debug( "QoS: submit_sm_callback faster (%s) than throughput (%s), slowing down %ss (requeuing).", qos_delay, qos_throughput_ysecond_td, qos_slow_down) # Relaunch queue callbacking after qos_slow_down seconds # self.qosTimer = task.deferLater(reactor, qos_slow_down, self.submit_sm_q.get) # self.qosTimer.addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) # Requeue the message yield self.rejectAndRequeueMessage(message, delay=qos_slow_down) defer.returnValue(False) self.qos_last_submit_sm_at = datetime.now() # Verify if message is a SubmitSm PDU if isinstance(SubmitSmPDU, SubmitSM) is False: self.log.error( "Received object[%s] is not an instance of SubmitSm: discarding this unknown object from queue", msgid) yield self.rejectMessage(message) defer.returnValue(False) # If the message has expired in the queue if 'headers' in message.content.properties and 'expiration' in message.content.properties['headers']: expiration_datetime = parser.parse(message.content.properties['headers']['expiration']) if expiration_datetime < datetime.now(): self.log.info( "Discarding expired message[%s]: expiration is %s", msgid, expiration_datetime) yield self.rejectMessage(message) defer.returnValue(False) # SMPP Client should be already connected if self.SMPPClientFactory.smpp is None: created_at = parser.parse(message.content.properties['headers']['created_at']) msgAge = datetime.now() - created_at if msgAge.seconds > self.config.submit_max_age_smppc_not_ready: self.log.error( "SMPPC [cid:%s] is not connected: Discarding (#%s) SubmitSmPDU[%s], over-aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, msgAge.seconds) yield self.rejectMessage(message) defer.returnValue(False) else: if self.config.submit_retrial_delay_smppc_not_ready: delay_str = ' with delay %s seconds' % self.config.submit_retrial_delay_smppc_not_ready else: delay_str = '' self.log.error( "SMPPC [cid:%s] is not connected: Requeuing (#%s) SubmitSmPDU[%s]%s, aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, delay_str, msgAge.seconds) yield self.rejectAndRequeueMessage(message, delay=self.config.submit_retrial_delay_smppc_not_ready) defer.returnValue(False) # SMPP Client should be already bound as transceiver or transmitter if self.SMPPClientFactory.smpp.isBound() is False: created_at = parser.parse(message.content.properties['headers']['created_at']) msgAge = datetime.now() - created_at if msgAge.seconds > self.config.submit_max_age_smppc_not_ready: self.log.error( "SMPPC [cid:%s] is not bound: Discarding (#%s) SubmitSmPDU[%s], over-aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, msgAge.seconds) yield self.rejectMessage(message) defer.returnValue(False) else: if self.config.submit_retrial_delay_smppc_not_ready: delay_str = ' with delay %s seconds' % self.config.submit_retrial_delay_smppc_not_ready else: delay_str = '' self.log.error("SMPPC [cid:%s] is not bound: Requeuing (#%s) SubmitSmPDU[%s]%s, aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, delay_str, msgAge) yield self.rejectAndRequeueMessage( message, delay=self.config.submit_retrial_delay_smppc_not_ready) defer.returnValue(False) # Finally: send the sms ! self.log.debug("Sending SubmitSmPDU[%s] through SMPPClientFactory [cid:%s]", msgid, self.SMPPClientFactory.config.id) d = self.SMPPClientFactory.smpp.sendDataRequest(SubmitSmPDU) d.addCallback(self.submit_sm_resp_event, message) yield d except SMPPRequestTimoutError: self.log.error("SubmitSmPDU[%s] request timed out through [cid:%s], message requeued.", msgid, self.SMPPClientFactory.config.id) self.rejectAndRequeueMessage(message) defer.returnValue(False) except LongSubmitSmTransactionError as e: self.log.error("Long SubmitSmPDU[%s] error in [cid:%s], message requeued: %s", msgid, self.SMPPClientFactory.config.id, e.message) self.rejectAndRequeueMessage(message) defer.returnValue(False) except Exception as e: self.log.critical("Rejecting SubmitSmPDU[%s] through [cid:%s] for an unknown error (%s): %s", msgid, self.SMPPClientFactory.config.id, type(e), e) self.rejectMessage(message) defer.returnValue(False) @defer.inlineCallbacks def submit_sm_resp_event(self, r, amqpMessage): msgid = amqpMessage.content.properties['message-id'] total_bill_amount = None will_be_retried = False try: submit_sm_resp_bill = pickle.loads( amqpMessage.content.properties['headers']['submit_sm_bill']).getSubmitSmRespBill() if r.response.status == CommandStatus.ESME_ROK: # No more retrials ! del self.submit_retrials[msgid] # Get bill information total_bill_amount = 0.0 if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts() > 0: total_bill_amount = submit_sm_resp_bill.getTotalAmounts() # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(r.request.params['esm_class'], 'gsmFeatures'): for gsmFeature in r.request.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break # What type of splitting ? splitMethod = None if 'sar_msg_ref_num' in r.request.params: splitMethod = 'sar' elif UDHI_INDICATOR_SET and r.request.params['short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' # Concatenate short_message if splitMethod is not None: _pdu = r.request if splitMethod == 'sar': short_message = _pdu.params['short_message'] else: short_message = _pdu.params['short_message'][6:] while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu if splitMethod == 'sar': short_message += _pdu.params['short_message'] else: short_message += _pdu.params['short_message'][6:] # Increase bill amount for each submit_sm_resp if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts() > 0: total_bill_amount += submit_sm_resp_bill.getTotalAmounts() else: short_message = r.request.params['short_message'] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len(short_message) else: logged_content = '%r' % short_message self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [smpp-msgid:%s] [status:%s] [prio:%s] [dlr:%s] [validity:%s] \ [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, msgid, r.response.params['message_id'], r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], logged_content) else: # Message must be retried ? if str(r.response.status) in self.config.submit_error_retrial: retrial = self.config.submit_error_retrial[str(r.response.status)] # Still have some retries to go ? if self.submit_retrials[msgid] < retrial['count']: # Requeue the message for later redelivery yield self.rejectAndRequeueMessage(amqpMessage, delay=retrial['delay']) will_be_retried = True else: # Prevent this list from over-growing del self.submit_retrials[msgid] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len(r.request.params['short_message']) else: logged_content = '%r' % r.request.params['short_message'] # Log the message self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [status:ERROR/%s] [retry:%s] [prio:%s] [dlr:%s] [validity:%s] \ [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, msgid, r.response.status, will_be_retried, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], logged_content) # It is a final submit_sm_resp ! if not will_be_retried: # Cancel any mapped rejectTimer to this message # (in case this message was rejected in the past) self.clearRejectTimer(msgid) self.log.debug("ACKing amqpMessage [%s] having routing_key [%s]", msgid, amqpMessage.routing_key) # ACK the message in queue, this will remove it from the queue yield self.ackMessage(amqpMessage) # Send DLR to DLRLookup if r.response.status == CommandStatus.ESME_ROK: dlr = DLR(pdu_type=r.response.id, msgid=msgid, status=r.response.status, smpp_msgid=r.response.params['message_id']) else: dlr = DLR(pdu_type=r.response.id, msgid=msgid, status=r.response.status) yield self.amqpBroker.publish(exchange='messaging', routing_key='dlr.submit_sm_resp', content=dlr) # Bill will be charged by bill_request.submit_sm_resp.UID queue consumer if total_bill_amount > 0: pubQueueName = 'bill_request.submit_sm_resp.%s' % submit_sm_resp_bill.user.uid content = SubmitSmRespBillContent(submit_sm_resp_bill.bid, submit_sm_resp_bill.user.uid, total_bill_amount) self.log.debug( "Requesting a SubmitSmRespBillContent from a bill [bid:%s] with routing_key[%s]: %s", submit_sm_resp_bill.bid, pubQueueName, total_bill_amount) yield self.amqpBroker.publish(exchange='billing', routing_key=pubQueueName, content=content) if self.config.publish_submit_sm_resp: # Send back submit_sm_resp to submit.sm.resp.CID queue # There's no actual listeners on this queue, it can be used to # track submit_sm_resp messages from a 3rd party app content = SubmitSmRespContent(r.response, msgid, pickleProtocol=self.pickleProtocol) self.log.debug("Sending back SubmitSmRespContent[%s] with routing_key[%s]", msgid, amqpMessage.content.properties['reply-to']) yield self.amqpBroker.publish(exchange='messaging', routing_key=amqpMessage.content.properties['reply-to'], content=content) except Exception as e: self.log.error('(%s) while handling submit_sm_resp pdu for msgid:%s: %s', type(e), msgid, e) else: if will_be_retried: defer.returnValue(False) def submit_sm_errback(self, error): """It appears that when closing a queue with the close() method it errbacks with a txamqp.queue.Closed exception, didn't find a clean way to stop consuming a queue without errbacking here so this is a workaround to make it clean, it can be considered as a @TODO requiring knowledge of the queue api behaviour """ if error.check(Closed) is None: # @todo: implement this errback # For info, this errback is called whenever: # - an error has occurred inside submit_sm_callback # - the qosTimer has been cancelled (self.clearQosTimer()) try: error.raiseException() except Exception as e: self.log.error("Error in submit_sm_errback (%s): %s", type(e), e) @defer.inlineCallbacks def concatDeliverSMs(self, HSetReturn, hashKey, splitMethod, total_segments, msg_ref_num, segment_seqnum): if HSetReturn == 0: self.log.warn('This hashKey %s already exists, will not reset it !', hashKey) return # @TODO: longDeliverSm part expiry must be configurable yield self.redisClient.expire(hashKey, 300) # This is the last part if segment_seqnum == total_segments: hvals = yield self.redisClient.hvals(hashKey) if len(hvals) != total_segments: self.log.warn( 'Received the last part (msg_ref_num:%s) and did not find all parts in redis, data lost !', msg_ref_num) return # Get PDUs pdus = {} for pickledValue in hvals: value = pickle.loads(pickledValue) pdus[value['segment_seqnum']] = value['pdu'] # Where is the message content to be found ? if 'short_message' in pdus[1].params: msg_content_key = 'short_message' elif 'message_payload' in pdus[1].params: msg_content_key = 'message_payload' else: self.log.warn('Cannot find message content in first pdu params: %s', pdus[1].params) return # Build concat_message_content concat_message_content = '' for i in range(total_segments): if splitMethod == 'sar': concat_message_content += pdus[i + 1].params[msg_content_key] else: concat_message_content += pdus[i + 1].params[msg_content_key][6:] # Build the final pdu and return it back to deliver_sm_event pdu = pdus[1] # Take the first part as a base of work # 1. Remove message splitting information from pdu if splitMethod == 'sar': del pdu.params['sar_segment_seqnum'] del pdu.params['sar_total_segments'] del pdu.params['sar_msg_ref_num'] else: pdu.params['esm_class'] = None # 2. Set the new concat_message_content pdu.params[msg_content_key] = concat_message_content routable = RoutableDeliverSm(pdu, Connector(self.SMPPClientFactory.config.id)) yield self.deliver_sm_event_post_interception(routable=routable, smpp=None, concatenated=True) def code_dlr_msgid(self, pdu): """Code the dlr msg id accordingly to SMPPc's dlr_msg_id_bases value""" try: if isinstance(pdu, DeliverSM): if self.SMPPClientFactory.config.dlr_msg_id_bases == 1: ret = ('%x' % int(pdu.dlr['id'])).upper().lstrip('0') elif self.SMPPClientFactory.config.dlr_msg_id_bases == 2: ret = int(str(pdu.dlr['id']), 16) else: ret = str(pdu.dlr['id']).upper().lstrip('0') else: # TODO: code dlr for submit_sm_resp maybe ? TBC ret = str(pdu.dlr['id']).upper().lstrip('0') except Exception as e: self.log.error('code_dlr_msgid, cannot code msgid [%s] with dlr_msg_id_bases:%s', pdu.dlr['id'], self.SMPPClientFactory.config.dlr_msg_id_bases) self.log.error('code_dlr_msgid, error details: %s', e) ret = str(pdu.dlr['id']).upper().lstrip('0') self.log.debug('code_dlr_msgid: %s coded to %s', pdu.dlr['id'], ret) return ret def deliver_sm_event_interceptor(self, smpp, pdu): self.log.debug('Intercepting deliver_sm event in smppc %s', self.SMPPClientFactory.config.id) if self.RouterPB is None: self.log.error( '(deliver_sm_event_interceptor/%s) RouterPB not set: deliver_sm will not be routed', self.SMPPClientFactory.config.id) return # Prepare for interception # this is a temporary routable instance to be used in interception routable = RoutableDeliverSm(pdu, Connector(self.SMPPClientFactory.config.id)) # Interception inline # @TODO: make Interception in a thread, just like httpapi interception interceptor = self.RouterPB.getMOInterceptionTable().getInterceptorFor(routable) if interceptor is not None: self.log.debug("RouterPB selected %s interceptor for this DeliverSmPDU", interceptor) if self.interceptorpb_client is None: smpp.factory.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: smpp.factory.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError('InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! d = self.interceptorpb_client.run_script(script, routable) d.addCallback(self.deliver_sm_event_post_interception, routable=routable, smpp=smpp) d.addErrback(self.deliver_sm_event_post_interception) return d else: return self.deliver_sm_event_post_interception(routable=routable, smpp=smpp) @defer.inlineCallbacks def deliver_sm_event_post_interception(self, *args, **kw): """This event is called whenever a deliver_sm pdu is received through a SMPPc It will hand the pdu to the router or a dlr thrower (depending if its a DLR or not). Note: this event will catch data_sm pdus as well """ try: # Control args if 'smpp' not in kw or 'routable' not in kw: self.log.error( 'deliver_sm_event_post_interception missing arguments after interception: %s', kw) raise InterceptorRunError( 'deliver_sm_event_post_interception missing arguments after interception') # Set defaults smpp = kw['smpp'] routable = kw['routable'] if 'concatenated' in kw: concatenated = kw['concatenated'] else: concatenated = False # Get message_content if 'short_message' in routable.pdu.params and len(routable.pdu.params['short_message']) > 0: message_content = routable.pdu.params['short_message'] elif 'message_payload' in routable.pdu.params: message_content = routable.pdu.params['message_payload'] elif 'short_message' in routable.pdu.params: message_content = routable.pdu.params['short_message'] else: message_content = None # Post interception: if len(args) == 1: if isinstance(args[0], bool) and not args[0]: smpp.factory.stats.inc('interceptor_error_count') self.log.error('Failed running interception script, got a False return.') raise InterceptorRunError('Failed running interception script, check log for details') elif isinstance(args[0], dict) and args[0]['smpp_status'] > 0: smpp.factory.stats.inc('interceptor_error_count') self.log.info( 'Interceptor script returned %s smpp_status error.', args[0]['smpp_status']) raise DeliverSmInterceptionError(code=args[0]['smpp_status']) elif isinstance(args[0], str): smpp.factory.stats.inc('interceptor_count') routable = pickle.loads(args[0]) else: smpp.factory.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', args[0]) raise InterceptorRunError( 'Failed running interception script, got the following return: %s' % args[0]) self.log.debug('Handling deliver_sm_event_post_interception event for smppc: %s', self.SMPPClientFactory.config.id) routable.pdu.dlr = self.SMPPOperationFactory.isDeliveryReceipt(routable.pdu) content = DeliverSmContent(routable, self.SMPPClientFactory.config.id, pickleProtocol=self.pickleProtocol, concatenated=concatenated) msgid = content.properties['message-id'] if routable.pdu.dlr is None: # We have a SMS-MO # UDH is set ? UDHI_INDICATOR_SET = False if 'esm_class' in routable.pdu.params and hasattr(routable.pdu.params['esm_class'], 'gsmFeatures'): for gsmFeature in routable.pdu.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break not_class2 = True if 'data_coding' in routable.pdu.params: dcs = routable.pdu.params['data_coding'] if (str(dcs.scheme) == 'GSM_MESSAGE_CLASS') and (dcs.schemeData is not None): not_class2 = (str(dcs.schemeData.msgClass) != 'CLASS_2') splitMethod = None # Is it a part of a long message ? if 'sar_msg_ref_num' in routable.pdu.params: splitMethod = 'sar' total_segments = routable.pdu.params['sar_total_segments'] segment_seqnum = routable.pdu.params['sar_segment_seqnum'] msg_ref_num = routable.pdu.params['sar_msg_ref_num'] self.log.debug( 'Received SMS-MO part [queue-msgid:%s] using SAR: ttl_segments=%s, segment_sn=%s, msgref=%s', msgid, total_segments, segment_seqnum, msg_ref_num) elif UDHI_INDICATOR_SET and not_class2 and message_content[:3] == '\x05\x00\x03': splitMethod = 'udh' total_segments = struct.unpack('!B', message_content[4])[0] segment_seqnum = struct.unpack('!B', message_content[5])[0] msg_ref_num = struct.unpack('!B', message_content[3])[0] self.log.debug( 'Received SMS-MO part [queue-msgid:%s] using UDH: ttl_segments=%s, segment_sn=%s, msgref=%s', msgid, total_segments, segment_seqnum, msg_ref_num) if splitMethod is None: # It's a simple short message or a part of a concatenated message routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug("Publishing DeliverSmContent[%s] with routing_key[%s]", msgid, routing_key) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # Get values from data_sm or deliver_sm priority_flag = None if 'priority_flag' in routable.pdu.params: priority_flag = routable.pdu.params['priority_flag'] validity_period = None if 'validity_period' in routable.pdu.params: validity_period = routable.pdu.params['validity_period'] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len(message_content) else: logged_content = '%r' % message_content self.log.info( "SMS-MO [cid:%s] [queue-msgid:%s] [status:%s] [prio:%s] [validity:%s] [from:%s] [to:%s] \ [content:%s]", self.SMPPClientFactory.config.id, msgid, routable.pdu.status, priority_flag, validity_period, routable.pdu.params['source_addr'], routable.pdu.params['destination_addr'], logged_content) else: # Long message part received if self.redisClient is None: self.log.critical( 'Invalid RC found while receiving part of long DeliverSm [queue-msgid:%s], MSG IS LOST !', msgid) else: # Save it to redis hashKey = "longDeliverSm:%s:%s:%s" % ( self.SMPPClientFactory.config.id, msg_ref_num, routable.pdu.params['destination_addr']) hashValues = {'pdu': routable.pdu, 'total_segments': total_segments, 'msg_ref_num': msg_ref_num, 'segment_seqnum': segment_seqnum} yield self.redisClient.hset( hashKey, segment_seqnum, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.concatDeliverSMs, hashKey, splitMethod, total_segments, msg_ref_num, segment_seqnum) self.log.info( "DeliverSmContent[%s] is part of long msg of (%s), will be enqueued after concatenation.", msgid, total_segments) # Flag it as "will_be_concatenated" and publish it to router routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug("Publishing DeliverSmContent[%s](flagged:wbc) with routing_key[%s]", msgid, routing_key) content.properties['headers']['will_be_concatenated'] = True yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) else: # This is a DLR ! # Send DLR to DLRLookup yield self.amqpBroker.publish(exchange='messaging', routing_key='dlr.deliver_sm', content=DLR(pdu_type=routable.pdu.id, msgid=self.code_dlr_msgid(routable.pdu), status=routable.pdu.dlr['stat'], cid=self.SMPPClientFactory.config.id, dlr_details=routable.pdu.dlr)) except (InterceptorRunError, DeliverSmInterceptionError) as e: # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len(message_content) else: logged_content = '%r' % message_content self.log.info("SMS-MO [cid:%s] [i-status:%s] [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, e.status, routable.pdu.params['source_addr'], routable.pdu.params['destination_addr'], logged_content) # Known exception handling defer.returnValue(DataHandlerResponse(status=e.status)) except Exception as e: # Unknown exception handling self.log.critical('Got an unknown exception (%s): %s', type(e), e) defer.returnValue(DataHandlerResponse(status=CommandStatus.ESME_RUNKNOWNERR))
def setUp(self): self.opFactory = SMPPOperationFactory(SMPPClientConfig(id='test-id'))
def setUp(self): self.opFactory = SMPPOperationFactory(SMPPClientConfig(id='test-id'))
class Rate(Resource): isleaf = True def __init__(self, HTTPApiConfig, RouterPB, stats, log, interceptorpb_client): Resource.__init__(self) self.RouterPB = RouterPB self.stats = stats self.log = log self.interceptorpb_client = interceptorpb_client # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) @defer.inlineCallbacks def route_routable(self, request): try: # Do we have a hex-content ? if 'hex-content' not in request.args: # Convert utf8 to GSM 03.38 if request.args['coding'][0] == '0': short_message = gsm_encode( request.args['content'][0].decode('utf-8')) else: # Otherwise forward it as is short_message = request.args['content'][0] else: # Otherwise convert hex to bin short_message = hex2bin(request.args['hex-content'][0]) # Authentication user = self.RouterPB.authenticateUser( username=request.args['username'][0], password=request.args['password'][0]) if user is None: self.stats.inc('auth_error_count') self.log.debug( "Authentication failure for username:%s and password:%s", request.args['username'][0], request.args['password'][0]) self.log.error("Authentication failure for username:%s", request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % request.args['username'][0]) # Update CnxStatus user.getCnxStatus().httpapi['connects_count'] += 1 user.getCnxStatus().httpapi['rate_request_count'] += 1 user.getCnxStatus().httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_add=None if 'from' not in request.args else request.args['from'][0], destination_addr=request.args['to'][0], short_message=short_message, data_coding=int(request.args['coding'][0]), ) self.log.debug("Built base SubmitSmPDU: %s", SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Rate', user, request, submit_sm=SubmitSmPDU) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Prepare for interception than routing routable = RoutableSubmitSm(SubmitSmPDU, user) self.log.debug("Built Routable %s for SubmitSmPDU: %s", routable, SubmitSmPDU) # Should we tag the routable ? tags = [] if 'tags' in request.args: tags = request.args['tags'][0].split(',') for tag in tags: routable.addTag(tag) self.log.debug('Tagged routable %s: +%s', routable, tag) # Intercept interceptor = self.RouterPB.getMTInterceptionTable( ).getInterceptorFor(routable) if interceptor is not None: self.log.debug( "RouterPB selected %s interceptor for this SubmitSmPDU", interceptor) if self.interceptorpb_client is None: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError( 'InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! r = yield self.interceptorpb_client.run_script( script, routable) if isinstance(r, dict) and r['http_status'] != 200: self.stats.inc('interceptor_error_count') self.log.error( 'Interceptor script returned %s http_status error.', r['http_status']) raise InterceptorRunError( code=r['http_status'], message='Interception specific error code %s' % r['http_status']) elif isinstance(r, str): self.stats.inc('interceptor_count') routable = pickle.loads(r) else: self.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', r) raise InterceptorRunError( message= 'Failed running interception script, check log for details' ) # Routing route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.log.error( "No route matched from user %s for SubmitSmPDU: %s", user, SubmitSmPDU) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s for this SubmitSmPDU", route) # Get number of PDUs to be sent (for billing purpose) _pdu = SubmitSmPDU submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Get the bill bill = route.getBillFor(user) response = { 'return': { 'unit_rate': bill.getTotalAmounts(), 'submit_sm_count': submit_sm_count }, 'status': 200 } except Exception as e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally: self.log.debug("Returning %s to %s.", response, request.getClientIP()) # Return message if response['return'] is None: response['return'] = 'System error' request.setResponseCode(500) else: request.setResponseCode(response['status']) request.write(json.dumps(response['return'])) request.finish() def render(self, request): """ /rate request processing Note: This method will indicate the rate of the message once sent """ self.log.debug("Rendering /rate response with args: %s from %s", request.args, request.getClientIP()) request.responseHeaders.addRawHeader(b"content-type", b"application/json") response = {'return': None, 'status': 200} self.stats.inc('request_count') self.stats.set('last_request_at', datetime.now()) try: # Validation (must be almost the same params as /send service) fields = { 'to': { 'optional': False, 'pattern': re.compile(r'^\+{0,1}\d+$') }, 'from': { 'optional': True }, 'coding': { 'optional': True, 'pattern': re.compile(r'^(0|1|2|3|4|5|6|7|8|9|10|13|14){1}$') }, 'username': { 'optional': False, 'pattern': re.compile(r'^.{1,15}$') }, 'password': { 'optional': False, 'pattern': re.compile(r'^.{1,8}$') }, # Priority validation pattern can be validated/filtered further more # through HttpAPICredentialValidator 'priority': { 'optional': True, 'pattern': re.compile(r'^[0-3]$') }, # Validity period validation pattern can be validated/filtered further more # through HttpAPICredentialValidator 'validity-period': { 'optional': True, 'pattern': re.compile(r'^\d+$') }, 'tags': { 'optional': True, 'pattern': re.compile(r'^([-a-zA-Z0-9,])*$') }, 'content': { 'optional': True }, 'hex-content': { 'optional': True }, } # Default coding is 0 when not provided if 'coding' not in request.args: request.args['coding'] = ['0'] # Content is optional, defaults to empty content string if 'hex-content' not in request.args and 'content' not in request.args: request.args['content'] = [''] # Make validation v = UrlArgsValidator(request, fields) v.validate() # Check if have content --OR-- hex-content # @TODO: make this inside UrlArgsValidator ! if 'content' in request.args and 'hex-content' in request.args: raise UrlArgsValidationError( "content and hex-content cannot be used both in same request." ) # Continue routing in a separate thread reactor.callFromThread(self.route_routable, request=request) except Exception as e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} self.log.debug("Returning %s to %s.", response, request.getClientIP()) # Return message if response['return'] is None: response['return'] = 'System error' request.setResponseCode(500) else: request.setResponseCode(response['status']) return json.dumps(response['return']) else: return NOT_DONE_YET
class Rate(Resource): def __init__(self, HTTPApiConfig, RouterPB, stats, log): Resource.__init__(self) self.RouterPB = RouterPB self.log = log self.stats = stats # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) def render(self, request): """ /rate request processing Note: This method will indicate the rate of the message once sent """ self.log.debug("Rendering /rate response with args: %s from %s" % (request.args, request.getClientIP())) request.responseHeaders.addRawHeader(b"content-type", b"application/json") response = {'return': None, 'status': 200} self.stats.inc('request_count') self.stats.set('last_request_at', datetime.now()) try: # Validation (must be almost the same params as /send service) fields = { 'to': { 'optional': False, 'pattern': re.compile(r'^\+{0,1}\d+$') }, 'from': { 'optional': True }, 'coding': { 'optional': True, 'pattern': re.compile(r'^(0|1|2|3|4|5|6|7|8|9|10|13|14){1}$') }, 'username': { 'optional': False, 'pattern': re.compile(r'^.{1,9}$') }, 'password': { 'optional': False, 'pattern': re.compile(r'^.{1,9}$') }, # Priority validation pattern can be validated/filtered further more through HttpAPICredentialValidator 'priority': { 'optional': True, 'pattern': re.compile(r'^[0-3]$') }, # Validity period validation pattern can be validated/filtered further more through HttpAPICredentialValidator 'validity-period': { 'optional': True, 'pattern': re.compile(r'^\d+$') }, 'content': { 'optional': True }, } # Default coding is 0 when not provided if 'coding' not in request.args: request.args['coding'] = ['0'] # Content is optional, defaults to empty string if 'content' not in request.args: request.args['content'] = [''] # Make validation v = UrlArgsValidator(request, fields) v.validate() # Authentication user = self.RouterPB.authenticateUser( username=request.args['username'][0], password=request.args['password'][0]) if user is None: self.stats.inc('auth_error_count') self.log.debug( "Authentication failure for username:%s and password:%s" % (request.args['username'][0], request.args['password'][0])) self.log.error("Authentication failure for username:%s" % request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % request.args['username'][0]) # Update CnxStatus user.getCnxStatus().httpapi['connects_count'] += 1 user.getCnxStatus().httpapi['rate_request_count'] += 1 user.getCnxStatus().httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_addr=None if 'from' not in request.args else request.args['from'][0], destination_addr=request.args['to'][0], short_message=request.args['content'][0], data_coding=int(request.args['coding'][0]), ) self.log.debug("Built base SubmitSmPDU: %s" % SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Rate', user, request, submit_sm=SubmitSmPDU) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Routing routedConnector = None # init routable = RoutableSubmitSm(SubmitSmPDU, user) route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.log.error( "No route matched from user %s for SubmitSmPDU: %s" % (user, SubmitSmPDU)) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s for this SubmitSmPDU" % route) routedConnector = route.getConnector() # Get number of PDUs to be sent (for billing purpose) _pdu = SubmitSmPDU submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Get the bill bill = route.getBillFor(user) response = { 'return': { 'unit_rate': bill.getTotalAmounts(), 'submit_sm_count': submit_sm_count }, 'status': 200 } except Exception, e: self.log.error("Error: %s" % e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally:
class Send(Resource): def __init__(self, HTTPApiConfig, RouterPB, SMPPClientManagerPB, log): Resource.__init__(self) self.SMPPClientManagerPB = SMPPClientManagerPB self.RouterPB = RouterPB self.log = log # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) def render(self, request): """ /send request processing Note: This method MUST behave exactly like jasmin.protocols.smpp.factory.SMPPServerFactory.submit_sm_event """ self.log.debug("Rendering /send response with args: %s from %s" % (request.args, request.getClientIP())) response = {'return': None, 'status': 200} # updated_request will be filled with default values where request will never get modified # updated_request is used for sending the SMS, request is just kept as an original request object updated_request = request try: # Validation fields = { 'to': { 'optional': False, 'pattern': re.compile(r'^\+{0,1}\d+$') }, 'from': { 'optional': True }, 'coding': { 'optional': True, 'pattern': re.compile(r'^(0|1|2|3|4|5|6|7|8|9|10|13|14){1}$') }, 'username': { 'optional': False, 'pattern': re.compile(r'^.{1,30}$') }, 'password': { 'optional': False, 'pattern': re.compile(r'^.{1,30}$') }, # Priority validation pattern can be validated/filtered further more through HttpAPICredentialValidator 'priority': { 'optional': True, 'pattern': re.compile(r'^[0-3]$') }, 'dlr': { 'optional': False, 'pattern': re.compile(r'^(yes|no)$') }, 'dlr-url': { 'optional': True, 'pattern': re.compile(r'^(http|https)\://.*$') }, # DLR Level validation pattern can be validated/filtered further more through HttpAPICredentialValidator 'dlr-level': { 'optional': True, 'pattern': re.compile(r'^[1-3]$') }, 'dlr-method': { 'optional': True, 'pattern': re.compile(r'^(get|post)$', re.IGNORECASE) }, 'content': { 'optional': False }, } # Default coding is 0 when not provided if 'coding' not in updated_request.args: updated_request.args['coding'] = ['0'] # Set default for undefined updated_request.arguments if 'dlr-url' in updated_request.args or 'dlr-level' in updated_request.args: updated_request.args['dlr'] = ['yes'] if 'dlr' not in updated_request.args: # Setting DLR updated_request to 'no' updated_request.args['dlr'] = ['no'] # Set default values if updated_request.args['dlr'][0] == 'yes': if 'dlr-level' not in updated_request.args: # If DLR is requested and no dlr-level were provided, assume minimum level (1) updated_request.args['dlr-level'] = [1] if 'dlr-method' not in updated_request.args: # If DLR is requested and no dlr-method were provided, assume default (POST) updated_request.args['dlr-method'] = ['POST'] # DLR method must be uppercase if 'dlr-method' in updated_request.args: updated_request.args['dlr-method'][0] = updated_request.args[ 'dlr-method'][0].upper() # Make validation v = UrlArgsValidator(updated_request, fields) v.validate() # Authentication user = self.RouterPB.authenticateUser( username=updated_request.args['username'][0], password=updated_request.args['password'][0]) if user is None: self.log.debug( "Authentication failure for username:%s and password:%s" % (updated_request.args['username'][0], updated_request.args['password'][0])) self.log.error("Authentication failure for username:%s" % updated_request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % updated_request.args['username'][0]) # Update CnxStatus user.CnxStatus.httpapi['connects_count'] += 1 user.CnxStatus.httpapi['submit_sm_request_count'] += 1 user.CnxStatus.httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_addr=None if 'from' not in updated_request.args else updated_request.args['from'][0], destination_addr=updated_request.args['to'][0], short_message=updated_request.args['content'][0], data_coding=int(updated_request.args['coding'][0]), ) self.log.debug("Built base SubmitSmPDU: %s" % SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Send', user, SubmitSmPDU, request) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Routing routedConnector = None # init routable = RoutableSubmitSm(SubmitSmPDU, user) route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.log.error( "No route matched from user %s for SubmitSmPDU: %s" % (user, SubmitSmPDU)) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s for this SubmitSmPDU" % route) routedConnector = route.getConnector() # Set priority priority = 0 if 'priority' in updated_request.args: priority = int(updated_request.args['priority'][0]) SubmitSmPDU.params['priority_flag'] = priority_flag_value_map[ priority] self.log.debug("SubmitSmPDU priority is set to %s" % priority) # Set DLR bit mask # c.f. 5.2.17 registered_delivery #################################################################### # dlr-level # Signification # registered_delivery # #################################################################### # 1 # SMS-C level # x x x x x x 1 0 # # 2 # Terminal level (only) # x x x x x x 0 1 # # 3 # SMS-C level and Terminal level # x x x x x x 0 1 # #################################################################### if updated_request.args['dlr'][0] == 'yes': if updated_request.args['dlr-level'][0] == '1': SubmitSmPDU.params[ 'registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt. NO_SMSC_DELIVERY_RECEIPT_REQUESTED) elif updated_request.args['dlr-level'][ 0] == '2' or updated_request.args['dlr-level'][ 0] == '3': SubmitSmPDU.params[ 'registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt. SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE) self.log.debug("SubmitSmPDU registered_delivery is set to %s" % str(SubmitSmPDU.params['registered_delivery'])) dlr_level = int(updated_request.args['dlr-level'][0]) if 'dlr-url' in updated_request.args: dlr_url = updated_request.args['dlr-url'][0] else: dlr_url = None if updated_request.args['dlr-level'][0] == '1': dlr_level_text = 'SMS-C' elif updated_request.args['dlr-level'][0] == '2': dlr_level_text = 'Terminal' else: dlr_level_text = 'All' dlr_method = updated_request.args['dlr-method'][0] else: dlr_url = None dlr_level = 1 dlr_level_text = 'No' dlr_method = None # Get number of PDUs to be sent (for billing purpose) _pdu = SubmitSmPDU submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Pre-sending submit_sm: Billing processing bill = route.getBillFor(user) self.log.debug( "SubmitSmBill [bid:%s] [ttlamounts:%s] generated for this SubmitSmPDU (x%s)" % (bill.bid, bill.getTotalAmounts(), submit_sm_count)) charging_requirements = [] u_balance = user.mt_credential.getQuota('balance') u_subsm_count = user.mt_credential.getQuota('submit_sm_count') if u_balance is not None and bill.getTotalAmounts() > 0: # Ensure user have enough balance to pay submit_sm and submit_sm_resp charging_requirements.append({ 'condition': bill.getTotalAmounts() * submit_sm_count <= u_balance, 'error_message': 'Not enough balance (%s) for charging: %s' % (u_balance, bill.getTotalAmounts()) }) if u_subsm_count is not None: # Ensure user have enough submit_sm_count to to cover the bill action (decrement_submit_sm_count) charging_requirements.append({ 'condition': bill.getAction('decrement_submit_sm_count') * submit_sm_count <= u_subsm_count, 'error_message': 'Not enough submit_sm_count (%s) for charging: %s' % (u_subsm_count, bill.getAction('decrement_submit_sm_count')) }) if self.RouterPB.chargeUserForSubmitSms( user, bill, submit_sm_count, charging_requirements) is None: self.log.error( 'Charging user %s failed, [bid:%s] [ttlamounts:%s] SubmitSmPDU (x%s)' % (user, bill.bid, bill.getTotalAmounts(), submit_sm_count)) raise ChargingError( 'Cannot charge submit_sm, check RouterPB log file for details' ) ######################################################## # Send SubmitSmPDU through smpp client manager PB server self.log.debug( "Connector '%s' is set to be a route for this SubmitSmPDU" % routedConnector.cid) c = self.SMPPClientManagerPB.perspective_submit_sm( routedConnector.cid, SubmitSmPDU, priority, pickled=False, dlr_url=dlr_url, dlr_level=dlr_level, dlr_method=dlr_method, submit_sm_resp_bill=bill.getSubmitSmRespBill()) # Build final response if not c.result: self.log.error('Failed to send SubmitSmPDU to [cid:%s]' % routedConnector.cid) raise ServerError( 'Cannot send submit_sm, check SMPPClientManagerPB log file for details' ) else: self.log.debug('SubmitSmPDU sent to [cid:%s], result = %s' % (routedConnector.cid, c.result)) response = {'return': c.result, 'status': 200} except Exception, e: self.log.error("Error: %s" % e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally:
class Send(Resource): isleaf = True def __init__(self, HTTPApiConfig, RouterPB, SMPPClientManagerPB, stats, log, interceptorpb_client): Resource.__init__(self) self.SMPPClientManagerPB = SMPPClientManagerPB self.RouterPB = RouterPB self.stats = stats self.log = log self.interceptorpb_client = interceptorpb_client # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) @defer.inlineCallbacks def route_routable(self, updated_request): try: # Authentication user = self.RouterPB.authenticateUser( username=updated_request.args['username'][0], password=updated_request.args['password'][0]) if user is None: self.stats.inc('auth_error_count') self.log.debug( "Authentication failure for username:%s and password:%s", updated_request.args['username'][0], updated_request.args['password'][0]) self.log.error("Authentication failure for username:%s", updated_request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % updated_request.args['username'][0]) # Update CnxStatus user.getCnxStatus().httpapi['connects_count'] += 1 user.getCnxStatus().httpapi['submit_sm_request_count'] += 1 user.getCnxStatus().httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_addr=None if 'from' not in updated_request.args else updated_request.args['from'][0], destination_addr=updated_request.args['to'][0], short_message=updated_request.args['content'][0], data_coding=int(updated_request.args['coding'][0])) self.log.debug("Built base SubmitSmPDU: %s", SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Send', user, updated_request, submit_sm=SubmitSmPDU) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Prepare for interception then routing routedConnector = None # init routable = RoutableSubmitSm(SubmitSmPDU, user) self.log.debug("Built Routable %s for SubmitSmPDU: %s", routable, SubmitSmPDU) # Should we tag the routable ? tags = [] if 'tags' in updated_request.args: tags = updated_request.args['tags'][0].split(',') for tag in tags: routable.addTag(tag) self.log.debug('Tagged routable %s: +%s', routable, tag) # Intercept interceptor = self.RouterPB.getMTInterceptionTable( ).getInterceptorFor(routable) if interceptor is not None: self.log.debug( "RouterPB selected %s interceptor for this SubmitSmPDU", interceptor) if self.interceptorpb_client is None: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError( 'InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! r = yield self.interceptorpb_client.run_script( script, routable) if isinstance(r, dict) and r['http_status'] != 200: self.stats.inc('interceptor_error_count') self.log.error( 'Interceptor script returned %s http_status error.', r['http_status']) raise InterceptorRunError( code=r['http_status'], message='Interception specific error code %s' % r['http_status']) elif isinstance(r, str): self.stats.inc('interceptor_count') routable = pickle.loads(r) else: self.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', r) raise InterceptorRunError( message= 'Failed running interception script, check log for details' ) # Get the route route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.stats.inc('route_error_count') self.log.error( "No route matched from user %s for SubmitSmPDU: %s", user, routable.pdu) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s route for this SubmitSmPDU", route) routedConnector = route.getConnector() # Is it a failover route ? then check for a bound connector, otherwise don't route # The failover route requires at least one connector to be up, no message enqueuing will # occur otherwise. if repr(route) == 'FailoverMTRoute': self.log.debug( 'Selected route is a failover, will ensure connector is bound:' ) while True: c = self.SMPPClientManagerPB.perspective_connector_details( routedConnector.cid) if c: self.log.debug('Connector [%s] is: %s', routedConnector.cid, c['session_state']) else: self.log.debug('Connector [%s] is not found', routedConnector.cid) if c and c['session_state'][:6] == 'BOUND_': # Choose this connector break else: # Check next connector, None if no more connectors are available routedConnector = route.getConnector() if routedConnector is None: break if routedConnector is None: self.stats.inc('route_error_count') self.log.error( "Failover route has no bound connector to handle SubmitSmPDU: %s", routable.pdu) raise ConnectorNotFoundError( "Failover route has no bound connectors") # Re-update SubmitSmPDU with parameters from the route's connector connector_config = self.SMPPClientManagerPB.perspective_connector_config( routedConnector.cid) if connector_config: connector_config = pickle.loads(connector_config) routable = update_submit_sm_pdu(routable=routable, config=connector_config) # Set priority priority = 0 if 'priority' in updated_request.args: priority = int(updated_request.args['priority'][0]) routable.pdu.params['priority_flag'] = priority_flag_value_map[ priority] self.log.debug("SubmitSmPDU priority is set to %s", priority) # Set validity_period if 'validity-period' in updated_request.args: delta = timedelta( minutes=int(updated_request.args['validity-period'][0])) routable.pdu.params['validity_period'] = datetime.today( ) + delta self.log.debug( "SubmitSmPDU validity_period is set to %s (+%s minutes)", routable.pdu.params['validity_period'], updated_request.args['validity-period'][0]) # Set DLR bit mask on the last pdu _last_pdu = routable.pdu while True: if hasattr(_last_pdu, 'nextPdu'): _last_pdu = _last_pdu.nextPdu else: break # DLR setting is clearly described in #107 _last_pdu.params['registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED) if updated_request.args['dlr'][0] == 'yes': _last_pdu.params['registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED) self.log.debug("SubmitSmPDU registered_delivery is set to %s", str(_last_pdu.params['registered_delivery'])) dlr_level = int(updated_request.args['dlr-level'][0]) if 'dlr-url' in updated_request.args: dlr_url = updated_request.args['dlr-url'][0] else: dlr_url = None if updated_request.args['dlr-level'][0] == '1': dlr_level_text = 'SMS-C' elif updated_request.args['dlr-level'][0] == '2': dlr_level_text = 'Terminal' else: dlr_level_text = 'All' dlr_method = updated_request.args['dlr-method'][0] else: dlr_url = None dlr_level = 0 dlr_level_text = 'No' dlr_method = None # QoS throttling if user.mt_credential.getQuota( 'http_throughput') >= 0 and user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] != 0: qos_throughput_second = 1 / float( user.mt_credential.getQuota('http_throughput')) qos_throughput_ysecond_td = timedelta( microseconds=qos_throughput_second * 1000000) qos_delay = datetime.now() - user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] if qos_delay < qos_throughput_ysecond_td: self.stats.inc('throughput_error_count') self.log.error( "QoS: submit_sm_event is faster (%s) than fixed throughput (%s), user:%s, rejecting message.", qos_delay, qos_throughput_ysecond_td, user) raise ThroughputExceededError("User throughput exceeded") user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] = datetime.now() # Get number of PDUs to be sent (for billing purpose) _pdu = routable.pdu submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Pre-sending submit_sm: Billing processing bill = route.getBillFor(user) self.log.debug( "SubmitSmBill [bid:%s] [ttlamounts:%s] generated for this SubmitSmPDU (x%s)", bill.bid, bill.getTotalAmounts(), submit_sm_count) charging_requirements = [] u_balance = user.mt_credential.getQuota('balance') u_subsm_count = user.mt_credential.getQuota('submit_sm_count') if u_balance is not None and bill.getTotalAmounts() > 0: # Ensure user have enough balance to pay submit_sm and submit_sm_resp charging_requirements.append({ 'condition': bill.getTotalAmounts() * submit_sm_count <= u_balance, 'error_message': 'Not enough balance (%s) for charging: %s' % (u_balance, bill.getTotalAmounts()) }) if u_subsm_count is not None: # Ensure user have enough submit_sm_count to to cover # the bill action (decrement_submit_sm_count) charging_requirements.append({ 'condition': bill.getAction('decrement_submit_sm_count') * submit_sm_count <= u_subsm_count, 'error_message': 'Not enough submit_sm_count (%s) for charging: %s' % (u_subsm_count, bill.getAction('decrement_submit_sm_count')) }) if self.RouterPB.chargeUserForSubmitSms( user, bill, submit_sm_count, charging_requirements) is None: self.stats.inc('charging_error_count') self.log.error( 'Charging user %s failed, [bid:%s] [ttlamounts:%s] SubmitSmPDU (x%s)', user, bill.bid, bill.getTotalAmounts(), submit_sm_count) raise ChargingError( 'Cannot charge submit_sm, check RouterPB log file for details' ) ######################################################## # Send SubmitSmPDU through smpp client manager PB server self.log.debug( "Connector '%s' is set to be a route for this SubmitSmPDU", routedConnector.cid) c = self.SMPPClientManagerPB.perspective_submit_sm( cid=routedConnector.cid, SubmitSmPDU=routable.pdu, submit_sm_bill=bill, priority=priority, pickled=False, dlr_url=dlr_url, dlr_level=dlr_level, dlr_method=dlr_method) # Build final response if not c.result: self.stats.inc('server_error_count') self.log.error('Failed to send SubmitSmPDU to [cid:%s]', routedConnector.cid) raise ServerError( 'Cannot send submit_sm, check SMPPClientManagerPB log file for details' ) else: self.stats.inc('success_count') self.stats.set('last_success_at', datetime.now()) self.log.debug('SubmitSmPDU sent to [cid:%s], result = %s', routedConnector.cid, c.result) response = {'return': c.result, 'status': 200} except Exception, e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally:
class SMPPClientSMListener(object): """ This is a listener object instantiated for every new SMPP connection, it is responsible of handling SubmitSm, DeliverSm and SubmitSm PDUs for a given SMPP connection """ def __init__(self, config, SMPPClientFactory, amqpBroker, redisClient, RouterPB=None, interceptorpb_client=None): self.config = config self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory( self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.RouterPB = RouterPB self.interceptorpb_client = interceptorpb_client self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.submit_retrials = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig( self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = TimedRotatingFileHandler(filename=self.config.log_file, when=self.config.log_rotate) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False def setSubmitSmQ(self, queue): self.log.debug('Setting a new submit_sm_q: %s', queue) self.submit_sm_q = queue def clearRejectTimer(self, msgid): if msgid in self.rejectTimers: timer = self.rejectTimers[msgid] if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearRejectTimers(self): for msgid, timer in self.rejectTimers.items(): if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearQosTimer(self): if self.qosTimer is not None and self.qosTimer.called is False: self.qosTimer.cancel() self.qosTimer = None def clearAllTimers(self): self.clearQosTimer() self.clearRejectTimers() @defer.inlineCallbacks def rejectAndRequeueMessage(self, message, delay=True): msgid = message.content.properties['message-id'] if delay: # Use configured requeue_delay or specific one if not isinstance(delay, bool): requeue_delay = delay else: requeue_delay = self.SMPPClientFactory.config.requeue_delay self.log.debug("Requeuing SubmitSmPDU[%s] in %s seconds", msgid, requeue_delay) # Requeue the message with a delay timer = reactor.callLater(requeue_delay, self.rejectMessage, message=message, requeue=1) # If any, clear timer before setting a new one self.clearRejectTimer(msgid) self.rejectTimers[msgid] = timer defer.returnValue(timer) else: self.log.debug("Requeuing SubmitSmPDU[%s] without delay", msgid) yield self.rejectMessage(message, requeue=1) @defer.inlineCallbacks def rejectMessage(self, message, requeue=0): yield self.amqpBroker.chan.basic_reject( delivery_tag=message.delivery_tag, requeue=requeue) @defer.inlineCallbacks def ackMessage(self, message): yield self.amqpBroker.chan.basic_ack(message.delivery_tag) @defer.inlineCallbacks def submit_sm_callback(self, message): """This callback is a queue listener it is called whenever a message was consumed from queue c.f. test_amqp.ConsumeTestCase for use cases """ msgid = None try: msgid = message.content.properties['message-id'] SubmitSmPDU = pickle.loads(message.content.body) self.submit_sm_q.get().addCallback( self.submit_sm_callback).addErrback(self.submit_sm_errback) self.log.debug( "Callbacked a submit_sm with a SubmitSmPDU[%s] (?): %s", msgid, SubmitSmPDU) # Update submit_sm retrial tracker if msgid in self.submit_retrials: self.submit_retrials[msgid] += 1 else: self.submit_retrials[msgid] = 1 if self.qos_last_submit_sm_at is None: self.qos_last_submit_sm_at = datetime(1970, 1, 1) if self.SMPPClientFactory.config.submit_sm_throughput > 0: # QoS throttling qos_throughput_second = 1 / float( self.SMPPClientFactory.config.submit_sm_throughput) qos_throughput_ysecond_td = timedelta( microseconds=qos_throughput_second * 1000000) qos_delay = datetime.now() - self.qos_last_submit_sm_at if qos_delay < qos_throughput_ysecond_td: qos_slow_down = float((qos_throughput_ysecond_td - qos_delay).microseconds) / 1000000 # We're faster than submit_sm_throughput, # slow down before taking a new message from the queue self.log.debug( "QoS: submit_sm_callback faster (%s) than throughput (%s), slowing down %ss (requeuing).", qos_delay, qos_throughput_ysecond_td, qos_slow_down) # Relaunch queue callbacking after qos_slow_down seconds # self.qosTimer = task.deferLater(reactor, qos_slow_down, self.submit_sm_q.get) # self.qosTimer.addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) # Requeue the message yield self.rejectAndRequeueMessage(message, delay=qos_slow_down) defer.returnValue(False) self.qos_last_submit_sm_at = datetime.now() # Verify if message is a SubmitSm PDU if isinstance(SubmitSmPDU, SubmitSM) is False: self.log.error( "Received object[%s] is not an instance of SubmitSm: discarding this unknown object from queue", msgid) yield self.rejectMessage(message) defer.returnValue(False) # If the message has expired in the queue if 'headers' in message.content.properties and 'expiration' in message.content.properties[ 'headers']: expiration_datetime = parser.parse( message.content.properties['headers']['expiration']) if expiration_datetime < datetime.now(): self.log.info( "Discarding expired message[%s]: expiration is %s", msgid, expiration_datetime) yield self.rejectMessage(message) defer.returnValue(False) # SMPP Client should be already connected if self.SMPPClientFactory.smpp is None: created_at = parser.parse( message.content.properties['headers']['created_at']) msgAge = datetime.now() - created_at if msgAge.seconds > self.config.submit_max_age_smppc_not_ready: self.log.error( "SMPPC [cid:%s] is not connected: Discarding (#%s) SubmitSmPDU[%s], over-aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, msgAge.seconds) yield self.rejectMessage(message) defer.returnValue(False) else: if self.config.submit_retrial_delay_smppc_not_ready: delay_str = ' with delay %s seconds' % self.config.submit_retrial_delay_smppc_not_ready else: delay_str = '' self.log.error( "SMPPC [cid:%s] is not connected: Requeuing (#%s) SubmitSmPDU[%s]%s, aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, delay_str, msgAge.seconds) yield self.rejectAndRequeueMessage( message, delay=self.config.submit_retrial_delay_smppc_not_ready) defer.returnValue(False) # SMPP Client should be already bound as transceiver or transmitter if self.SMPPClientFactory.smpp.isBound() is False: created_at = parser.parse( message.content.properties['headers']['created_at']) msgAge = datetime.now() - created_at if msgAge.seconds > self.config.submit_max_age_smppc_not_ready: self.log.error( "SMPPC [cid:%s] is not bound: Discarding (#%s) SubmitSmPDU[%s], over-aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, msgAge.seconds) yield self.rejectMessage(message) defer.returnValue(False) else: if self.config.submit_retrial_delay_smppc_not_ready: delay_str = ' with delay %s seconds' % self.config.submit_retrial_delay_smppc_not_ready else: delay_str = '' self.log.error( "SMPPC [cid:%s] is not bound: Requeuing (#%s) SubmitSmPDU[%s]%s, aged %s seconds.", self.SMPPClientFactory.config.id, self.submit_retrials[msgid], msgid, delay_str, msgAge) yield self.rejectAndRequeueMessage( message, delay=self.config.submit_retrial_delay_smppc_not_ready) defer.returnValue(False) # Finally: send the sms ! self.log.debug( "Sending SubmitSmPDU[%s] through SMPPClientFactory [cid:%s]", msgid, self.SMPPClientFactory.config.id) d = self.SMPPClientFactory.smpp.sendDataRequest(SubmitSmPDU) d.addCallback(self.submit_sm_resp_event, message) yield d except SMPPRequestTimoutError: self.log.error( "SubmitSmPDU[%s] request timed out through [cid:%s], message requeued.", msgid, self.SMPPClientFactory.config.id) self.rejectAndRequeueMessage(message) defer.returnValue(False) except LongSubmitSmTransactionError as e: self.log.error( "Long SubmitSmPDU[%s] error in [cid:%s], message requeued: %s", msgid, self.SMPPClientFactory.config.id, e.message) self.rejectAndRequeueMessage(message) defer.returnValue(False) except Exception as e: self.log.critical( "Rejecting SubmitSmPDU[%s] through [cid:%s] for an unknown error (%s): %s", msgid, self.SMPPClientFactory.config.id, type(e), e) self.rejectMessage(message) defer.returnValue(False) @defer.inlineCallbacks def submit_sm_resp_event(self, r, amqpMessage): msgid = amqpMessage.content.properties['message-id'] total_bill_amount = None will_be_retried = False try: submit_sm_resp_bill = pickle.loads( amqpMessage.content.properties['headers'] ['submit_sm_bill']).getSubmitSmRespBill() if r.response.status == CommandStatus.ESME_ROK: # No more retrials ! del self.submit_retrials[msgid] # Get bill information total_bill_amount = 0.0 if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts( ) > 0: total_bill_amount = submit_sm_resp_bill.getTotalAmounts() # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(r.request.params['esm_class'], 'gsmFeatures'): for gsmFeature in r.request.params[ 'esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break # What type of splitting ? splitMethod = None if 'sar_msg_ref_num' in r.request.params: splitMethod = 'sar' elif UDHI_INDICATOR_SET and r.request.params[ 'short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' # Concatenate short_message if splitMethod is not None: _pdu = r.request if splitMethod == 'sar': short_message = _pdu.params['short_message'] else: short_message = _pdu.params['short_message'][6:] while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu if splitMethod == 'sar': short_message += _pdu.params['short_message'] else: short_message += _pdu.params['short_message'][6:] # Increase bill amount for each submit_sm_resp if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts( ) > 0: total_bill_amount += submit_sm_resp_bill.getTotalAmounts( ) else: short_message = r.request.params['short_message'] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len( short_message) else: logged_content = '%r' % short_message self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [smpp-msgid:%s] [status:%s] [prio:%s] [dlr:%s] [validity:%s] \ [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, msgid, r.response.params['message_id'], r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], logged_content) else: # Message must be retried ? if str(r.response.status) in self.config.submit_error_retrial: retrial = self.config.submit_error_retrial[str( r.response.status)] # Still have some retries to go ? if self.submit_retrials[msgid] < retrial['count']: # Requeue the message for later redelivery yield self.rejectAndRequeueMessage( amqpMessage, delay=retrial['delay']) will_be_retried = True else: # Prevent this list from over-growing del self.submit_retrials[msgid] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len( r.request.params['short_message']) else: logged_content = '%r' % r.request.params['short_message'] # Log the message self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [status:ERROR/%s] [retry:%s] [prio:%s] [dlr:%s] [validity:%s] \ [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, msgid, r.response.status, will_be_retried, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], logged_content) # It is a final submit_sm_resp ! if not will_be_retried: # Cancel any mapped rejectTimer to this message # (in case this message was rejected in the past) self.clearRejectTimer(msgid) self.log.debug( "ACKing amqpMessage [%s] having routing_key [%s]", msgid, amqpMessage.routing_key) # ACK the message in queue, this will remove it from the queue yield self.ackMessage(amqpMessage) # Send DLR to DLRLookup if r.response.status == CommandStatus.ESME_ROK: dlr = DLR(pdu_type=r.response.id, msgid=msgid, status=r.response.status, smpp_msgid=r.response.params['message_id']) else: dlr = DLR(pdu_type=r.response.id, msgid=msgid, status=r.response.status) yield self.amqpBroker.publish(exchange='messaging', routing_key='dlr.submit_sm_resp', content=dlr) # Bill will be charged by bill_request.submit_sm_resp.UID queue consumer if total_bill_amount > 0: pubQueueName = 'bill_request.submit_sm_resp.%s' % submit_sm_resp_bill.user.uid content = SubmitSmRespBillContent(submit_sm_resp_bill.bid, submit_sm_resp_bill.user.uid, total_bill_amount) self.log.debug( "Requesting a SubmitSmRespBillContent from a bill [bid:%s] with routing_key[%s]: %s", submit_sm_resp_bill.bid, pubQueueName, total_bill_amount) yield self.amqpBroker.publish(exchange='billing', routing_key=pubQueueName, content=content) if self.config.publish_submit_sm_resp: # Send back submit_sm_resp to submit.sm.resp.CID queue # There's no actual listeners on this queue, it can be used to # track submit_sm_resp messages from a 3rd party app content = SubmitSmRespContent( r.response, msgid, pickleProtocol=self.pickleProtocol) self.log.debug( "Sending back SubmitSmRespContent[%s] with routing_key[%s]", msgid, amqpMessage.content.properties['reply-to']) yield self.amqpBroker.publish( exchange='messaging', routing_key=amqpMessage.content.properties['reply-to'], content=content) except Exception as e: self.log.error( '(%s) while handling submit_sm_resp pdu for msgid:%s: %s', type(e), msgid, e) else: if will_be_retried: defer.returnValue(False) def submit_sm_errback(self, error): """It appears that when closing a queue with the close() method it errbacks with a txamqp.queue.Closed exception, didn't find a clean way to stop consuming a queue without errbacking here so this is a workaround to make it clean, it can be considered as a @TODO requiring knowledge of the queue api behaviour """ if error.check(Closed) is None: # @todo: implement this errback # For info, this errback is called whenever: # - an error has occurred inside submit_sm_callback # - the qosTimer has been cancelled (self.clearQosTimer()) try: error.raiseException() except Exception as e: self.log.error("Error in submit_sm_errback (%s): %s", type(e), e) @defer.inlineCallbacks def concatDeliverSMs(self, HSetReturn, hashKey, splitMethod, total_segments, msg_ref_num, segment_seqnum): if HSetReturn == 0: self.log.warn( 'This hashKey %s already exists, will not reset it !', hashKey) return # @TODO: longDeliverSm part expiry must be configurable yield self.redisClient.expire(hashKey, 300) # This is the last part if segment_seqnum == total_segments: hvals = yield self.redisClient.hvals(hashKey) if len(hvals) != total_segments: self.log.warn( 'Received the last part (msg_ref_num:%s) and did not find all parts in redis, data lost !', msg_ref_num) return # Get PDUs pdus = {} for pickledValue in hvals: value = pickle.loads(pickledValue) pdus[value['segment_seqnum']] = value['pdu'] # Where is the message content to be found ? if 'short_message' in pdus[1].params: msg_content_key = 'short_message' elif 'message_payload' in pdus[1].params: msg_content_key = 'message_payload' else: self.log.warn( 'Cannot find message content in first pdu params: %s', pdus[1].params) return # Build concat_message_content concat_message_content = '' for i in range(total_segments): if splitMethod == 'sar': concat_message_content += pdus[i + 1].params[msg_content_key] else: concat_message_content += pdus[ i + 1].params[msg_content_key][6:] # Build the final pdu and return it back to deliver_sm_event pdu = pdus[1] # Take the first part as a base of work # 1. Remove message splitting information from pdu if splitMethod == 'sar': del pdu.params['sar_segment_seqnum'] del pdu.params['sar_total_segments'] del pdu.params['sar_msg_ref_num'] else: pdu.params['esm_class'] = None # 2. Set the new concat_message_content pdu.params[msg_content_key] = concat_message_content routable = RoutableDeliverSm( pdu, Connector(self.SMPPClientFactory.config.id)) yield self.deliver_sm_event_post_interception(routable=routable, smpp=None, concatenated=True) def code_dlr_msgid(self, pdu): """Code the dlr msg id accordingly to SMPPc's dlr_msg_id_bases value""" try: if isinstance(pdu, DeliverSM): if self.SMPPClientFactory.config.dlr_msg_id_bases == 1: ret = ('%x' % int(pdu.dlr['id'])).upper().lstrip('0') elif self.SMPPClientFactory.config.dlr_msg_id_bases == 2: ret = int(str(pdu.dlr['id']), 16) else: ret = str(pdu.dlr['id']).upper().lstrip('0') else: # TODO: code dlr for submit_sm_resp maybe ? TBC ret = str(pdu.dlr['id']).upper().lstrip('0') except Exception as e: self.log.error( 'code_dlr_msgid, cannot code msgid [%s] with dlr_msg_id_bases:%s', pdu.dlr['id'], self.SMPPClientFactory.config.dlr_msg_id_bases) self.log.error('code_dlr_msgid, error details: %s', e) ret = str(pdu.dlr['id']).upper().lstrip('0') self.log.debug('code_dlr_msgid: %s coded to %s', pdu.dlr['id'], ret) return ret def deliver_sm_event_interceptor(self, smpp, pdu): self.log.debug('Intercepting deliver_sm event in smppc %s', self.SMPPClientFactory.config.id) if self.RouterPB is None: self.log.error( '(deliver_sm_event_interceptor/%s) RouterPB not set: deliver_sm will not be routed', self.SMPPClientFactory.config.id) return # Prepare for interception # this is a temporary routable instance to be used in interception routable = RoutableDeliverSm( pdu, Connector(self.SMPPClientFactory.config.id)) # Interception inline # @TODO: make Interception in a thread, just like httpapi interception interceptor = self.RouterPB.getMOInterceptionTable().getInterceptorFor( routable) if interceptor is not None: self.log.debug( "RouterPB selected %s interceptor for this DeliverSmPDU", interceptor) if self.interceptorpb_client is None: smpp.factory.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: smpp.factory.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError( 'InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! d = self.interceptorpb_client.run_script(script, routable) d.addCallback(self.deliver_sm_event_post_interception, routable=routable, smpp=smpp) d.addErrback(self.deliver_sm_event_post_interception) return d else: return self.deliver_sm_event_post_interception(routable=routable, smpp=smpp) @defer.inlineCallbacks def deliver_sm_event_post_interception(self, *args, **kw): """This event is called whenever a deliver_sm pdu is received through a SMPPc It will hand the pdu to the router or a dlr thrower (depending if its a DLR or not). Note: this event will catch data_sm pdus as well """ try: # Control args if 'smpp' not in kw or 'routable' not in kw: self.log.error( 'deliver_sm_event_post_interception missing arguments after interception: %s', kw) raise InterceptorRunError( 'deliver_sm_event_post_interception missing arguments after interception' ) # Set defaults smpp = kw['smpp'] routable = kw['routable'] if 'concatenated' in kw: concatenated = kw['concatenated'] else: concatenated = False # Get message_content if 'short_message' in routable.pdu.params and len( routable.pdu.params['short_message']) > 0: message_content = routable.pdu.params['short_message'] elif 'message_payload' in routable.pdu.params: message_content = routable.pdu.params['message_payload'] elif 'short_message' in routable.pdu.params: message_content = routable.pdu.params['short_message'] else: message_content = None # Post interception: if len(args) == 1: if isinstance(args[0], bool) and not args[0]: smpp.factory.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got a False return.' ) raise InterceptorRunError( 'Failed running interception script, check log for details' ) elif isinstance(args[0], dict) and args[0]['smpp_status'] > 0: smpp.factory.stats.inc('interceptor_error_count') self.log.info( 'Interceptor script returned %s smpp_status error.', args[0]['smpp_status']) raise DeliverSmInterceptionError( code=args[0]['smpp_status']) elif isinstance(args[0], str): smpp.factory.stats.inc('interceptor_count') routable = pickle.loads(args[0]) else: smpp.factory.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', args[0]) raise InterceptorRunError( 'Failed running interception script, got the following return: %s' % args[0]) self.log.debug( 'Handling deliver_sm_event_post_interception event for smppc: %s', self.SMPPClientFactory.config.id) routable.pdu.dlr = self.SMPPOperationFactory.isDeliveryReceipt( routable.pdu) content = DeliverSmContent(routable, self.SMPPClientFactory.config.id, pickleProtocol=self.pickleProtocol, concatenated=concatenated) msgid = content.properties['message-id'] if routable.pdu.dlr is None: # We have a SMS-MO # UDH is set ? UDHI_INDICATOR_SET = False if 'esm_class' in routable.pdu.params and hasattr( routable.pdu.params['esm_class'], 'gsmFeatures'): for gsmFeature in routable.pdu.params[ 'esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break not_class2 = True if 'data_coding' in routable.pdu.params: dcs = routable.pdu.params['data_coding'] if (str(dcs.scheme) == 'GSM_MESSAGE_CLASS') and (dcs.schemeData is not None): not_class2 = (str(dcs.schemeData.msgClass) != 'CLASS_2') splitMethod = None # Is it a part of a long message ? if 'sar_msg_ref_num' in routable.pdu.params: splitMethod = 'sar' total_segments = routable.pdu.params['sar_total_segments'] segment_seqnum = routable.pdu.params['sar_segment_seqnum'] msg_ref_num = routable.pdu.params['sar_msg_ref_num'] self.log.debug( 'Received SMS-MO part [queue-msgid:%s] using SAR: ttl_segments=%s, segment_sn=%s, msgref=%s', msgid, total_segments, segment_seqnum, msg_ref_num) elif UDHI_INDICATOR_SET and not_class2 and message_content[: 3] == '\x05\x00\x03': splitMethod = 'udh' total_segments = struct.unpack('!B', message_content[4])[0] segment_seqnum = struct.unpack('!B', message_content[5])[0] msg_ref_num = struct.unpack('!B', message_content[3])[0] self.log.debug( 'Received SMS-MO part [queue-msgid:%s] using UDH: ttl_segments=%s, segment_sn=%s, msgref=%s', msgid, total_segments, segment_seqnum, msg_ref_num) if splitMethod is None: # It's a simple short message or a part of a concatenated message routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug( "Publishing DeliverSmContent[%s] with routing_key[%s]", msgid, routing_key) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # Get values from data_sm or deliver_sm priority_flag = None if 'priority_flag' in routable.pdu.params: priority_flag = routable.pdu.params['priority_flag'] validity_period = None if 'validity_period' in routable.pdu.params: validity_period = routable.pdu.params[ 'validity_period'] # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len( message_content) else: logged_content = '%r' % message_content self.log.info( "SMS-MO [cid:%s] [queue-msgid:%s] [status:%s] [prio:%s] [validity:%s] [from:%s] [to:%s] \ [content:%s]", self.SMPPClientFactory.config.id, msgid, routable.pdu.status, priority_flag, validity_period, routable.pdu.params['source_addr'], routable.pdu.params['destination_addr'], logged_content) else: # Long message part received if self.redisClient is None: self.log.critical( 'Invalid RC found while receiving part of long DeliverSm [queue-msgid:%s], MSG IS LOST !', msgid) else: # Save it to redis hashKey = "longDeliverSm:%s:%s:%s" % ( self.SMPPClientFactory.config.id, msg_ref_num, routable.pdu.params['destination_addr']) hashValues = { 'pdu': routable.pdu, 'total_segments': total_segments, 'msg_ref_num': msg_ref_num, 'segment_seqnum': segment_seqnum } yield self.redisClient.hset( hashKey, segment_seqnum, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.concatDeliverSMs, hashKey, splitMethod, total_segments, msg_ref_num, segment_seqnum) self.log.info( "DeliverSmContent[%s] is part of long msg of (%s), will be enqueued after concatenation.", msgid, total_segments) # Flag it as "will_be_concatenated" and publish it to router routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug( "Publishing DeliverSmContent[%s](flagged:wbc) with routing_key[%s]", msgid, routing_key) content.properties['headers'][ 'will_be_concatenated'] = True yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) else: # This is a DLR ! # Send DLR to DLRLookup yield self.amqpBroker.publish( exchange='messaging', routing_key='dlr.deliver_sm', content=DLR(pdu_type=routable.pdu.id, msgid=self.code_dlr_msgid(routable.pdu), status=routable.pdu.dlr['stat'], cid=self.SMPPClientFactory.config.id, dlr_details=routable.pdu.dlr)) except (InterceptorRunError, DeliverSmInterceptionError) as e: # Do not log text for privacy reasons # Added in #691 if self.config.log_privacy: logged_content = '** %s byte content **' % len(message_content) else: logged_content = '%r' % message_content self.log.info( "SMS-MO [cid:%s] [i-status:%s] [from:%s] [to:%s] [content:%s]", self.SMPPClientFactory.config.id, e.status, routable.pdu.params['source_addr'], routable.pdu.params['destination_addr'], logged_content) # Known exception handling defer.returnValue(DataHandlerResponse(status=e.status)) except Exception as e: # Unknown exception handling self.log.critical('Got an unknown exception (%s): %s', type(e), e) defer.returnValue( DataHandlerResponse(status=CommandStatus.ESME_RUNKNOWNERR))
class Send(Resource): isleaf = True def __init__(self, HTTPApiConfig, RouterPB, SMPPClientManagerPB, stats, log, interceptorpb_client): Resource.__init__(self) self.SMPPClientManagerPB = SMPPClientManagerPB self.RouterPB = RouterPB self.stats = stats self.log = log self.interceptorpb_client = interceptorpb_client # opFactory is initiated with a dummy SMPPClientConfig used for building SubmitSm only self.opFactory = SMPPOperationFactory( long_content_max_parts=HTTPApiConfig.long_content_max_parts, long_content_split=HTTPApiConfig.long_content_split) @defer.inlineCallbacks def route_routable(self, updated_request): try: # Do we have a hex-content ? if 'hex-content' not in updated_request.args: # Convert utf8 to GSM 03.38 if updated_request.args['coding'][0] == '0': short_message = gsm_encode( updated_request.args['content'][0].decode('utf-8')) else: # Otherwise forward it as is short_message = updated_request.args['content'][0] else: # Otherwise convert hex to bin short_message = hex2bin(updated_request.args['hex-content'][0]) # Authentication user = self.RouterPB.authenticateUser( username=updated_request.args['username'][0], password=updated_request.args['password'][0]) if user is None: self.stats.inc('auth_error_count') self.log.debug( "Authentication failure for username:%s and password:%s", updated_request.args['username'][0], updated_request.args['password'][0]) self.log.error("Authentication failure for username:%s", updated_request.args['username'][0]) raise AuthenticationError( 'Authentication failure for username:%s' % updated_request.args['username'][0]) # Update CnxStatus user.getCnxStatus().httpapi['connects_count'] += 1 user.getCnxStatus().httpapi['submit_sm_request_count'] += 1 user.getCnxStatus().httpapi['last_activity_at'] = datetime.now() # Build SubmitSmPDU SubmitSmPDU = self.opFactory.SubmitSM( source_addr=None if 'from' not in updated_request.args else updated_request.args['from'][0], destination_addr=updated_request.args['to'][0], short_message=short_message, data_coding=int(updated_request.args['coding'][0]), custom_tlvs=updated_request.args['custom_tlvs'][0]) self.log.debug("Built base SubmitSmPDU: %s", SubmitSmPDU) # Make Credential validation v = HttpAPICredentialValidator('Send', user, updated_request, submit_sm=SubmitSmPDU) v.validate() # Update SubmitSmPDU by default values from user MtMessagingCredential SubmitSmPDU = v.updatePDUWithUserDefaults(SubmitSmPDU) # Prepare for interception then routing routedConnector = None # init routable = RoutableSubmitSm(SubmitSmPDU, user) self.log.debug("Built Routable %s for SubmitSmPDU: %s", routable, SubmitSmPDU) # Should we tag the routable ? tags = [] if 'tags' in updated_request.args: tags = updated_request.args['tags'][0].split(',') for tag in tags: routable.addTag(tag) self.log.debug('Tagged routable %s: +%s', routable, tag) # Intercept interceptor = self.RouterPB.getMTInterceptionTable( ).getInterceptorFor(routable) if interceptor is not None: self.log.debug( "RouterPB selected %s interceptor for this SubmitSmPDU", interceptor) if self.interceptorpb_client is None: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not set !") raise InterceptorNotSetError('InterceptorPB not set !') if not self.interceptorpb_client.isConnected: self.stats.inc('interceptor_error_count') self.log.error("InterceptorPB not connected !") raise InterceptorNotConnectedError( 'InterceptorPB not connected !') script = interceptor.getScript() self.log.debug("Interceptor script loaded: %s", script) # Run ! r = yield self.interceptorpb_client.run_script( script, routable) if isinstance(r, dict) and r['http_status'] != 200: self.stats.inc('interceptor_error_count') self.log.error( 'Interceptor script returned %s http_status error.', r['http_status']) raise InterceptorRunError( code=r['http_status'], message='Interception specific error code %s' % r['http_status']) elif isinstance(r, str): self.stats.inc('interceptor_count') routable = pickle.loads(r) else: self.stats.inc('interceptor_error_count') self.log.error( 'Failed running interception script, got the following return: %s', r) raise InterceptorRunError( message= 'Failed running interception script, check log for details' ) # Get the route route = self.RouterPB.getMTRoutingTable().getRouteFor(routable) if route is None: self.stats.inc('route_error_count') self.log.error( "No route matched from user %s for SubmitSmPDU: %s", user, routable.pdu) raise RouteNotFoundError("No route found") # Get connector from selected route self.log.debug("RouterPB selected %s route for this SubmitSmPDU", route) routedConnector = route.getConnector() # Is it a failover route ? then check for a bound connector, otherwise don't route # The failover route requires at least one connector to be up, no message enqueuing will # occur otherwise. if repr(route) == 'FailoverMTRoute': self.log.debug( 'Selected route is a failover, will ensure connector is bound:' ) while True: c = self.SMPPClientManagerPB.perspective_connector_details( routedConnector.cid) if c: self.log.debug('Connector [%s] is: %s', routedConnector.cid, c['session_state']) else: self.log.debug('Connector [%s] is not found', routedConnector.cid) if c and c['session_state'][:6] == 'BOUND_': # Choose this connector break else: # Check next connector, None if no more connectors are available routedConnector = route.getConnector() if routedConnector is None: break if routedConnector is None: self.stats.inc('route_error_count') self.log.error( "Failover route has no bound connector to handle SubmitSmPDU: %s", routable.pdu) raise ConnectorNotFoundError( "Failover route has no bound connectors") # Re-update SubmitSmPDU with parameters from the route's connector connector_config = self.SMPPClientManagerPB.perspective_connector_config( routedConnector.cid) if connector_config: connector_config = pickle.loads(connector_config) routable = update_submit_sm_pdu(routable=routable, config=connector_config) # Set priority priority = 0 if 'priority' in updated_request.args: priority = int(updated_request.args['priority'][0]) routable.pdu.params['priority_flag'] = priority_flag_value_map[ priority] self.log.debug("SubmitSmPDU priority is set to %s", priority) # Set schedule_delivery_time if 'sdt' in updated_request.args: routable.pdu.params['schedule_delivery_time'] = parse( updated_request.args['sdt'][0]) self.log.debug( "SubmitSmPDU schedule_delivery_time is set to %s (%s)", routable.pdu.params['schedule_delivery_time'], updated_request.args['sdt'][0]) # Set validity_period if 'validity-period' in updated_request.args: delta = timedelta( minutes=int(updated_request.args['validity-period'][0])) routable.pdu.params['validity_period'] = datetime.today( ) + delta self.log.debug( "SubmitSmPDU validity_period is set to %s (+%s minutes)", routable.pdu.params['validity_period'], updated_request.args['validity-period'][0]) # Set DLR bit mask on the last pdu _last_pdu = routable.pdu while True: if hasattr(_last_pdu, 'nextPdu'): _last_pdu = _last_pdu.nextPdu else: break # DLR setting is clearly described in #107 _last_pdu.params['registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED) if updated_request.args['dlr'][0] == 'yes': _last_pdu.params['registered_delivery'] = RegisteredDelivery( RegisteredDeliveryReceipt.SMSC_DELIVERY_RECEIPT_REQUESTED) self.log.debug("SubmitSmPDU registered_delivery is set to %s", str(_last_pdu.params['registered_delivery'])) dlr_level = int(updated_request.args['dlr-level'][0]) if 'dlr-url' in updated_request.args: dlr_url = updated_request.args['dlr-url'][0] else: dlr_url = None if updated_request.args['dlr-level'][0] == '1': dlr_level_text = 'SMS-C' elif updated_request.args['dlr-level'][0] == '2': dlr_level_text = 'Terminal' else: dlr_level_text = 'All' dlr_method = updated_request.args['dlr-method'][0] else: dlr_url = None dlr_level = 0 dlr_level_text = 'No' dlr_method = None # QoS throttling if user.mt_credential.getQuota( 'http_throughput') >= 0 and user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] != 0: qos_throughput_second = 1 / float( user.mt_credential.getQuota('http_throughput')) qos_throughput_ysecond_td = timedelta( microseconds=qos_throughput_second * 1000000) qos_delay = datetime.now() - user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] if qos_delay < qos_throughput_ysecond_td: self.stats.inc('throughput_error_count') self.log.error( "QoS: submit_sm_event is faster (%s) than fixed throughput (%s), user:%s, rejecting message.", qos_delay, qos_throughput_ysecond_td, user) raise ThroughputExceededError("User throughput exceeded") user.getCnxStatus( ).httpapi['qos_last_submit_sm_at'] = datetime.now() # Get number of PDUs to be sent (for billing purpose) _pdu = routable.pdu submit_sm_count = 1 while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu submit_sm_count += 1 # Pre-sending submit_sm: Billing processing bill = route.getBillFor(user) self.log.debug( "SubmitSmBill [bid:%s] [ttlamounts:%s] generated for this SubmitSmPDU (x%s)", bill.bid, bill.getTotalAmounts(), submit_sm_count) charging_requirements = [] u_balance = user.mt_credential.getQuota('balance') u_subsm_count = user.mt_credential.getQuota('submit_sm_count') if u_balance is not None and bill.getTotalAmounts() > 0: # Ensure user have enough balance to pay submit_sm and submit_sm_resp charging_requirements.append({ 'condition': bill.getTotalAmounts() * submit_sm_count <= u_balance, 'error_message': 'Not enough balance (%s) for charging: %s' % (u_balance, bill.getTotalAmounts()) }) if u_subsm_count is not None: # Ensure user have enough submit_sm_count to to cover # the bill action (decrement_submit_sm_count) charging_requirements.append({ 'condition': bill.getAction('decrement_submit_sm_count') * submit_sm_count <= u_subsm_count, 'error_message': 'Not enough submit_sm_count (%s) for charging: %s' % (u_subsm_count, bill.getAction('decrement_submit_sm_count')) }) if self.RouterPB.chargeUserForSubmitSms( user, bill, submit_sm_count, charging_requirements) is None: self.stats.inc('charging_error_count') self.log.error( 'Charging user %s failed, [bid:%s] [ttlamounts:%s] SubmitSmPDU (x%s)', user, bill.bid, bill.getTotalAmounts(), submit_sm_count) raise ChargingError( 'Cannot charge submit_sm, check RouterPB log file for details' ) ######################################################## # Send SubmitSmPDU through smpp client manager PB server self.log.debug( "Connector '%s' is set to be a route for this SubmitSmPDU", routedConnector.cid) c = self.SMPPClientManagerPB.perspective_submit_sm( uid=user.uid, cid=routedConnector.cid, SubmitSmPDU=routable.pdu, submit_sm_bill=bill, priority=priority, pickled=False, dlr_url=dlr_url, dlr_level=dlr_level, dlr_method=dlr_method) # Build final response if not c.result: self.stats.inc('server_error_count') self.log.error('Failed to send SubmitSmPDU to [cid:%s]', routedConnector.cid) raise ServerError( 'Cannot send submit_sm, check SMPPClientManagerPB log file for details' ) else: self.stats.inc('success_count') self.stats.set('last_success_at', datetime.now()) self.log.debug('SubmitSmPDU sent to [cid:%s], result = %s', routedConnector.cid, c.result) response = {'return': c.result, 'status': 200} except Exception as e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} finally: self.log.debug("Returning %s to %s.", response, updated_request.getClientIP()) updated_request.setResponseCode(response['status']) # Default return _return = 'Error "%s"' % response['return'] # Success return if response['status'] == 200 and routedConnector is not None: self.log.info( 'SMS-MT [uid:%s] [cid:%s] [msgid:%s] [prio:%s] [dlr:%s] [from:%s] [to:%s] [content:%s]', user.uid, routedConnector.cid, response['return'], priority, dlr_level_text, routable.pdu.params['source_addr'], updated_request.args['to'][0], re.sub(r'[^\x20-\x7E]+', '.', short_message)) _return = 'Success "%s"' % response['return'] updated_request.write(_return) updated_request.finish() def render(self, request): """ /send request processing Note: This method MUST behave exactly like jasmin.protocols.smpp.factory.SMPPServerFactory.submit_sm_event """ self.log.debug("Rendering /send response with args: %s from %s", request.args, request.getClientIP()) request.responseHeaders.addRawHeader(b"content-type", b"text/plain") response = {'return': None, 'status': 200} self.stats.inc('request_count') self.stats.set('last_request_at', datetime.now()) # updated_request will be filled with default values where request will never get modified # updated_request is used for sending the SMS, request is just kept as an original request object updated_request = request try: # Validation (must have almost the same params as /rate service) fields = { 'to': { 'optional': False, 'pattern': re.compile(r'^\+{0,1}\d+$') }, 'from': { 'optional': True }, 'coding': { 'optional': True, 'pattern': re.compile(r'^(0|1|2|3|4|5|6|7|8|9|10|13|14){1}$') }, 'username': { 'optional': False, 'pattern': re.compile(r'^.{1,15}$') }, 'password': { 'optional': False, 'pattern': re.compile(r'^.{1,8}$') }, # Priority validation pattern can be validated/filtered further more # through HttpAPICredentialValidator 'priority': { 'optional': True, 'pattern': re.compile(r'^[0-3]$') }, 'sdt': { 'optional': True, 'pattern': re.compile( r'^\d{2}\d{2}\d{2}\d{2}\d{2}\d{2}\d{1}\d{2}(\+|-|R)$') }, # Validity period validation pattern can be validated/filtered further more # through HttpAPICredentialValidator 'validity-period': { 'optional': True, 'pattern': re.compile(r'^\d+$') }, 'dlr': { 'optional': False, 'pattern': re.compile(r'^(yes|no)$') }, 'dlr-url': { 'optional': True, 'pattern': re.compile(r'^(http|https)\://.*$') }, # DLR Level validation pattern can be validated/filtered further more # through HttpAPICredentialValidator 'dlr-level': { 'optional': True, 'pattern': re.compile(r'^[1-3]$') }, 'dlr-method': { 'optional': True, 'pattern': re.compile(r'^(get|post)$', re.IGNORECASE) }, 'tags': { 'optional': True, 'pattern': re.compile(r'^([-a-zA-Z0-9,])*$') }, 'content': { 'optional': True }, 'hex-content': { 'optional': True }, 'custom_tlvs': { 'optional': True } } if updated_request.getHeader('content-type') == 'application/json': json_body = updated_request.content.read() json_data = json.loads(json_body) for key, value in json_data.items(): # Make the values look like they came from form encoding all surrounded by [ ] if isinstance(value, unicode): value = value.encode() updated_request.args[key.encode()] = [value] # If no custom TLVs present, defaujlt to an [] which will be passed down to SubmitSM if 'custom_tlvs' not in updated_request.args: updated_request.args['custom_tlvs'] = [[]] # Default coding is 0 when not provided if 'coding' not in updated_request.args: updated_request.args['coding'] = ['0'] # Set default for undefined updated_request.arguments if 'dlr-url' in updated_request.args or 'dlr-level' in updated_request.args: updated_request.args['dlr'] = ['yes'] if 'dlr' not in updated_request.args: # Setting DLR updated_request to 'no' updated_request.args['dlr'] = ['no'] # Set default values if updated_request.args['dlr'][0] == 'yes': if 'dlr-level' not in updated_request.args: # If DLR is requested and no dlr-level were provided, assume minimum level (1) updated_request.args['dlr-level'] = [1] if 'dlr-method' not in updated_request.args: # If DLR is requested and no dlr-method were provided, assume default (POST) updated_request.args['dlr-method'] = ['POST'] # DLR method must be uppercase if 'dlr-method' in updated_request.args: updated_request.args['dlr-method'][0] = updated_request.args[ 'dlr-method'][0].upper() # Make validation v = UrlArgsValidator(updated_request, fields) v.validate() # Check if have content --OR-- hex-content # @TODO: make this inside UrlArgsValidator ! if 'content' not in request.args and 'hex-content' not in request.args: raise UrlArgsValidationError( "content or hex-content not present.") elif 'content' in request.args and 'hex-content' in request.args: raise UrlArgsValidationError( "content and hex-content cannot be used both in same request." ) # Continue routing in a separate thread reactor.callFromThread(self.route_routable, updated_request=updated_request) except Exception as e: self.log.error("Error: %s", e) if hasattr(e, 'code'): response = {'return': e.message, 'status': e.code} else: response = {'return': "Unknown error: %s" % e, 'status': 500} self.log.debug("Returning %s to %s.", response, updated_request.getClientIP()) updated_request.setResponseCode(response['status']) return 'Error "%s"' % response['return'] else: return NOT_DONE_YET
class SMPPClientSMListener: debug_it = {'rejectCount': 0} ''' This is a listener object instanciated for every new SMPP connection, it is responsible of handling SubmitSm, DeliverSm and SubmitSm PDUs for a given SMPP connection ''' def __init__(self, SMPPClientSMListenerConfig, SMPPClientFactory, amqpBroker, redisClient): self.config = SMPPClientSMListenerConfig self.SMPPClientFactory = SMPPClientFactory self.SMPPOperationFactory = SMPPOperationFactory( self.SMPPClientFactory.config) self.amqpBroker = amqpBroker self.redisClient = redisClient self.submit_sm_q = None self.qos_last_submit_sm_at = None self.rejectTimers = {} self.qosTimer = None # Set pickleProtocol self.pickleProtocol = SMPPClientPBConfig( self.config.config_file).pickle_protocol # Set up a dedicated logger self.log = logging.getLogger(LOG_CATEGORY) if len(self.log.handlers) != 1: self.log.setLevel(self.config.log_level) handler = logging.FileHandler(filename=self.config.log_file) formatter = logging.Formatter(self.config.log_format, self.config.log_date_format) handler.setFormatter(formatter) self.log.addHandler(handler) self.log.propagate = False def setSubmitSmQ(self, queue): self.log.debug('Setting a new submit_sm_q: %s' % queue) self.submit_sm_q = queue def clearRejectTimer(self, msgid): if msgid in self.rejectTimers: t = self.rejectTimers[msgid] if t.active(): t.cancel() del self.rejectTimers[msgid] def clearRejectTimers(self): for msgid, timer in self.rejectTimers.items(): if timer.active(): timer.cancel() del self.rejectTimers[msgid] def clearQosTimer(self): if self.qosTimer is not None and self.qosTimer.called == False: self.qosTimer.cancel() self.qosTimer = None def clearAllTimers(self): self.clearQosTimer() self.clearRejectTimers() @defer.inlineCallbacks def rejectAndRequeueMessage(self, message, delay=True): msgid = message.content.properties['message-id'] if delay != False: self.log.debug( "Requeuing SubmitSmPDU[%s] in %s seconds" % (msgid, self.SMPPClientFactory.config.requeue_delay)) # Use configured requeue_delay or specific one if delay is not bool: requeue_delay = delay else: requeue_delay = self.SMPPClientFactory.config.requeue_delay # Requeue the message with a delay t = reactor.callLater(requeue_delay, self.rejectMessage, message=message, requeue=1) # If any, clear timer before setting a new one self.clearRejectTimer(msgid) self.rejectTimers[msgid] = t defer.returnValue(t) else: self.log.debug("Requeuing SubmitSmPDU[%s] without delay" % msgid) yield self.rejectMessage(message, requeue=1) @defer.inlineCallbacks def rejectMessage(self, message, requeue=0): yield self.amqpBroker.chan.basic_reject( delivery_tag=message.delivery_tag, requeue=requeue) @defer.inlineCallbacks def ackMessage(self, message): yield self.amqpBroker.chan.basic_ack(message.delivery_tag) @defer.inlineCallbacks def setKeyExpiry(self, callbackArg, key, expiry): yield self.redisClient.expire(key, expiry) @defer.inlineCallbacks def submit_sm_callback(self, message): """This callback is a queue listener it is called whenever a message was consumed from queue c.f. test_amqp.ConsumeTestCase for use cases """ msgid = message.content.properties['message-id'] SubmitSmPDU = pickle.loads(message.content.body) self.submit_sm_q.get().addCallback(self.submit_sm_callback).addErrback( self.submit_sm_errback) self.log.debug( "Callbacked a submit_sm with a SubmitSmPDU[%s] (?): %s" % (msgid, SubmitSmPDU)) if self.qos_last_submit_sm_at is None: self.qos_last_submit_sm_at = datetime(1970, 1, 1) if self.SMPPClientFactory.config.submit_sm_throughput > 0: # QoS throttling qos_throughput_second = 1 / float( self.SMPPClientFactory.config.submit_sm_throughput) qos_throughput_ysecond_td = timedelta( microseconds=qos_throughput_second * 1000000) qos_delay = datetime.now() - self.qos_last_submit_sm_at if qos_delay < qos_throughput_ysecond_td: qos_slow_down = float((qos_throughput_ysecond_td - qos_delay).microseconds) / 1000000 # We're faster than submit_sm_throughput, slow down before taking a new message from the queue self.log.debug( "QoS: submit_sm_callback is faster (%s) than fixed throughput (%s), slowing down by %s seconds (message will be requeued)." % (qos_delay, qos_throughput_ysecond_td, qos_slow_down)) # Relaunch queue callbacking after qos_slow_down seconds #self.qosTimer = task.deferLater(reactor, qos_slow_down, self.submit_sm_q.get) #self.qosTimer.addCallback(self.submit_sm_callback).addErrback(self.submit_sm_errback) # Requeue the message yield self.rejectAndRequeueMessage(message, delay=qos_slow_down) defer.returnValue(False) self.qos_last_submit_sm_at = datetime.now() # Verify if message is a SubmitSm PDU if isinstance(SubmitSmPDU, SubmitSM) == False: self.log.error( "Received an object[%s] which is not an instance of SubmitSm: discarding this unkown object from the queue" % msgid) yield self.rejectMessage(message) defer.returnValue(False) # If the message has expired in the queue if 'headers' in message.content.properties and 'expiration' in message.content.properties[ 'headers']: expiration_datetime = parser.parse( message.content.properties['headers']['expiration']) if expiration_datetime < datetime.now(): self.log.info( "Discarding expired message[%s]: expiration is %s" % (msgid, expiration_datetime)) yield self.rejectMessage(message) defer.returnValue(False) # SMPP Client should be already connected if self.SMPPClientFactory.smpp == None: self.log.error( "SMPP Client is not connected: requeuing SubmitSmPDU[%s]" % msgid) yield self.rejectAndRequeueMessage(message) defer.returnValue(False) # SMPP Client should be already bound as transceiver or transmitter if self.SMPPClientFactory.smpp.isBound() == False: self.log.error( "SMPP Client is not bound: Requeuing SubmitSmPDU[%s]" % msgid) yield self.rejectAndRequeueMessage(message) defer.returnValue(False) self.log.debug("Sending SubmitSmPDU through SMPPClientFactory") yield self.SMPPClientFactory.smpp.sendDataRequest( SubmitSmPDU).addCallback(self.submit_sm_resp_event, message) @defer.inlineCallbacks def submit_sm_resp_event(self, r, amqpMessage): msgid = amqpMessage.content.properties['message-id'] total_bill_amount = None if ('headers' not in amqpMessage.content.properties or 'submit_sm_resp_bill' not in amqpMessage.content.properties['headers']): submit_sm_resp_bill = None else: submit_sm_resp_bill = pickle.loads( amqpMessage.content.properties['headers'] ['submit_sm_resp_bill']) if r.response.status == CommandStatus.ESME_ROK: # Get bill information total_bill_amount = 0.0 if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts( ) > 0: total_bill_amount = submit_sm_resp_bill.getTotalAmounts() # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(r.request.params['esm_class'], 'gsmFeatures'): for gsmFeature in r.request.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break # What type of splitting ? splitMethod = None if 'sar_msg_ref_num' in r.request.params: splitMethod = 'sar' elif UDHI_INDICATOR_SET and r.request.params[ 'short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' # Concatenate short_message if splitMethod is not None: _pdu = r.request if splitMethod == 'sar': short_message = _pdu.params['short_message'] else: short_message = _pdu.params['short_message'][6:] while hasattr(_pdu, 'nextPdu'): _pdu = _pdu.nextPdu if splitMethod == 'sar': short_message += _pdu.params['short_message'] else: short_message += _pdu.params['short_message'][6:] # Increase bill amount for each submit_sm_resp if submit_sm_resp_bill is not None and submit_sm_resp_bill.getTotalAmounts( ) > 0: total_bill_amount += submit_sm_resp_bill.getTotalAmounts( ) else: short_message = r.request.params['short_message'] self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [smpp-msgid:%s] [status:%s] [prio:%s] [dlr:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % (self.SMPPClientFactory.config.id, msgid, r.response.params['message_id'], r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], short_message)) else: self.log.info( "SMS-MT [cid:%s] [queue-msgid:%s] [status:ERROR/%s] [prio:%s] [dlr:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % (self.SMPPClientFactory.config.id, msgid, r.response.status, amqpMessage.content.properties['priority'], r.request.params['registered_delivery'].receipt, 'none' if ('headers' not in amqpMessage.content.properties or 'expiration' not in amqpMessage.content.properties['headers']) else amqpMessage.content.properties['headers']['expiration'], r.request.params['source_addr'], r.request.params['destination_addr'], r.request.params['short_message'])) # Cancel any mapped rejectTimer to this message (in case this message was rejected in the past) self.clearRejectTimer(msgid) self.log.debug("ACKing amqpMessage [%s] having routing_key [%s]", msgid, amqpMessage.routing_key) # ACK the message in queue, this will remove it from the queue yield self.ackMessage(amqpMessage) # Redis client is connected ? if self.redisClient is not None: # Check for HTTP DLR request from redis 'dlr' key # If there's a pending delivery receipt request then serve it # back by publishing a DLRContentForHttpapi to the messaging exchange pickledDlr = None pickledSmppsMap = None pickledDlr = yield self.redisClient.get("dlr:%s" % msgid) if pickledDlr is None: pickledSmppsMap = yield self.redisClient.get("smppsmap:%s" % msgid) if pickledDlr is not None: self.log.debug( 'There is a HTTP DLR request for msgid[%s] ...' % (msgid)) dlr = pickle.loads(pickledDlr) dlr_url = dlr['url'] dlr_level = dlr['level'] dlr_method = dlr['method'] dlr_expiry = dlr['expiry'] if dlr_level in [1, 3]: self.log.debug( 'Got DLR information for msgid[%s], url:%s, level:%s' % (msgid, dlr_url, dlr_level)) content = DLRContentForHttpapi( str(r.response.status), msgid, dlr_url, # The dlr_url in DLRContentForHttpapi indicates the level # of the actual delivery receipt (1) and not the requested # one (maybe 1 or 3) dlr_level=1, method=dlr_method) routing_key = 'dlr_thrower.http' self.log.debug( "Publishing DLRContentForHttpapi[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # DLR request is removed if: # - If level 1 is requested (SMSC level only) # - SubmitSmResp returned an error (no more delivery will be tracked) # # When level 3 is requested, the DLR will be removed when # receiving a deliver_sm (terminal receipt) if dlr_level == 1 or r.response.status != CommandStatus.ESME_ROK: self.log.debug('Removing DLR request for msgid[%s]' % msgid) yield self.redisClient.delete("dlr:%s" % msgid) else: self.log.debug( 'Terminal level receipt is requested, will not send any DLR receipt at this level.' ) if dlr_level in [2, 3]: # Map received submit_sm_resp's message_id to the msg for later rceipt handling self.log.debug( 'Mapping smpp msgid: %s to queue msgid: %s, expiring in %s' % (r.response.params['message_id'], msgid, dlr_expiry)) hashKey = "queue-msgid:%s" % r.response.params['message_id'] hashValues = { 'msgid': msgid, 'connector_type': 'httpapi', } self.redisClient.set( hashKey, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.setKeyExpiry, hashKey, dlr_expiry) elif pickledSmppsMap is not None: self.log.debug('There is a SMPPs mapping for msgid[%s] ...' % (msgid)) smpps_map = pickle.loads(pickledSmppsMap) system_id = smpps_map['system_id'] source_addr = smpps_map['source_addr'] destination_addr = smpps_map['destination_addr'] registered_delivery = smpps_map['registered_delivery'] smpps_map_expiry = smpps_map['expiry'] # Do we need to forward the receipt to the original sender ? if ((r.response.status == CommandStatus.ESME_ROK and str(registered_delivery.receipt) in [ 'SMSC_DELIVERY_RECEIPT_REQUESTED', 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE' ]) or (r.response.status != CommandStatus.ESME_ROK and str(registered_delivery.receipt) == 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE')): self.log.debug( 'Got DLR information for msgid[%s], registered_deliver%s, system_id:%s' % (msgid, registered_delivery, system_id)) content = DLRContentForSmpps(str(r.response.status), msgid, system_id, source_addr, destination_addr) routing_key = 'dlr_thrower.smpps' self.log.debug( "Publishing DLRContentForSmpps[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) # Map received submit_sm_resp's message_id to the msg for later rceipt handling self.log.debug( 'Mapping smpp msgid: %s to queue msgid: %s, expiring in %s' % (r.response.params['message_id'], msgid, smpps_map_expiry)) hashKey = "queue-msgid:%s" % r.response.params['message_id'] hashValues = { 'msgid': msgid, 'connector_type': 'smpps', } self.redisClient.set( hashKey, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.setKeyExpiry, hashKey, smpps_map_expiry) else: self.log.warn('No valid RC were found while checking msg[%s] !' % msgid) # Bill will be charged by bill_request.submit_sm_resp.UID queue consumer if total_bill_amount > 0: pubQueueName = 'bill_request.submit_sm_resp.%s' % submit_sm_resp_bill.user.uid content = SubmitSmRespBillContent(submit_sm_resp_bill.bid, submit_sm_resp_bill.user.uid, total_bill_amount) self.log.debug( "Requesting a SubmitSmRespBillContent from a bill [bid:%s] with routing_key[%s]: %s" % (submit_sm_resp_bill.bid, pubQueueName, total_bill_amount)) yield self.amqpBroker.publish(exchange='billing', routing_key=pubQueueName, content=content) if self.config.publish_submit_sm_resp: # Send back submit_sm_resp to submit.sm.resp.CID queue # There's no actual listeners on this queue, it can be used to # track submit_sm_resp messages from a 3rd party app content = SubmitSmRespContent(r.response, msgid, pickleProtocol=self.pickleProtocol) self.log.debug( "Sending back SubmitSmRespContent[%s] with routing_key[%s]" % (msgid, amqpMessage.content.properties['reply-to'])) yield self.amqpBroker.publish( exchange='messaging', routing_key=amqpMessage.content.properties['reply-to'], content=content) def submit_sm_errback(self, error): """It appears that when closing a queue with the close() method it errbacks with a txamqp.queue.Closed exception, didnt find a clean way to stop consuming a queue without errbacking here so this is a workaround to make it clean, it can be considered as a @TODO requiring knowledge of the queue api behaviour """ if error.check(Closed) == None: #@todo: implement this errback # For info, this errback is called whenever: # - an error has occured inside submit_sm_callback # - the qosTimer has been cancelled (self.clearQosTimer()) self.log.error("Error in submit_sm_errback: %s" % error.getErrorMessage()) @defer.inlineCallbacks def concatDeliverSMs(self, HSetReturn, splitMethod, total_segments, msg_ref_num, segment_seqnum): hashKey = "longDeliverSm:%s" % (msg_ref_num) if HSetReturn != 1: self.log.warn('Error (%s) when trying to set hashKey %s' % (HSetReturn, hashKey)) return # @TODO: longDeliverSm part expiry must be configurable yield self.redisClient.expire(hashKey, 300) # This is the last part if segment_seqnum == total_segments: hvals = yield self.redisClient.hvals(hashKey) if len(hvals) != total_segments: self.log.warn( 'Received the last part (msg_ref_num:%s) and did not find all parts in redis, data lost !' % msg_ref_num) return # Get PDUs pdus = {} for pickledValue in hvals: value = pickle.loads(pickledValue) pdus[value['segment_seqnum']] = value['pdu'] # Build short_message short_message = '' for i in range(total_segments): if splitMethod == 'sar': short_message += pdus[i + 1].params['short_message'] else: short_message += pdus[i + 1].params['short_message'][6:] # Build the final pdu and return it back to deliver_sm_event pdu = pdus[1] # Take the first part as a base of work # 1. Remove message splitting information from pdu if splitMethod == 'sar': del (pdu.params['sar_segment_seqnum']) del (pdu.params['sar_total_segments']) del (pdu.params['sar_msg_ref_num']) else: pdu.params['esm_class'] = None # 2. Set the new short_message pdu.params['short_message'] = short_message yield self.deliver_sm_event(smpp=None, pdu=pdu, concatenated=True) @defer.inlineCallbacks def deliver_sm_event(self, smpp, pdu, concatenated=False): """This event is called whenever a deliver_sm pdu is received through a SMPPc It will hand the pdu to the router or a dlr thrower (depending if its a DLR or not). """ pdu.dlr = self.SMPPOperationFactory.isDeliveryReceipt(pdu) content = DeliverSmContent(pdu, self.SMPPClientFactory.config.id, pickleProtocol=self.pickleProtocol, concatenated=concatenated) msgid = content.properties['message-id'] if pdu.dlr is None: # We have a SMS-MO # UDH is set ? UDHI_INDICATOR_SET = False if hasattr(pdu.params['esm_class'], 'gsmFeatures'): for gsmFeature in pdu.params['esm_class'].gsmFeatures: if str(gsmFeature) == 'UDHI_INDICATOR_SET': UDHI_INDICATOR_SET = True break splitMethod = None # Is it a part of a long message ? if 'sar_msg_ref_num' in pdu.params: splitMethod = 'sar' total_segments = pdu.params['sar_total_segments'] segment_seqnum = pdu.params['sar_segment_seqnum'] msg_ref_num = pdu.params['sar_msg_ref_num'] self.log.debug( 'Received a part of SMS-MO [queue-msgid:%s] using SAR options: total_segments=%s, segmen_seqnum=%s, msg_ref_num=%s' % (msgid, total_segments, segment_seqnum, msg_ref_num)) elif UDHI_INDICATOR_SET and pdu.params[ 'short_message'][:3] == '\x05\x00\x03': splitMethod = 'udh' total_segments = struct.unpack( '!B', pdu.params['short_message'][4])[0] segment_seqnum = struct.unpack( '!B', pdu.params['short_message'][5])[0] msg_ref_num = struct.unpack('!B', pdu.params['short_message'][3])[0] self.log.debug( 'Received a part of SMS-MO [queue-msgid:%s] using UDH options: total_segments=%s, segmen_seqnum=%s, msg_ref_num=%s' % (msgid, total_segments, segment_seqnum, msg_ref_num)) if splitMethod is None: # It's a simple short message or a part of a concatenated message routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug( "Publishing DeliverSmContent[%s] with routing_key[%s]" % (msgid, routing_key)) yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) self.log.info( "SMS-MO [cid:%s] [queue-msgid:%s] [status:%s] [prio:%s] [validity:%s] [from:%s] [to:%s] [content:%s]" % (self.SMPPClientFactory.config.id, msgid, pdu.status, pdu.params['priority_flag'], pdu.params['validity_period'], pdu.params['source_addr'], pdu.params['destination_addr'], pdu.params['short_message'])) else: # Long message part received if self.redisClient is None: self.warn( 'No valid RC were found while receiving a part of a long DeliverSm [queue-msgid:%s], MESSAGE IS LOST !' % msgid) # Save it to redis hashKey = "longDeliverSm:%s" % (msg_ref_num) hashValues = { 'pdu': pdu, 'total_segments': total_segments, 'msg_ref_num': msg_ref_num, 'segment_seqnum': segment_seqnum } self.redisClient.hset( hashKey, segment_seqnum, pickle.dumps(hashValues, self.pickleProtocol)).addCallback( self.concatDeliverSMs, splitMethod, total_segments, msg_ref_num, segment_seqnum) self.log.info( "DeliverSmContent[%s] is a part of a long message of %s parts, will be sent to queue after concatenation." % (msgid, total_segments)) # Flag it as "will_be_concatenated" and publish it to router routing_key = 'deliver.sm.%s' % self.SMPPClientFactory.config.id self.log.debug( "Publishing DeliverSmContent[%s](flagged:wbc) with routing_key[%s]" % (msgid, routing_key)) content.properties['headers']['will_be_concatenated'] = True yield self.amqpBroker.publish(exchange='messaging', routing_key=routing_key, content=content) else: # This is a DLR ! # Check for DLR request if self.redisClient is not None: q = yield self.redisClient.get("queue-msgid:%s" % pdu.dlr['id']) submit_sm_queue_id = None connector_type = None if q is not None: q = pickle.loads(q) submit_sm_queue_id = q['msgid'] connector_type = q['connector_type'] if submit_sm_queue_id is not None and connector_type == 'httpapi': pickledDlr = yield self.redisClient.get("dlr:%s" % submit_sm_queue_id) if pickledDlr is not None: dlr = pickle.loads(pickledDlr) dlr_url = dlr['url'] dlr_level = dlr['level'] dlr_method = dlr['method'] if dlr_level in [2, 3]: self.log.debug( 'Got DLR information for msgid[%s], url:%s, level:%s' % (submit_sm_queue_id, dlr_url, dlr_level)) content = DLRContentForHttpapi( pdu.dlr['stat'], submit_sm_queue_id, dlr_url, # The dlr_url in DLRContentForHttpapi indicates the level # of the actual delivery receipt (2) and not the # requested one (maybe 2 or 3) dlr_level=2, id_smsc=pdu.dlr['id'], sub=pdu.dlr['sub'], dlvrd=pdu.dlr['dlvrd'], subdate=pdu.dlr['sdate'], donedate=pdu.dlr['ddate'], err=pdu.dlr['err'], text=pdu.dlr['text'], method=dlr_method) routing_key = 'dlr_thrower.http' self.log.debug( "Publishing DLRContentForHttpapi[%s] with routing_key[%s]" % (submit_sm_queue_id, routing_key)) yield self.amqpBroker.publish( exchange='messaging', routing_key=routing_key, content=content) self.log.debug( 'Removing DLR request for msgid[%s]' % submit_sm_queue_id) yield self.redisClient.delete('dlr:%s' % submit_sm_queue_id) else: self.log.debug( 'SMS-C receipt is requested, will not send any DLR receipt at this level.' ) else: self.log.warn( 'Got invalid DLR information for msgid[%s], url:%s, level:%s' % (submit_sm_queue_id, dlr_url, dlr_level)) elif submit_sm_queue_id is not None and connector_type == 'smpps': pickledSmppsMap = yield self.redisClient.get( "smppsmap:%s" % submit_sm_queue_id) if pickledSmppsMap is not None: smpps_map = pickle.loads(pickledSmppsMap) system_id = smpps_map['system_id'] source_addr = smpps_map['source_addr'] destination_addr = smpps_map['destination_addr'] registered_delivery = smpps_map['registered_delivery'] smpps_map_expiry = smpps_map['expiry'] success_states = ['ACCEPTD', 'DELIVRD'] final_states = [ 'DELIVRD', 'EXPIRED', 'DELETED', 'UNDELIV', 'REJECTD' ] # Do we need to forward the receipt to the original sender ? if ((pdu.dlr['stat'] in success_states and str(registered_delivery.receipt) in [ 'SMSC_DELIVERY_RECEIPT_REQUESTED', 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE' ]) or (pdu.dlr['stat'] not in success_states and str(registered_delivery.receipt) == 'SMSC_DELIVERY_RECEIPT_REQUESTED_FOR_FAILURE')): self.log.debug( 'Got DLR information for msgid[%s], registered_deliver%s, system_id:%s' % (submit_sm_queue_id, registered_delivery, system_id)) content = DLRContentForSmpps( pdu.dlr['stat'], submit_sm_queue_id, system_id, source_addr, destination_addr) routing_key = 'dlr_thrower.smpps' self.log.debug( "Publishing DLRContentForSmpps[%s] with routing_key[%s]" % (submit_sm_queue_id, routing_key)) yield self.amqpBroker.publish( exchange='messaging', routing_key=routing_key, content=content) if pdu.dlr['stat'] in final_states: self.log.debug( 'Removing SMPPs map for msgid[%s]' % submit_sm_queue_id) yield self.redisClient.delete( 'smppsmap:%s' % submit_sm_queue_id) else: self.log.warn('Got a DLR for an unknown message id: %s' % pdu.dlr['id']) else: self.log.warn( 'DLR for msgid[%s] is not checked, no valid RC were found' % msgid) self.log.info( "DLR [cid:%s] [smpp-msgid:%s] [status:%s] [submit date:%s] [done date:%s] [submitted/delivered messages:%s/%s] [err:%s] [content:%s]" % ( self.SMPPClientFactory.config.id, pdu.dlr['id'], pdu.dlr['stat'], pdu.dlr['sdate'], pdu.dlr['ddate'], pdu.dlr['sub'], pdu.dlr['dlvrd'], pdu.dlr['err'], pdu.dlr['text'], ))