fix: complete first version issue remediation

This commit is contained in:
hectorzhao
2026-07-11 10:14:37 +08:00
parent 709ac97764
commit 208a6c23f8
73 changed files with 1549 additions and 245 deletions
+127 -22
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
@@ -86,6 +87,7 @@ type DownstreamUplink struct {
type downstreamSession struct {
messageID string
account string
protocol string
srcID string
phoneNumber string
gatewayMsgID uint64
@@ -143,6 +145,7 @@ func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger
resp.AuthIsmg = string(authISMG[:])
session := downstreamSession{
account: strings.TrimSpace(defaultString(auth.Account, account)),
protocol: cmppVersionName(req.Version),
srcID: strings.TrimSpace(auth.Account),
remoteIP: remoteIP(packet.Conn.Conn.RemoteAddr()),
connectedAt: time.Now().UTC(),
@@ -153,59 +156,150 @@ func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger
}
rememberAccount(session)
go s.flushPending(defaultString(auth.Account, account), logger)
logger.Printf("cmpp inbound account=%s login ok remote=%s", account, packet.Conn.Conn.RemoteAddr())
logger.Printf(
"cmpp inbound event=login_accepted protocol=%s requested_version=0x%02x response_version=0x30 account=%s remote=%s",
cmppVersionName(req.Version), req.Version, account, packet.Conn.Conn.RemoteAddr(),
)
return false, nil
}
func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) {
req, ok := packet.Packer.(*cmpp.Cmpp3SubmitReqPkt)
req, ok := normalizeInboundSubmit(packet.Packer)
if !ok {
return true, nil
}
resp := response.Packer.(*cmpp.Cmpp3SubmitRspPkt)
account := strings.TrimRight(req.MsgSrc, "\x00")
account := strings.TrimRight(req.msgSrc, "\x00")
phone := ""
if len(req.DestTerminalId) > 0 {
phone = strings.TrimRight(req.DestTerminalId[0], "\x00")
if len(req.destTerminalIDs) > 0 {
phone = strings.TrimRight(req.destTerminalIDs[0], "\x00")
}
content, err := decodeContent(req.MsgFmt, req.MsgContent)
remote := packet.Conn.Conn.RemoteAddr()
clientProtocol := inboundClientProtocol(account, packet.Conn, req.protocol)
logger.Printf(
"cmpp inbound event=submit_received protocol=%s packet_type=%s account=%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, 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)
if err != nil {
logger.Printf("cmpp inbound decode submit failed account=%s seq=%d err=%v", account, req.SeqId, err)
resp.Result = 9
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)
return false, nil
}
result, err := s.submit(packet.Conn.Conn.RemoteAddr(), submitRequest{
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,
SrcID: req.srcID,
DestID: phone,
SequenceID: req.SeqId,
RemoteIP: remoteIP(packet.Conn.Conn.RemoteAddr()),
SequenceID: req.sequenceID,
RemoteIP: remoteIP(remote),
})
if err != nil || !result.Accepted {
logger.Printf("cmpp inbound submit rejected account=%s phone=%s seq=%d err=%v", account, phone, req.SeqId, err)
resp.Result = 9
reason := "api returned accepted=false"
if err != nil {
reason = err.Error()
}
logger.Printf(
"cmpp inbound event=submit_rejected protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s result=9 stage=business duration_ms=%d content_chars=%d content_hash=%s reason=%q",
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash, reason,
)
setInboundSubmitResponse(response.Packer, 0, 9)
return false, nil
}
resp.MsgId = messageIDFrom(result.MessageID, req.SeqId)
resp.Result = 0
gatewayMsgID := messageIDFrom(result.MessageID, req.sequenceID)
setInboundSubmitResponse(response.Packer, gatewayMsgID, 0)
rememberDownstream(downstreamSession{
messageID: result.MessageID,
account: account,
srcID: strings.TrimSpace(req.SrcId),
protocol: clientProtocol,
srcID: strings.TrimSpace(req.srcID),
phoneNumber: phone,
gatewayMsgID: resp.MsgId,
remoteIP: remoteIP(packet.Conn.Conn.RemoteAddr()),
gatewayMsgID: gatewayMsgID,
remoteIP: remoteIP(remote),
connectedAt: time.Now().UTC(),
conn: packet.Conn,
mu: &sync.Mutex{},
presence: s.PresenceStore,
instanceID: s.gatewayInstanceID(),
})
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,
)
return false, nil
}
type inboundSubmitPacket struct {
protocol string
pkTotal uint8
pkNumber 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, 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,
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 inboundClientProtocol(account string, conn *cmpp.Conn, fallback string) string {
downstreamRegistry.RLock()
defer downstreamRegistry.RUnlock()
session := downstreamRegistry.byAccount[account]
if session != nil && session.conn == conn && session.protocol != "" {
return session.protocol
}
return fallback
}
func cmppVersionName(version cmpp.Type) string {
switch version {
case cmpp.V20:
return "cmpp20"
case cmpp.V21:
return "cmpp21"
case cmpp.V30:
return "cmpp30"
default:
return fmt.Sprintf("unknown_0x%02x", uint8(version))
}
}
func (s Server) authenticate(remote net.Addr, account string, authSource string, timestamp uint32) (authResponse, error) {
payload := authRequest{
Account: account,
@@ -320,11 +414,22 @@ func (s Server) post(ctx context.Context, path string, payload any, result any)
return err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return fmt.Errorf("read api response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("api returned %s", resp.Status)
detail := strings.TrimSpace(string(responseBody))
if detail == "" {
return fmt.Errorf("api returned %s", resp.Status)
}
return fmt.Errorf("api returned %s: %s", resp.Status, detail)
}
if result != nil {
return json.NewDecoder(resp.Body).Decode(result)
if len(responseBody) == 0 {
return io.EOF
}
return json.Unmarshal(responseBody, result)
}
return nil
}
+64
View File
@@ -1,6 +1,7 @@
package inbound
import (
"bytes"
"context"
"encoding/json"
"log"
@@ -166,6 +167,69 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
}
}
func TestPostIncludesAPIErrorResponseBody(t *testing.T) {
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message":"CMPP submit content does not match an approved template and signature","statusCode":400}`))
}))
defer api.Close()
err := (Server{APIBaseURL: api.URL}).post(context.Background(), "/inbound/submit", map[string]string{"account": "100001"}, nil)
if err == nil || !bytes.Contains([]byte(err.Error()), []byte("CMPP submit content does not match an approved template and signature")) {
t.Fatalf("expected API response body in error, got %v", err)
}
}
func TestNormalizeInboundSubmitSupportsCMPP2AndCMPP3(t *testing.T) {
tests := []struct {
name string
packet any
protocol string
}{
{name: "cmpp2", packet: &cmpp.Cmpp2SubmitReqPkt{MsgSrc: "100001", SeqId: 20}, protocol: "cmpp20"},
{name: "cmpp3", packet: &cmpp.Cmpp3SubmitReqPkt{MsgSrc: "100001", SeqId: 30}, protocol: "cmpp30"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, ok := normalizeInboundSubmit(test.packet)
if !ok || got.protocol != test.protocol || got.msgSrc != "100001" {
t.Fatalf("unexpected normalized packet: %+v ok=%v", got, ok)
}
})
}
}
func TestSetInboundSubmitResponseSupportsCMPP2AndCMPP3(t *testing.T) {
cmpp2 := &cmpp.Cmpp2SubmitRspPkt{}
setInboundSubmitResponse(cmpp2, 101, 9)
if cmpp2.MsgId != 101 || cmpp2.Result != 9 {
t.Fatalf("unexpected CMPP2 response: %+v", cmpp2)
}
cmpp3 := &cmpp.Cmpp3SubmitRspPkt{}
setInboundSubmitResponse(cmpp3, 202, 9)
if cmpp3.MsgId != 202 || cmpp3.Result != 9 {
t.Fatalf("unexpected CMPP3 response: %+v", cmpp3)
}
}
func TestInboundClientProtocolUsesConnectRequestVersion(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
conn := &cmpp.Conn{}
downstreamRegistry.byAccount["100001"] = &downstreamSession{
account: "100001",
protocol: "cmpp20",
conn: conn,
}
if got := inboundClientProtocol("100001", conn, "cmpp30"); got != "cmpp20" {
t.Fatalf("protocol = %s, want cmpp20", got)
}
if got := inboundClientProtocol("missing", conn, "cmpp30"); got != "cmpp30" {
t.Fatalf("fallback protocol = %s, want cmpp30", got)
}
}
func TestFlushOnlineAccountsFetchesPendingDeliveries(t *testing.T) {
resetDownstreamRegistry()
defer resetDownstreamRegistry()
+1
View File
@@ -51,6 +51,7 @@ type Route struct {
type CMPP struct {
ServiceID string `json:"serviceId"`
SrcID string `json:"srcId"`
ExtensionDigits int `json:"extensionDigits"`
RegisteredDelivery int `json:"registeredDelivery"`
MsgFmt int `json:"msgFmt"`
FeeUserType int `json:"feeUserType,omitempty"`
+4
View File
@@ -31,6 +31,7 @@ func TestSubmitCommandUnmarshalsQueuePriority(t *testing.T) {
"cmpp": {
"serviceId": "SMS",
"srcId": "10690000",
"extensionDigits": 4,
"registeredDelivery": 1,
"msgFmt": 8
},
@@ -62,4 +63,7 @@ func TestSubmitCommandUnmarshalsQueuePriority(t *testing.T) {
if command.Upstream.DesiredConnections != 2 || command.Upstream.WindowSize != 16 {
t.Fatalf("unexpected upstream window config: %+v", command.Upstream)
}
if command.CMPP.ExtensionDigits != 4 {
t.Fatalf("unexpected extension digits: %d", command.CMPP.ExtensionDigits)
}
}