feat: harden CMPP delivery and platform workflows
This commit is contained in:
@@ -44,18 +44,25 @@ type authRequest struct {
|
||||
}
|
||||
|
||||
type submitRequest struct {
|
||||
Account string `json:"account"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type submitResponseMessage struct {
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
Content string `json:"content"`
|
||||
SrcID string `json:"srcId,omitempty"`
|
||||
DestID string `json:"destId,omitempty"`
|
||||
SequenceID uint32 `json:"sequenceId,omitempty"`
|
||||
RemoteIP string `json:"remoteIp,omitempty"`
|
||||
MessageID string `json:"messageId"`
|
||||
}
|
||||
|
||||
type submitResponse struct {
|
||||
Accepted bool `json:"accepted"`
|
||||
MessageID string `json:"messageId"`
|
||||
Accepted bool `json:"accepted"`
|
||||
MessageID string `json:"messageId"`
|
||||
Messages []submitResponseMessage `json:"messages,omitempty"`
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
@@ -68,17 +75,18 @@ type authResponse struct {
|
||||
}
|
||||
|
||||
type DownstreamReceipt struct {
|
||||
DeliveryID string `json:"deliveryId,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
ApplicationID string `json:"applicationId,omitempty"`
|
||||
MessageID string `json:"messageId"`
|
||||
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
|
||||
PhoneNumber string `json:"phoneNumber,omitempty"`
|
||||
ReceiptStatus string `json:"receiptStatus"`
|
||||
RawStatus string `json:"rawStatus,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
SubmitSequenceID uint32 `json:"submitSequenceId,omitempty"`
|
||||
DeliveredAt string `json:"deliveredAt,omitempty"`
|
||||
DeliveryID string `json:"deliveryId,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
ApplicationID string `json:"applicationId,omitempty"`
|
||||
MessageID string `json:"messageId"`
|
||||
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
|
||||
PhoneNumber string `json:"phoneNumber,omitempty"`
|
||||
ReceiptStatus string `json:"receiptStatus"`
|
||||
RawStatus string `json:"rawStatus,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
SubmitSequenceID uint32 `json:"submitSequenceId,omitempty"`
|
||||
SubmitGroupMessageID string `json:"submitGroupMessageId,omitempty"`
|
||||
DeliveredAt string `json:"deliveredAt,omitempty"`
|
||||
}
|
||||
|
||||
type DownstreamUplink struct {
|
||||
@@ -267,9 +275,13 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
|
||||
setInboundSubmitResponse(response.Packer, 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(req.destTerminalIDs) > 0 {
|
||||
phone = strings.TrimRight(req.destTerminalIDs[0], "\x00")
|
||||
if len(phones) > 0 {
|
||||
phone = phones[0]
|
||||
}
|
||||
remote := packet.Conn.Conn.RemoteAddr()
|
||||
clientProtocol := defaultString(session.protocol, req.protocol)
|
||||
@@ -290,13 +302,14 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
|
||||
contentHash := fmt.Sprintf("%x", md5.Sum([]byte(content)))
|
||||
startedAt := time.Now()
|
||||
result, err := s.submit(remote, submitRequest{
|
||||
Account: account,
|
||||
PhoneNumber: phone,
|
||||
Content: content,
|
||||
SrcID: req.srcID,
|
||||
DestID: phone,
|
||||
SequenceID: req.sequenceID,
|
||||
RemoteIP: remoteIP(remote),
|
||||
Account: account,
|
||||
PhoneNumber: phone,
|
||||
PhoneNumbers: phones,
|
||||
Content: content,
|
||||
SrcID: req.srcID,
|
||||
DestID: phone,
|
||||
SequenceID: req.sequenceID,
|
||||
RemoteIP: remoteIP(remote),
|
||||
})
|
||||
if err != nil || !result.Accepted {
|
||||
reason := "api returned accepted=false"
|
||||
@@ -312,24 +325,34 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
|
||||
}
|
||||
gatewayMsgID := messageIDFrom(result.MessageID, req.sequenceID)
|
||||
setInboundSubmitResponse(response.Packer, gatewayMsgID, 0)
|
||||
rememberDownstream(downstreamSession{
|
||||
messageID: result.MessageID,
|
||||
account: account,
|
||||
enterpriseCode: session.enterpriseCode,
|
||||
protocol: clientProtocol,
|
||||
srcID: strings.TrimSpace(req.srcID),
|
||||
phoneNumber: phone,
|
||||
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,
|
||||
})
|
||||
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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
if current := findSessionByConn(packet.Conn); current != nil && current.report != nil {
|
||||
go current.report(current, "submit", "")
|
||||
}
|
||||
@@ -344,8 +367,8 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
|
||||
}()
|
||||
}
|
||||
logger.Printf(
|
||||
"cmpp inbound event=submit_accepted protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s 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, result.MessageID, gatewayMsgID, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash,
|
||||
"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
|
||||
}
|
||||
@@ -742,9 +765,9 @@ func forgetDownstream(session *downstreamSession) {
|
||||
return
|
||||
}
|
||||
downstreamRegistry.Lock()
|
||||
if session.messageID != "" {
|
||||
if current := downstreamRegistry.byMessageID[session.messageID]; current == session {
|
||||
delete(downstreamRegistry.byMessageID, session.messageID)
|
||||
for messageID, current := range downstreamRegistry.byMessageID {
|
||||
if current != nil && current.conn == session.conn {
|
||||
delete(downstreamRegistry.byMessageID, messageID)
|
||||
}
|
||||
}
|
||||
if session.account != "" {
|
||||
@@ -1003,7 +1026,7 @@ func recoverReceiptSession(event DownstreamReceipt) *downstreamSession {
|
||||
}
|
||||
recovered := *accountSession
|
||||
recovered.messageID = event.MessageID
|
||||
recovered.gatewayMsgID = messageIDFrom(event.MessageID, event.SubmitSequenceID)
|
||||
recovered.gatewayMsgID = messageIDFrom(defaultString(event.SubmitGroupMessageID, event.MessageID), event.SubmitSequenceID)
|
||||
return &recovered
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -112,7 +113,13 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotSubmit); err != nil {
|
||||
t.Fatalf("decode submit: %v", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(submitResponse{Accepted: true, MessageID: "MSG-1"})
|
||||
_ = json.NewEncoder(w).Encode(submitResponse{
|
||||
Accepted: true, MessageID: "MSG-1",
|
||||
Messages: []submitResponseMessage{
|
||||
{PhoneNumber: "13500002696", MessageID: "MSG-1"},
|
||||
{PhoneNumber: "13600002696", MessageID: "MSG-2"},
|
||||
},
|
||||
})
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
|
||||
case "/api/gateway/events/inbound/connection":
|
||||
@@ -174,8 +181,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
FeeType: "02",
|
||||
FeeCode: "0",
|
||||
SrcId: "10690000",
|
||||
DestUsrTl: 1,
|
||||
DestTerminalId: []string{"13500002696"},
|
||||
DestUsrTl: 2,
|
||||
DestTerminalId: []string{"13500002696", "13600002696"},
|
||||
MsgLength: uint8(len(content)),
|
||||
MsgContent: content,
|
||||
})
|
||||
@@ -188,8 +195,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
}
|
||||
sendResult, err := PushReceiptWithResult(DownstreamReceipt{
|
||||
DeliveryID: "delivery-1",
|
||||
MessageID: "MSG-1",
|
||||
PhoneNumber: "13500002696",
|
||||
MessageID: "MSG-2",
|
||||
PhoneNumber: "13600002696",
|
||||
ReceiptStatus: "delivered",
|
||||
DeliveredAt: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
})
|
||||
@@ -204,7 +211,7 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
if err := receipt.Unpack([]byte(deliver.MsgContent)); err != nil {
|
||||
t.Fatalf("unpack pushed receipt: %v", err)
|
||||
}
|
||||
if receipt.Stat != "DELIVRD" || receipt.DestTerminalId != "13500002696" {
|
||||
if receipt.Stat != "DELIVRD" || receipt.DestTerminalId != "13600002696" || receipt.MsgId != rsp.MsgId {
|
||||
t.Fatalf("unexpected pushed receipt: %+v", receipt)
|
||||
}
|
||||
if err := client.SendRspPkt(&cmpp.Cmpp3DeliverRspPkt{MsgId: deliver.MsgId, Result: 0}, deliver.SeqId); err != nil {
|
||||
@@ -221,7 +228,8 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
if gotAuth.Account != account || gotAuth.AuthSource == "" || gotAuth.RemoteIP == "" {
|
||||
t.Fatalf("unexpected auth payload: %+v", gotAuth)
|
||||
}
|
||||
if gotSubmit.Account != account || gotSubmit.PhoneNumber != "13500002696" || gotSubmit.Content != "测试入站" {
|
||||
if gotSubmit.Account != account || gotSubmit.PhoneNumber != "13500002696" || gotSubmit.Content != "测试入站" ||
|
||||
!reflect.DeepEqual(gotSubmit.PhoneNumbers, []string{"13500002696", "13600002696"}) {
|
||||
t.Fatalf("unexpected submit payload: %+v", gotSubmit)
|
||||
}
|
||||
}
|
||||
@@ -742,6 +750,15 @@ func TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists(t *testi
|
||||
if recovered.gatewayMsgID != messageIDFrom("MSG-NOT-REMEMBERED", 1216579149) || recovered.gatewayMsgID == 0 {
|
||||
t.Fatalf("unexpected recovered Msg_Id: %d", recovered.gatewayMsgID)
|
||||
}
|
||||
first := recoverReceiptSession(DownstreamReceipt{
|
||||
MessageID: "MSG-FIRST", SubmitGroupMessageID: "MSG-GROUP", Account: "100001", SubmitSequenceID: 77,
|
||||
})
|
||||
second := recoverReceiptSession(DownstreamReceipt{
|
||||
MessageID: "MSG-SECOND", SubmitGroupMessageID: "MSG-GROUP", Account: "100001", SubmitSequenceID: 77,
|
||||
})
|
||||
if first == nil || second == nil || first.gatewayMsgID != second.gatewayMsgID || first.gatewayMsgID != messageIDFrom("MSG-GROUP", 77) {
|
||||
t.Fatalf("multi-destination recovery did not preserve the original Msg_Id: first=%+v second=%+v", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendDownstreamRejectsZeroMessageID(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user