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
+138
View File
@@ -235,6 +235,144 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
}
}
func TestDecodeInboundLongMessageStripsConcatUDHBeforeUCS2Decode(t *testing.T) {
payload, err := cmpputils.Utf8ToUcs2("【深圳市合正物业服务有限公司】第一片正文")
if err != nil {
t.Fatalf("encode content: %v", err)
}
raw := append([]byte{0x05, 0x00, 0x03, 0x10, 0x02, 0x01}, []byte(payload)...)
content, fragment, err := decodeInboundSubmitContent(inboundSubmitPacket{
pkTotal: 2,
pkNumber: 1,
tpUdhi: 1,
msgFmt: 8,
msgContent: string(raw),
})
if err != nil {
t.Fatalf("decode long message: %v", err)
}
if content != "【深圳市合正物业服务有限公司】第一片正文" {
t.Fatalf("decoded content = %q", content)
}
if fragment == nil || fragment.Reference != 0x10 || fragment.Total != 2 || fragment.Index != 1 {
t.Fatalf("unexpected fragment metadata: %+v", fragment)
}
}
func TestDecodeInboundLongMessageSupports16BitConcatReference(t *testing.T) {
payload, err := cmpputils.Utf8ToUcs2("第二片正文")
if err != nil {
t.Fatalf("encode content: %v", err)
}
raw := append([]byte{0x06, 0x08, 0x04, 0x12, 0x34, 0x02, 0x02}, []byte(payload)...)
content, fragment, err := decodeInboundSubmitContent(inboundSubmitPacket{
pkTotal: 2,
pkNumber: 2,
tpUdhi: 1,
msgFmt: 8,
msgContent: string(raw),
})
if err != nil {
t.Fatalf("decode long message: %v", err)
}
if content != "第二片正文" {
t.Fatalf("decoded content = %q", content)
}
if fragment == nil || fragment.Reference != 0x1234 || fragment.Total != 2 || fragment.Index != 2 {
t.Fatalf("unexpected fragment metadata: %+v", fragment)
}
}
func TestInboundServerForwardsLongMessageFragmentsWithoutUDHAndAcknowledgesEachSubmit(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
account := "100001"
password := "secret-hash"
var mu sync.Mutex
submits := make([]submitRequest, 0, 2)
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/gateway/events/inbound/authenticate":
_ = json.NewEncoder(w).Encode(authResponse{PasswordCipher: password, Account: account, EnterpriseCode: account})
case "/api/gateway/events/inbound/submit":
var submit submitRequest
if err := json.NewDecoder(r.Body).Decode(&submit); err != nil {
t.Fatalf("decode submit: %v", err)
}
mu.Lock()
submits = append(submits, submit)
count := len(submits)
mu.Unlock()
status := "fragment_pending"
if count == 2 {
status = "accepted"
}
_ = json.NewEncoder(w).Encode(map[string]any{
"accepted": true, "messageId": "MSG-LONG-1", "status": status,
})
case "/api/gateway/events/downstream/pending":
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
case "/api/gateway/events/inbound/connection":
w.WriteHeader(http.StatusOK)
default:
t.Fatalf("unexpected api path: %s", r.URL.Path)
}
}))
defer api.Close()
addr := reserveTCPAddr(t)
go func() { _ = (Server{Addr: addr, APIBaseURL: api.URL + "/api"}).ListenAndServe() }()
time.Sleep(300 * time.Millisecond)
client := cmpp.NewClient(cmpp.V20)
defer client.Disconnect()
if err := client.Connect(addr, account, password, 2*time.Second); err != nil {
t.Fatalf("connect CMPP2 inbound: %v", err)
}
parts := []string{"【签名】第一片", "第二片正文"}
var firstResponseMsgID uint64
for index, text := range parts {
payload, _ := cmpputils.Utf8ToUcs2(text)
raw := append([]byte{0x05, 0x00, 0x03, 0x22, 0x02, byte(index + 1)}, []byte(payload)...)
if _, err := client.SendReqPkt(&cmpp.Cmpp2SubmitReqPkt{
PkTotal: 2, PkNumber: uint8(index + 1), TpUdhi: 1, RegisteredDelivery: 1, MsgLevel: 1,
ServiceId: "cmpp", FeeUserType: 2, FeeTerminalId: "13500002696",
MsgFmt: 8, MsgSrc: account, FeeType: "02", FeeCode: "0", SrcId: "10690000",
DestUsrTl: 1, DestTerminalId: []string{"13500002696"}, MsgLength: uint8(len(raw)), MsgContent: string(raw),
}); err != nil {
t.Fatalf("send long-message fragment %d: %v", index+1, err)
}
if rsp := recvSubmitRsp20(t, client); rsp.Result != 0 || rsp.MsgId == 0 {
t.Fatalf("unexpected fragment %d response: %+v", index+1, rsp)
} else if index == 0 {
firstResponseMsgID = rsp.MsgId
}
}
mu.Lock()
defer mu.Unlock()
if len(submits) != 2 {
t.Fatalf("submit API calls = %d, want 2", len(submits))
}
for index, submit := range submits {
if submit.Content != parts[index] {
t.Fatalf("fragment %d content = %q", index+1, submit.Content)
}
if submit.LongMessage == nil || submit.LongMessage.Reference != 0x22 ||
submit.LongMessage.Total != 2 || submit.LongMessage.Index != index+1 || submit.LongMessage.Format != 8 {
t.Fatalf("fragment %d metadata = %+v", index+1, submit.LongMessage)
}
}
downstreamRegistry.RLock()
session := downstreamRegistry.byMessageID["MSG-LONG-1"]
downstreamRegistry.RUnlock()
if session == nil || session.gatewayMsgID != firstResponseMsgID {
t.Fatalf("stored long-message Msg_Id = %v, want first fragment Msg_Id %v", session, firstResponseMsgID)
}
}
func TestSubmitResponsePrecedesQueuedFailureReceipt(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()