fix: reassemble inbound CMPP long messages

This commit is contained in:
hectorzhao
2026-07-23 21:04:35 +08:00
parent f186aee00b
commit b29576fcd1
9 changed files with 1102 additions and 22 deletions
+82 -11
View File
@@ -44,14 +44,22 @@ type authRequest struct {
}
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"`
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 {
@@ -291,7 +299,7 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
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, err := decodeContent(req.msgFmt, 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",
@@ -311,6 +319,7 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
DestID: phone,
SequenceID: req.sequenceID,
RemoteIP: remoteIP(remote),
LongMessage: longMessage,
})
if err != nil || !result.Accepted {
reason := "api returned accepted=false"
@@ -465,6 +474,7 @@ type inboundSubmitPacket struct {
protocol string
pkTotal uint8
pkNumber uint8
tpUdhi uint8
msgFmt uint8
msgSrc string
srcID string
@@ -477,13 +487,13 @@ func normalizeInboundSubmit(packet any) (inboundSubmitPacket, bool) {
switch req := packet.(type) {
case *cmpp.Cmpp2SubmitReqPkt:
return inboundSubmitPacket{
protocol: "cmpp20", pkTotal: req.PkTotal, pkNumber: req.PkNumber, msgFmt: req.MsgFmt,
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, msgFmt: req.MsgFmt,
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
@@ -697,6 +707,60 @@ func decodeContent(format uint8, content string) (string, error) {
}
}
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 apiBaseURL(value string) string {
if value == "" {
return "http://127.0.0.1:3000/api"
@@ -733,6 +797,13 @@ func rememberDownstream(session downstreamSession) {
}
session.touchPresence("connected", true, false)
downstreamRegistry.Lock()
if existing := downstreamRegistry.byMessageID[session.messageID]; existing != nil && existing.conn == session.conn {
// A downstream long message returns one SUBMIT_RESP per fragment but is
// persisted as one platform message. Keep the first fragment Msg_Id so
// online delivery and restart recovery (which persists the first
// Sequence_Id) address the same client-side message.
session.gatewayMsgID = existing.gatewayMsgID
}
downstreamRegistry.byMessageID[session.messageID] = &session
downstreamRegistry.byConn[session.conn] = &session
if session.account != "" {