import hashlib import socket import struct import time from django.conf import settings from django.utils.translation import gettext_lazy as _ from common.exceptions import JMSException from common.utils import get_logger from .base import BaseSMSClient logger = get_logger(__file__) CMPP_CONNECT = 0x00000001 # 请求连接 CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答 CMPP_TERMINATE = 0x00000002 # 终止连接 CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答 CMPP_SUBMIT = 0x00000004 # 提交短信 CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答 CMPP_DELIVER = 0x00000005 # 短信下发 CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答 class CMPPBaseRequestInstance(object): def __init__(self): self.command_id = '' self.body = b'' self.length = 0 def get_header(self, sequence_id): length = struct.pack('!L', 12 + self.length) command_id = struct.pack('!L', self.command_id) sequence_id = struct.pack('!L', sequence_id) return length + command_id + sequence_id def get_message(self, sequence_id): return self.get_header(sequence_id) + self.body class CMPPConnectRequestInstance(CMPPBaseRequestInstance): def __init__(self, sp_id, sp_secret): if len(sp_id) != 6: raise ValueError(_("sp_id is 6 bits")) super().__init__() source_addr = sp_id.encode('utf-8') sp_secret = sp_secret.encode('utf-8') version = struct.pack('!B', 0x02) timestamp = struct.pack('!L', int(self.get_now())) authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8') auth_source_md5 = hashlib.md5(authenticator_source).digest() self.body = source_addr + auth_source_md5 + version + timestamp self.length = len(self.body) self.command_id = CMPP_CONNECT @staticmethod def get_now(): return time.strftime('%m%d%H%M%S', time.localtime(time.time())) class CMPPSubmitRequestInstance(CMPPBaseRequestInstance): def __init__(self, msg_src, dest_terminal_id, msg_content, src_id, service_id='', dest_usr_tl=1): if len(msg_content) >= 70: raise JMSException('The message length should be within 70 characters') if len(dest_terminal_id) > 100: raise JMSException('The number of users receiving information should be less than 100') super().__init__() msg_id = 8 * b'\x00' pk_total = struct.pack('!B', 1) pk_number = struct.pack('!B', 1) registered_delivery = struct.pack('!B', 0) msg_level = struct.pack('!B', 0) service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8') fee_user_type = struct.pack('!B', 2) fee_terminal_id = ('0' * 21).encode('utf-8') tp_pid = struct.pack('!B', 0) tp_udhi = struct.pack('!B', 0) msg_fmt = struct.pack('!B', 8) fee_type = '01'.encode('utf-8') fee_code = '000000'.encode('utf-8') valid_time = ('\x00' * 17).encode('utf-8') at_time = ('\x00' * 17).encode('utf-8') src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8') reserve = b'\x00' * 8 _msg_length = struct.pack('!B', len(msg_content) * 2) _msg_src = msg_src.encode('utf-8') _dest_usr_tl = struct.pack('!B', dest_usr_tl) _msg_content = msg_content.encode('utf-16-be') _dest_terminal_id = b''.join([ (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id ]) self.length = 126 + 21 * dest_usr_tl + len(_msg_content) self.command_id = CMPP_SUBMIT self.body = msg_id + pk_total + pk_number + registered_delivery \ + msg_level + service_id + fee_user_type + fee_terminal_id \ + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \ + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \ + _msg_length + _msg_content + reserve class CMPPTerminateRequestInstance(CMPPBaseRequestInstance): def __init__(self): super().__init__() self.body = b'' self.command_id = CMPP_TERMINATE class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance): def __init__(self, msg_id, result=0): super().__init__() msg_id = struct.pack('!Q', msg_id) result = struct.pack('!B', result) self.length = len(self.body) self.body = msg_id + result class CMPPResponseInstance(object): def __init__(self): self.command_id = None self.length = None self.response_handler_map = { CMPP_CONNECT_RESP: self.connect_response_parse, CMPP_SUBMIT_RESP: self.submit_response_parse, CMPP_DELIVER: self.deliver_request_parse, } @staticmethod def connect_response_parse(body): status, = struct.unpack('!B', body[0:1]) authenticator_ISMG = body[1:17] version, = struct.unpack('!B', body[17:18]) return { 'Status': status, 'AuthenticatorISMG': authenticator_ISMG, 'Version': version } @staticmethod def submit_response_parse(body): msg_id = body[:8] result = struct.unpack('!B', body[8:9]) return { 'Msg_Id': msg_id, 'Result': result[0] } @staticmethod def deliver_request_parse(body): msg_id, = struct.unpack('!Q', body[0:8]) dest_id = body[8:29] service_id = body[29:39] tp_pid = struct.unpack('!B', body[39:40]) tp_udhi = struct.unpack('!B', body[40:41]) msg_fmt = struct.unpack('!B', body[41:42]) src_terminal_id = body[42:63] registered_delivery = struct.unpack('!B', body[63:64]) msg_length = struct.unpack('!B', body[64:65]) msg_content = body[65:msg_length[0] + 65] return { 'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id, 'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt, 'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery, 'Msg_Length': msg_length, 'Msg_content': msg_content } def parse_header(self, data): self.command_id, = struct.unpack('!L', data[4:8]) sequence_id, = struct.unpack('!L', data[8:12]) return { 'length': self.length, 'command_id': hex(self.command_id), 'sequence_id': sequence_id } def parse_body(self, body): response_body_func = self.response_handler_map.get(self.command_id) if response_body_func is None: raise JMSException('Unable to parse the returned result: %s' % body) return response_body_func(body) def parse(self, data): self.length, = struct.unpack('!L', data[0:4]) header = self.parse_header(data) body = self.parse_body(data[12:self.length]) return header, body class CMPPClient(object): def __init__(self, host, port, sp_id, sp_secret, src_id, service_id): self.ip = host self.port = port self.sp_id = sp_id self.sp_secret = sp_secret self.src_id = src_id self.service_id = service_id self._sequence_id = 0 self._is_connect = False self._times = 3 self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._connect() @property def sequence_id(self): s = self._sequence_id self._sequence_id += 1 return s def _connect(self): self.__socket.settimeout(5) error_msg = _('Failed to connect to the CMPP gateway server, err: {}') for i in range(self._times): try: self.__socket.connect((self.ip, self.port)) except Exception as err: error_msg = error_msg.format(str(err)) logger.warning(error_msg) time.sleep(1) else: self._is_connect = True break else: raise JMSException(error_msg) def send(self, instance): if isinstance(instance, CMPPBaseRequestInstance): message = instance.get_message(sequence_id=self.sequence_id) else: message = instance self.__socket.send(message) def recv(self): raw_length = self.__socket.recv(4) length, = struct.unpack('!L', raw_length) header, body = CMPPResponseInstance().parse( raw_length + self.__socket.recv(length - 4) ) return header, body def close(self): if self._is_connect: terminate_request = CMPPTerminateRequestInstance() self.send(terminate_request) self.__socket.close() def _cmpp_connect(self): connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret) self.send(connect_request) header, body = self.recv() if body['Status'] != 0: raise JMSException('CMPPv2.0 authentication failed: %s' % body) def _cmpp_send_sms(self, dest, sign_name, template_code, template_param): """ 优先发送template_param中message的信息 若该内容不存在,则根据template_code构建验证码发送 """ message = template_param.get('message') if message is None: code = template_param.get('code') message = template_code.replace('{code}', code) msg = '【%s】 %s' % (sign_name, message) submit_request = CMPPSubmitRequestInstance( msg_src=self.sp_id, src_id=self.src_id, msg_content=msg, dest_usr_tl=len(dest), dest_terminal_id=dest, service_id=self.service_id ) self.send(submit_request) header, body = self.recv() command_id = header.get('command_id') if command_id == CMPP_DELIVER: deliver_request = CMPPDeliverRespRequestInstance( msg_id=body['Msg_Id'], result=body['Result'] ) self.send(deliver_request) def send_sms(self, dest, sign_name, template_code, template_param): try: self._cmpp_connect() self._cmpp_send_sms(dest, sign_name, template_code, template_param) except Exception as e: logger.error('CMPPv2.0 Error: %s', e) self.close() raise JMSException(e) class CMPP2SMS(BaseSMSClient): SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2' @classmethod def new_from_settings(cls): return cls( host=settings.CMPP2_HOST, port=settings.CMPP2_PORT, sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET, service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''), ) def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''): try: self.client = CMPPClient( host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id ) except Exception as err: self.client = None logger.warning(err) raise JMSException(err) @staticmethod def need_pre_check(): return False def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs): try: logger.info(f'CMPPv2.0 sms send: ' f'phone_numbers={phone_numbers} ' f'sign_name={sign_name} ' f'template_code={template_code} ' f'template_param={template_param}') self.client.send_sms(phone_numbers, sign_name, template_code, template_param) except Exception as e: raise JMSException(e) client = CMPP2SMS