fix: 修复 CMPP 协议字段容量与版本兼容性
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-20 15:49:37 +08:00
parent b24cd7c08d
commit 001d5f2cbd
37 changed files with 1933 additions and 295 deletions
+22 -7
View File
@@ -15,6 +15,7 @@ package cmpp
import (
"errors"
"fmt"
"net"
"time"
)
@@ -72,7 +73,7 @@ func (cli *Client) Connect(servAddr, user, password string, timeout time.Duratio
}
var ok bool
var status uint8
var status uint32
if cli.typ == V20 || cli.typ == V21 {
var rsp *Cmpp2ConnRspPkt
rsp, ok = p.(*Cmpp2ConnRspPkt)
@@ -80,7 +81,7 @@ func (cli *Client) Connect(servAddr, user, password string, timeout time.Duratio
err = ErrRespNotMatch
return err
}
status = rsp.Status
status = uint32(rsp.Status)
} else {
var rsp *Cmpp3ConnRspPkt
rsp, ok = p.(*Cmpp3ConnRspPkt)
@@ -88,15 +89,16 @@ func (cli *Client) Connect(servAddr, user, password string, timeout time.Duratio
err = ErrRespNotMatch
return err
}
status = uint8(rsp.Status)
status = rsp.Status
}
if status != 0 {
if status <= ErrnoConnOthers { //ErrnoConnOthers = 5
err = ConnRspStatusErrMap[status]
if status <= uint32(ErrnoConnOthers) { //ErrnoConnOthers = 5
err = ConnRspStatusErrMap[uint8(status)]
} else {
err = ConnRspStatusErrMap[ErrnoConnOthers]
}
err = fmt.Errorf("CMPP CONNECT_RESP status=%d: %w", status, err)
return err
}
@@ -112,8 +114,21 @@ func (cli *Client) Disconnect() {
// SendReqPkt pack the cmpp request packet structure and send it to the other peer.
func (cli *Client) SendReqPkt(packet Packer) (uint32, error) {
seq := <-cli.conn.SeqId
return seq, cli.conn.SendPkt(packet, seq)
return cli.SendReqPktAvailable(packet, nil)
}
// The caller holds its pending-request lock until the returned sequence is registered.
// A wrapped sequence must never replace an outstanding request.
func (cli *Client) SendReqPktAvailable(packet Packer, available func(uint32) bool) (uint32, error) {
for {
seq, ok := <-cli.conn.SeqId
if !ok {
return 0, ErrConnIsClosed
}
if available == nil || available(seq) {
return seq, cli.conn.SendPkt(packet, seq)
}
}
}
// SendRspPkt pack the cmpp response packet structure and send it to the other peer.
+1 -1
View File
@@ -46,7 +46,7 @@ const (
CMPP_HEADER_LEN uint32 = 12
CMPP2_PACKET_MAX uint32 = 2477
CMPP2_PACKET_MIN uint32 = 12
CMPP3_PACKET_MAX uint32 = 3335
CMPP3_PACKET_MAX uint32 = Cmpp3SubmitReqPktMaxLen
CMPP3_PACKET_MIN uint32 = 12
)
@@ -0,0 +1,181 @@
package cmpp
import (
"encoding/binary"
"fmt"
"net"
"reflect"
"strings"
"testing"
"time"
)
func TestReceiptVersionLayout(t *testing.T) {
for _, version := range []Type{V20, V21, V30} {
width, size := 21, 60
if version == V30 {
width, size = 32, 71
}
original := CmppReceiptPkt{MsgId: ^uint64(0), Stat: "DELIVRD", SubmitTime: "2609201200", DoneTime: "2609201201", DestTerminalId: strings.Repeat("9", width), SmscSequence: ^uint32(0)}
raw, err := original.PackVersion(version)
if err != nil {
t.Fatal(err)
}
if len(raw) != size || binary.BigEndian.Uint32(raw[size-4:]) != ^uint32(0) {
t.Fatalf("wrong layout: %x", raw)
}
var decoded CmppReceiptPkt
if err = decoded.UnpackVersion(raw, version); err != nil || !reflect.DeepEqual(decoded, original) {
t.Fatalf("roundtrip: %+v %v", decoded, err)
}
for _, bad := range [][]byte{raw[:len(raw)-1], append(append([]byte{}, raw...), 0)} {
if decoded.UnpackVersion(bad, version) == nil {
t.Fatal("invalid length accepted")
}
}
other := V30
if version == V30 {
other = V20
}
if decoded.UnpackVersion(raw, other) == nil {
t.Fatal("wrong version accepted")
}
original.DestTerminalId += "1"
if _, err = original.PackVersion(version); err == nil {
t.Fatal("truncated destination")
}
}
}
// Real TCP peers exercise the bounded reader, not only Pack/Unpack in memory.
func tcpPair(t *testing.T, version Type) (*Conn, net.Conn) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
client, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
server, err := listener.Accept()
if err != nil {
t.Fatal(err)
}
conn := NewConn(server, version)
conn.SetState(CONN_AUTHOK)
t.Cleanup(func() { conn.Close(); client.Close() })
return conn, client
}
func TestCMPP3LargeSubmitAndMalformedPackets(t *testing.T) {
for _, length := range []int{140, 159} {
p := Cmpp3SubmitReqPkt{DestUsrTl: 99, DestTerminalId: make([]string, 99), MsgLength: uint8(length), MsgContent: strings.Repeat("x", length)}
if length == 140 {
p.MsgFmt = 8
}
raw, err := p.Pack(^uint32(0))
if err != nil {
t.Fatal(err)
}
if len(raw) != 3331+length {
t.Fatalf("size %d", len(raw))
}
conn, peer := tcpPair(t, V30)
go peer.Write(raw)
pkt, err := conn.RecvAndUnpackPkt(time.Second)
if err != nil {
t.Fatal(err)
}
decoded := pkt.(*Cmpp3SubmitReqPkt)
if decoded.SeqId != ^uint32(0) || len(decoded.DestTerminalId) != 99 || decoded.MsgContent != p.MsgContent {
t.Fatal("wire mismatch")
}
var d Cmpp3SubmitReqPkt
if d.Unpack(raw[8:len(raw)-1]) == nil || d.Unpack(append(raw[8:], 0)) == nil {
t.Fatal("malformed body accepted")
}
p.DestUsrTl = 100
p.DestTerminalId = append(p.DestTerminalId, "")
if _, err = p.Pack(0); err == nil {
t.Fatal("100 destinations accepted")
}
}
for _, size := range []uint32{0, 11, CMPP3_PACKET_MAX + 1, ^uint32(0)} {
conn, peer := tcpPair(t, V30)
raw := make([]byte, 4)
binary.BigEndian.PutUint32(raw, size)
go peer.Write(raw)
if _, err := conn.RecvAndUnpackPkt(time.Second); err == nil {
t.Fatalf("accepted length %d", size)
}
}
p := Cmpp3SubmitReqPkt{DestUsrTl: 1, DestTerminalId: []string{"1"}, MsgFmt: 8, MsgLength: 141, MsgContent: strings.Repeat("a", 141)}
if _, err := p.Pack(0); err == nil {
t.Fatal("oversized non-ASCII accepted")
}
p.MsgFmt, p.MsgLength, p.MsgContent = 0, 160, strings.Repeat("a", 160)
if _, err := p.Pack(0); err == nil {
t.Fatal("ASCII must be strictly shorter than 160 bytes")
}
}
func TestConnectStatusKeepsAll32Bits(t *testing.T) {
for _, status := range []uint32{0, 5, 255, 256, ^uint32(0)} {
t.Run(fmt.Sprint(status), func(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
done := make(chan error, 1)
go func() {
peer, e := listener.Accept()
if e != nil {
done <- e
return
}
conn := NewConn(peer, V30)
defer conn.Close()
conn.SetState(CONN_CONNECTED)
req, e := conn.RecvAndUnpackPkt(time.Second)
if e == nil {
e = conn.SendPkt(&Cmpp3ConnRspPkt{Status: status, Version: V30}, req.(*CmppConnReqPkt).SeqId)
}
done <- e
}()
client := NewClient(V30)
defer client.Disconnect()
err = client.Connect(listener.Addr().String(), "123456", "secret", time.Second)
if status == 0 && err != nil {
t.Fatal(err)
}
if status != 0 && (err == nil || !strings.Contains(err.Error(), fmt.Sprintf("status=%d", status))) {
t.Fatalf("status truncated: %v", err)
}
if e := <-done; e != nil {
t.Fatal(e)
}
})
}
}
func TestRequestSequenceSkipsInFlightAcrossWrap(t *testing.T) {
conn, peer := tcpPair(t, V30)
sequences := make(chan uint32, 3)
sequences <- ^uint32(0)
sequences <- 0
sequences <- 1
conn.SeqId = sequences
client := &Client{conn: conn, typ: V30}
read := make(chan error, 1)
go func() { raw := make([]byte, 12); _, err := peer.Read(raw); read <- err }()
seq, err := client.SendReqPktAvailable(&CmppActiveTestReqPkt{}, func(n uint32) bool { return n != ^uint32(0) })
if err != nil || seq != 0 {
t.Fatalf("zero lost: %d %v", seq, err)
}
if err = <-read; err != nil {
t.Fatal(err)
}
}
+41 -5
View File
@@ -13,11 +13,15 @@
package cmpp
import "encoding/binary"
import (
"encoding/binary"
"fmt"
)
// Packet length const for cmpp receipt packet.
const (
CmppReceiptPktLen uint32 = 60 //60d, 0x3c
Cmpp3ReceiptPktLen uint32 = 71
CmppReceiptPktLen uint32 = 60 //60d, 0x3c
)
type CmppReceiptPkt struct {
@@ -31,7 +35,18 @@ type CmppReceiptPkt struct {
// Pack packs the CmppReceiptPkt to bytes stream for client side.
func (p *CmppReceiptPkt) Pack() ([]byte, error) {
var pktLen uint32 = CmppReceiptPktLen
return p.PackVersion(V20)
}
// PackVersion uses the negotiated connection version, never a body-length guess.
func (p *CmppReceiptPkt) PackVersion(version Type) ([]byte, error) {
pktLen, width, err := receiptLayout(version)
if err != nil {
return nil, err
}
if len(p.Stat) > 7 || len(p.SubmitTime) > 10 || len(p.DoneTime) > 10 || len(p.DestTerminalId) > width {
return nil, fmt.Errorf("receipt field exceeds protocol width")
}
var w = newPacketWriter(pktLen)
@@ -39,7 +54,7 @@ func (p *CmppReceiptPkt) Pack() ([]byte, error) {
w.WriteFixedSizeString(p.Stat, 7)
w.WriteFixedSizeString(p.SubmitTime, 10)
w.WriteFixedSizeString(p.DoneTime, 10)
w.WriteFixedSizeString(p.DestTerminalId, 21)
w.WriteFixedSizeString(p.DestTerminalId, width)
w.WriteInt(binary.BigEndian, p.SmscSequence)
return w.Bytes()
@@ -49,6 +64,27 @@ func (p *CmppReceiptPkt) Pack() ([]byte, error) {
// After unpack, you will get all value of fields in
// CmppReceiptPkt struct.
func (p *CmppReceiptPkt) Unpack(data []byte) error {
return p.UnpackVersion(data, V20)
}
func receiptLayout(version Type) (uint32, int, error) {
switch version {
case V20, V21:
return CmppReceiptPktLen, 21, nil
case V30:
return Cmpp3ReceiptPktLen, 32, nil
}
return 0, 0, fmt.Errorf("unsupported receipt version: %v", version)
}
func (p *CmppReceiptPkt) UnpackVersion(data []byte, version Type) error {
size, width, err := receiptLayout(version)
if err != nil {
return err
}
if len(data) != int(size) {
return fmt.Errorf("invalid receipt length %d for version %v (expected %d)", len(data), version, size)
}
var r = newPacketReader(data)
r.ReadInt(binary.BigEndian, &p.MsgId)
@@ -62,7 +98,7 @@ func (p *CmppReceiptPkt) Unpack(data []byte) error {
doneTime := r.ReadCString(10)
p.DoneTime = string(doneTime)
destTerminalId := r.ReadCString(21)
destTerminalId := r.ReadCString(width)
p.DestTerminalId = string(destTerminalId)
r.ReadInt(binary.BigEndian, &p.SmscSequence)
+39 -2
View File
@@ -152,6 +152,9 @@ type Cmpp3SubmitRspPkt struct {
// Before calling Pack, you should initialize a Cmpp2SubmitReqPkt variable
// with correct field value.
func (p *Cmpp2SubmitReqPkt) Pack(seqId uint32) ([]byte, error) {
if err := validateSubmit(p.DestUsrTl, p.DestTerminalId, p.MsgFmt, p.MsgLength, p.MsgContent); err != nil {
return nil, err
}
var pktLen uint32 = CMPP_HEADER_LEN + 117 + uint32(p.DestUsrTl)*21 + 1 + uint32(p.MsgLength) + 8
var w = newPacketWriter(pktLen)
@@ -200,6 +203,8 @@ func (p *Cmpp2SubmitReqPkt) Pack(seqId uint32) ([]byte, error) {
// Usually it is used in server side. After unpack, you will get all value of fields in
// Cmpp2SubmitReqPkt struct.
func (p *Cmpp2SubmitReqPkt) Unpack(data []byte) error {
p.DestTerminalId = nil
var r = newPacketReader(data)
// Sequence Id
@@ -259,7 +264,13 @@ func (p *Cmpp2SubmitReqPkt) Unpack(data []byte) error {
reserve := r.ReadCString(8)
p.Reserve = string(reserve)
return r.Error()
if err := r.Error(); err != nil {
return err
}
if len(data) != 130+int(p.DestUsrTl)*21+int(p.MsgLength) {
return errSubmitInvalidStruct
}
return validateSubmit(p.DestUsrTl, p.DestTerminalId, p.MsgFmt, p.MsgLength, p.MsgContent)
}
// Pack packs the Cmpp2SubmitRspPkt to bytes stream for Server side.
@@ -302,6 +313,9 @@ func (p *Cmpp2SubmitRspPkt) Unpack(data []byte) error {
// Before calling Pack, you should initialize a Cmpp3SubmitReqPkt variable
// with correct field value.
func (p *Cmpp3SubmitReqPkt) Pack(seqId uint32) ([]byte, error) {
if err := validateSubmit(p.DestUsrTl, p.DestTerminalId, p.MsgFmt, p.MsgLength, p.MsgContent); err != nil {
return nil, err
}
var pktLen uint32 = CMPP_HEADER_LEN + 129 + uint32(p.DestUsrTl)*32 + 1 + 1 + uint32(p.MsgLength) + 20
var w = newPacketWriter(pktLen)
@@ -352,6 +366,8 @@ func (p *Cmpp3SubmitReqPkt) Pack(seqId uint32) ([]byte, error) {
// Usually it is used in server side. After unpack, you will get all value of fields in
// Cmpp3SubmitReqPkt struct.
func (p *Cmpp3SubmitReqPkt) Unpack(data []byte) error {
p.DestTerminalId = nil
var r = newPacketReader(data)
// Sequence Id
@@ -413,7 +429,13 @@ func (p *Cmpp3SubmitReqPkt) Unpack(data []byte) error {
linkId := r.ReadCString(20)
p.LinkId = string(linkId)
return r.Error()
if err := r.Error(); err != nil {
return err
}
if len(data) != 155+int(p.DestUsrTl)*32+int(p.MsgLength) {
return errSubmitInvalidStruct
}
return validateSubmit(p.DestUsrTl, p.DestTerminalId, p.MsgFmt, p.MsgLength, p.MsgContent)
}
// Pack packs the Cmpp3SubmitRspPkt to bytes stream for Server side.
@@ -451,3 +473,18 @@ func (p *Cmpp3SubmitRspPkt) Unpack(data []byte) error {
return r.Error()
}
// The receive buffer is bounded separately; validate counts before accepting the body.
func validateSubmit(count uint8, destinations []string, format, length uint8, content string) error {
if count == 0 || count > 99 || int(count) != len(destinations) {
return errSubmitInvalidStruct
}
limit := 140
if format == 0 {
limit = 159 // CMPP specifies ASCII <160 bytes; other formats <=140.
}
if int(length) != len(content) || len(content) > limit {
return errSubmitInvalidMsgLength
}
return nil
}