Files
lislgosms/gateway/internal/inbound/submit.go
T

334 lines
12 KiB
Go

package inbound
import (
"context"
"crypto/md5"
"errors"
"fmt"
cmpp "github.com/bigwhite/gocmpp"
cmpputils "github.com/bigwhite/gocmpp/utils"
"log"
"net"
"strings"
"sync"
"time"
)
// One client Submit may contain multiple destinations. The API result retains
// one internal message mapping per destination while CMPP receives one response.
type submitRequest struct {
Account string `json:"account"`
PhoneNumber string `json:"phoneNumber,omitempty"`
PhoneNumbers []string `json:"phoneNumbers,omitempty"`
Content string `json:"content"`
SrcID string `json:"srcId,omitempty"`
DestID string `json:"destId,omitempty"`
SequenceID uint32 `json:"sequenceId,omitempty"`
RemoteIP string `json:"remoteIp,omitempty"`
LongMessage *inboundLongMessageFragment `json:"longMessage,omitempty"`
}
type inboundLongMessageFragment struct {
Reference int `json:"reference"`
Total int `json:"total"`
Index int `json:"index"`
Format int `json:"format"`
}
type submitResponseMessage struct {
PhoneNumber string `json:"phoneNumber"`
MessageID string `json:"messageId"`
}
type submitResponse struct {
Accepted bool `json:"accepted"`
Result uint32 `json:"result,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
MessageID string `json:"messageId"`
Messages []submitResponseMessage `json:"messages,omitempty"`
}
func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) {
req, ok := normalizeInboundSubmit(packet.Packer)
if !ok {
return true, nil
}
session := findSessionByConn(packet.Conn)
if session == nil || strings.TrimSpace(session.account) == "" {
logger.Printf(
"cmpp inbound event=submit_rejected protocol=%s packet_type=%s remote=%s seq=%d result=9 stage=session reason=%q",
req.protocol, req.protocol, packet.Conn.Conn.RemoteAddr(), req.sequenceID, "authenticated connection session not found",
)
setInboundSubmitResponse(response.Packer, 0, 9)
response.AfterSend = s.submitResponseProtocolLogger("", req.protocol, req.sequenceID, "", "", 0, 9)
return false, nil
}
account := session.account
enterpriseCode := strings.TrimRight(req.msgSrc, "\x00")
if session.enterpriseCode != "" && enterpriseCode != session.enterpriseCode {
logger.Printf(
"cmpp inbound event=submit_rejected protocol=%s packet_type=%s account=%s enterprise_code=%s remote=%s seq=%d result=9 stage=protocol reason=%q",
defaultString(session.protocol, req.protocol), req.protocol, account, enterpriseCode, packet.Conn.Conn.RemoteAddr(), req.sequenceID,
fmt.Sprintf("enterprise code mismatch: expected %s", session.enterpriseCode),
)
setInboundSubmitResponse(response.Packer, 0, 9)
response.AfterSend = s.submitResponseProtocolLogger(account, defaultString(session.protocol, req.protocol), req.sequenceID, "", "", 0, 9)
return false, nil
}
phones := make([]string, len(req.destTerminalIDs))
for index, destination := range req.destTerminalIDs {
phones[index] = strings.TrimSpace(strings.TrimRight(destination, "\x00"))
}
phone := ""
if len(phones) > 0 {
phone = phones[0]
}
remote := packet.Conn.Conn.RemoteAddr()
clientProtocol := defaultString(session.protocol, req.protocol)
logger.Printf(
"cmpp inbound event=submit_received protocol=%s packet_type=%s account=%s enterprise_code=%s remote=%s seq=%d phone=%s src_id=%s msg_fmt=%d pk=%d/%d dest_count=%d content_bytes=%d",
clientProtocol, req.protocol, account, enterpriseCode, remote, req.sequenceID, phone, strings.TrimSpace(req.srcID), req.msgFmt,
req.pkNumber, req.pkTotal, len(req.destTerminalIDs), len(req.msgContent),
)
content, longMessage, err := decodeInboundSubmitContent(req)
if err != nil {
logger.Printf(
"cmpp inbound event=submit_rejected protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s result=9 stage=decode reason=%q",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, err,
)
setInboundSubmitResponse(response.Packer, 0, 9)
response.AfterSend = s.submitResponseProtocolLogger(account, clientProtocol, req.sequenceID, phone, "", 0, 9)
return false, nil
}
contentHash := fmt.Sprintf("%x", md5.Sum([]byte(content)))
startedAt := time.Now()
releaseSubmitBarrier := beginDownstreamSubmitBarrier(packet.Conn)
result, err := s.submit(remote, submitRequest{
Account: account,
PhoneNumber: phone,
PhoneNumbers: phones,
Content: content,
SrcID: req.srcID,
DestID: phone,
SequenceID: req.sequenceID,
RemoteIP: remoteIP(remote),
LongMessage: longMessage,
})
if err != nil || !result.Accepted {
reason := "api returned accepted=false"
if err != nil {
reason = err.Error()
}
responseResult := result.Result
if responseResult == 0 {
responseResult = 9
}
logger.Printf(
"cmpp inbound event=submit_rejected protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s result=%d stage=business duration_ms=%d content_chars=%d content_hash=%s reason=%q",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, responseResult, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash, reason,
)
setInboundSubmitResponse(response.Packer, 0, responseResult)
protocolLogger := s.submitResponseProtocolLogger(account, clientProtocol, req.sequenceID, phone, result.MessageID, 0, responseResult)
response.AfterSend = func(sendErr error) {
releaseSubmitBarrier()
protocolLogger(sendErr)
}
return false, nil
}
gatewayMsgID := messageIDFrom(result.MessageID, req.sequenceID)
setInboundSubmitResponse(response.Packer, gatewayMsgID, 0)
responseMessages := result.Messages
if len(responseMessages) == 0 {
responseMessages = []submitResponseMessage{{PhoneNumber: phone, MessageID: result.MessageID}}
}
for index, acceptedMessage := range responseMessages {
acceptedPhone := strings.TrimSpace(acceptedMessage.PhoneNumber)
if acceptedPhone == "" && index < len(phones) {
acceptedPhone = phones[index]
}
rememberDownstream(downstreamSession{
messageID: acceptedMessage.MessageID,
account: account,
tenantID: result.TenantID,
applicationID: result.ApplicationID,
enterpriseCode: session.enterpriseCode,
protocol: clientProtocol,
srcID: strings.TrimSpace(req.srcID),
phoneNumber: acceptedPhone,
gatewayMsgID: gatewayMsgID,
remoteIP: remoteIP(remote),
connectedAt: time.Now().UTC(),
connectionID: session.connectionID,
conn: packet.Conn,
mu: &sync.Mutex{},
presence: s.PresenceStore,
instanceID: s.gatewayInstanceID(),
report: session.report,
deliveryReport: session.deliveryReport,
protocolLog: session.protocolLog,
})
}
if current := findSessionByConn(packet.Conn); current != nil && current.report != nil {
go current.report(current, "submit", "")
}
response.AfterSend = func(sendErr error) {
releaseSubmitBarrier()
s.emitProtocolLog(protocolLogEvent{
Protocol: "cmpp",
Direction: "platform_to_client",
EventType: "submit_resp",
Status: protocolSendStatus(sendErr),
TenantID: result.TenantID,
ApplicationID: result.ApplicationID,
Account: account,
MessageID: result.MessageID,
GatewayMessageID: fmt.Sprint(gatewayMsgID),
Phone: phone,
ResultCode: protocolSendResultCode(sendErr, 0),
Detail: protocolSubmitResponseDetail(req.sequenceID, sendErr),
})
if sendErr != nil {
return
}
go func() {
if _, err := s.flushPending(account, logger); err != nil {
logger.Printf("cmpp inbound event=post_submit_pending_flush_failed account=%s message_id=%s error=%q", account, result.MessageID, err.Error())
}
}()
}
logger.Printf(
"cmpp inbound event=submit_accepted protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s dest_count=%d accepted_count=%d result=0 message_id=%s gateway_message_id=%d duration_ms=%d content_chars=%d content_hash=%s",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, len(phones), len(responseMessages), result.MessageID, gatewayMsgID, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash,
)
return false, nil
}
type inboundSubmitPacket struct {
protocol string
pkTotal uint8
pkNumber uint8
tpUdhi uint8
msgFmt uint8
msgSrc string
srcID string
destTerminalIDs []string
msgContent string
sequenceID uint32
}
func normalizeInboundSubmit(packet any) (inboundSubmitPacket, bool) {
switch req := packet.(type) {
case *cmpp.Cmpp2SubmitReqPkt:
return inboundSubmitPacket{
protocol: "cmpp20", pkTotal: req.PkTotal, pkNumber: req.PkNumber, tpUdhi: req.TpUdhi, msgFmt: req.MsgFmt,
msgSrc: req.MsgSrc, srcID: req.SrcId, destTerminalIDs: req.DestTerminalId,
msgContent: req.MsgContent, sequenceID: req.SeqId,
}, true
case *cmpp.Cmpp3SubmitReqPkt:
return inboundSubmitPacket{
protocol: "cmpp30", pkTotal: req.PkTotal, pkNumber: req.PkNumber, tpUdhi: req.TpUdhi, msgFmt: req.MsgFmt,
msgSrc: req.MsgSrc, srcID: req.SrcId, destTerminalIDs: req.DestTerminalId,
msgContent: req.MsgContent, sequenceID: req.SeqId,
}, true
default:
return inboundSubmitPacket{}, false
}
}
func setInboundSubmitResponse(packet any, messageID uint64, result uint32) {
switch resp := packet.(type) {
case *cmpp.Cmpp2SubmitRspPkt:
resp.MsgId = messageID
resp.Result = uint8(result)
case *cmpp.Cmpp3SubmitRspPkt:
resp.MsgId = messageID
resp.Result = result
}
}
func (s Server) submit(remote net.Addr, payload submitRequest) (submitResponse, error) {
payload.RemoteIP = remoteIP(remote)
var result submitResponse
err := s.post(context.Background(), "/gateway/events/inbound/submit", payload, &result)
return result, err
}
func decodeContent(format uint8, content string) (string, error) {
switch format {
case 8:
return cmpputils.Ucs2ToUtf8(content)
case 15:
return cmpputils.GB18030ToUtf8(content)
default:
return content, nil
}
}
func decodeInboundSubmitContent(req inboundSubmitPacket) (string, *inboundLongMessageFragment, error) {
raw := []byte(req.msgContent)
if req.tpUdhi == 0 && req.pkTotal <= 1 {
content, err := decodeContent(req.msgFmt, req.msgContent)
return content, nil, err
}
if len(raw) == 0 {
return "", nil, errors.New("UDH message content is empty")
}
headerLength := int(raw[0]) + 1
if headerLength > len(raw) {
return "", nil, fmt.Errorf("UDH length %d exceeds message content length %d", headerLength, len(raw))
}
var reference, total, index int
switch {
case len(raw) >= 6 && raw[0] == 0x05 && raw[1] == 0x00 && raw[2] == 0x03:
reference = int(raw[3])
total = int(raw[4])
index = int(raw[5])
case len(raw) >= 7 && raw[0] == 0x06 && raw[1] == 0x08 && raw[2] == 0x04:
reference = int(raw[3])<<8 | int(raw[4])
total = int(raw[5])
index = int(raw[6])
default:
if req.pkTotal > 1 {
return "", nil, errors.New("concatenated CMPP submit is missing a supported 8-bit or 16-bit UDH")
}
content, err := decodeContent(req.msgFmt, string(raw[headerLength:]))
return content, nil, err
}
if total < 2 || index < 1 || index > total {
return "", nil, fmt.Errorf("invalid concatenated UDH total/index %d/%d", index, total)
}
if req.pkTotal > 0 && int(req.pkTotal) != total {
return "", nil, fmt.Errorf("PkTotal %d does not match UDH total %d", req.pkTotal, total)
}
if req.pkNumber > 0 && int(req.pkNumber) != index {
return "", nil, fmt.Errorf("PkNumber %d does not match UDH index %d", req.pkNumber, index)
}
content, err := decodeContent(req.msgFmt, string(raw[headerLength:]))
if err != nil {
return "", nil, err
}
return content, &inboundLongMessageFragment{
Reference: reference,
Total: total,
Index: index,
Format: int(req.msgFmt),
}, nil
}
func messageIDFrom(value string, seq uint32) uint64 {
hash := md5.Sum([]byte(value))
result := uint64(seq)
for _, item := range hash[:6] {
result = (result << 8) + uint64(item)
}
if result == 0 {
return uint64(time.Now().UnixNano())
}
return result
}