396 lines
13 KiB
Go
396 lines
13 KiB
Go
package mtprotoedge
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"crypto/rsa"
|
||
"net"
|
||
"testing"
|
||
"time"
|
||
|
||
"go.uber.org/zap/zaptest"
|
||
|
||
"github.com/gotd/log/logzap"
|
||
"github.com/iamxvbaba/td/bin"
|
||
"github.com/iamxvbaba/td/crypto"
|
||
"github.com/iamxvbaba/td/exchange"
|
||
"github.com/iamxvbaba/td/proto"
|
||
"github.com/iamxvbaba/td/tg"
|
||
"github.com/iamxvbaba/td/tlprofile"
|
||
"github.com/iamxvbaba/td/transport"
|
||
)
|
||
|
||
// legacyCanonicalTestConn explicitly declares the canonical-only profile used
|
||
// by old connection-state tests. Production exact-path tests must instead call
|
||
// FreezeLayerProfile/SeedLayerProfile with protocol evidence.
|
||
func legacyCanonicalTestConn(t testing.TB, c *Conn) *Conn {
|
||
return legacyLayerWireTestConn(t, c, int(tlprofile.ProfileCanonical))
|
||
}
|
||
|
||
// legacyLayerWireTestConn preserves only the old tests' profile setup. It does
|
||
// not enable any wire conversion; application values still need an exact
|
||
// generated binding at the outbound boundary.
|
||
func legacyLayerWireTestConn(t testing.TB, c *Conn, layer int) *Conn {
|
||
t.Helper()
|
||
if c == nil {
|
||
t.Fatal("nil legacy exact-layer test Conn")
|
||
}
|
||
profile, ok := tlprofile.ResolveProfile(layer)
|
||
if !ok {
|
||
t.Fatalf("unsupported generated test Layer %d", layer)
|
||
}
|
||
if err := c.FreezeLayerProfile(profile); err != nil {
|
||
t.Fatalf("freeze generated test Layer %d: %v", layer, err)
|
||
}
|
||
c.setLegacyClientLayer(layer)
|
||
return c
|
||
}
|
||
|
||
// exactTestUpdatesEncoded gives transport/state-machine tests an explicit
|
||
// generated session binding without invoking the production fan-out cache.
|
||
// Tests which assert wire conversion use layerUpdatesFanout directly instead.
|
||
func exactTestUpdatesEncoded(t testing.TB, c *Conn, body []byte) *encodedOutboundMessage {
|
||
t.Helper()
|
||
if c == nil {
|
||
t.Fatal("nil exact test Conn")
|
||
}
|
||
state := c.LayerProfileState()
|
||
if state.Origin == LayerProfileUnknown {
|
||
t.Fatal("exact test Conn has no generated Layer profile")
|
||
}
|
||
return &encodedOutboundMessage{
|
||
body: append([]byte(nil), body...),
|
||
typeID: tg.UpdatesTooLongTypeID,
|
||
layer: &outboundLayerBinding{
|
||
profile: state.Profile,
|
||
epoch: state.Epoch,
|
||
},
|
||
}
|
||
}
|
||
|
||
func exactTestUpdatesTooLong(t testing.TB, c *Conn) *encodedOutboundMessage {
|
||
t.Helper()
|
||
var body bin.Buffer
|
||
if err := (&tg.UpdatesTooLong{}).Encode(&body); err != nil {
|
||
t.Fatalf("encode exact test updatesTooLong: %v", err)
|
||
}
|
||
return exactTestUpdatesEncoded(t, c, body.Raw())
|
||
}
|
||
|
||
// opaqueExactTestRPCResult is an explicit request-bound capability for tests
|
||
// of compression, retention and delivery mechanics. Semantic result conversion
|
||
// is covered by generated dispatcher tests; no production path constructs it.
|
||
type opaqueExactTestRPCResult struct{ result bin.Encoder }
|
||
|
||
func (r *opaqueExactTestRPCResult) Encode(b *bin.Buffer) error { return r.result.Encode(b) }
|
||
|
||
func (r *opaqueExactTestRPCResult) exactLayerRPCResultBinding() outboundLayerBinding {
|
||
return outboundLayerBinding{
|
||
profile: tlprofile.ProfileCanonical,
|
||
kind: outboundLayerBindingRequest,
|
||
}
|
||
}
|
||
|
||
func exactTestRPCResult(result bin.Encoder) bin.Encoder {
|
||
if result == nil || isLayerInvariantRPCResultEncoder(result) {
|
||
return result
|
||
}
|
||
if _, ok := result.(exactLayerRPCResultEncoder); ok {
|
||
return result
|
||
}
|
||
return &opaqueExactTestRPCResult{result: result}
|
||
}
|
||
|
||
// startTestServer 生成 RSA key、监听随机端口并启动 Server,返回监听地址与公钥。
|
||
// 通过 t.Cleanup 自动取消并校验优雅退出。opts 的 RSAKey/Logger/DC 会被补默认。
|
||
func startTestServer(t *testing.T, opts Options) (addr string, pub exchange.PublicKey, srv *Server) {
|
||
t.Helper()
|
||
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
if err != nil {
|
||
t.Fatalf("gen rsa: %v", err)
|
||
}
|
||
opts.RSAKey = rsaKey
|
||
if opts.Logger == nil {
|
||
opts.Logger = zaptest.NewLogger(t)
|
||
}
|
||
if opts.DC == 0 {
|
||
opts.DC = 2
|
||
}
|
||
|
||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
t.Fatalf("listen: %v", err)
|
||
}
|
||
|
||
srv = New(opts)
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
serveErr := make(chan error, 1)
|
||
go func() { serveErr <- srv.Serve(ctx, ln) }()
|
||
t.Cleanup(func() {
|
||
cancel()
|
||
select {
|
||
case err := <-serveErr:
|
||
if err != nil {
|
||
t.Errorf("serve: %v", err)
|
||
}
|
||
case <-time.After(5 * time.Second):
|
||
t.Error("server did not stop after ctx cancel")
|
||
}
|
||
})
|
||
|
||
return ln.Addr().String(), exchange.PublicKey{RSA: &rsaKey.PublicKey}, srv
|
||
}
|
||
|
||
// dialHandshake 建立 TCP 连接、完成 intermediate 协商与 MTProto 密钥交换,
|
||
// 返回连接、握手结果与 client 端 cipher。连接通过 t.Cleanup 自动关闭。
|
||
func dialHandshake(t *testing.T, addr string, dc int, pub exchange.PublicKey) (transport.Conn, exchange.ClientExchangeResult, crypto.Cipher) {
|
||
t.Helper()
|
||
conn := dialTransportOnly(t, addr)
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
auth, err := exchange.NewExchanger(conn, dc).
|
||
WithRand(rand.Reader).
|
||
WithLogger(logzap.New(zaptest.NewLogger(t).Named("client"))).
|
||
Client([]exchange.PublicKey{pub}).
|
||
Run(ctx)
|
||
if err != nil {
|
||
t.Fatalf("client exchange: %v", err)
|
||
}
|
||
return conn, auth, crypto.NewClientCipher(rand.Reader)
|
||
}
|
||
|
||
func dialTransportOnly(t *testing.T, addr string) transport.Conn {
|
||
t.Helper()
|
||
raw, err := net.Dial("tcp", addr)
|
||
if err != nil {
|
||
t.Fatalf("dial: %v", err)
|
||
}
|
||
conn, err := transport.Intermediate.Handshake(raw)
|
||
if err != nil {
|
||
_ = raw.Close()
|
||
t.Fatalf("transport handshake: %v", err)
|
||
}
|
||
t.Cleanup(func() { _ = conn.Close() })
|
||
return conn
|
||
}
|
||
|
||
// freezeActiveTestSessionProfile gives low-level transport fixtures the exact
|
||
// profile that a production invokeWithLayer admission would have proven. It is
|
||
// intentionally explicit: handshake/new_session_created alone never implies a
|
||
// TL Layer, and production push code must keep failing closed in that state.
|
||
func freezeActiveTestSessionProfile(t *testing.T, sessions *SessionManager, authKeyID [8]byte, sessionID int64, profile tlprofile.Profile) {
|
||
t.Helper()
|
||
if sessions == nil {
|
||
t.Fatal("freeze test session profile on nil SessionManager")
|
||
}
|
||
key := sessionKey{authKeyID: authKeyID, sessionID: sessionID}
|
||
sessions.mu.RLock()
|
||
c := sessions.bySession[key]
|
||
sessions.mu.RUnlock()
|
||
if c == nil {
|
||
t.Fatalf("active test session %x/%d is missing", authKeyID, sessionID)
|
||
}
|
||
if err := c.FreezeLayerProfile(profile); err != nil {
|
||
t.Fatalf("freeze active test session profile %d: %v", profile, err)
|
||
}
|
||
}
|
||
|
||
// sendEncrypted 用 client cipher 加密并发送一条带 msgID 的消息。
|
||
func sendEncrypted(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, msgID int64, msg bin.Encoder) {
|
||
t.Helper()
|
||
sendEncryptedWithSalt(t, conn, cipher, auth, auth.ServerSalt, msgID, msg)
|
||
}
|
||
|
||
// sendEncryptedWithSalt 用指定 salt 加密并发送一条消息。
|
||
func sendEncryptedWithSalt(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, salt, msgID int64, msg bin.Encoder) {
|
||
t.Helper()
|
||
body, seqNo := encodeClientMessageForTest(t, msg)
|
||
sendEncryptedWithSaltAndSeq(t, conn, cipher, auth, salt, msgID, seqNo, body)
|
||
}
|
||
|
||
func sendEncryptedWithSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, msgID int64, seqNo int32, msg bin.Encoder) {
|
||
t.Helper()
|
||
body := encodeClientMessageBodyForTest(t, msg)
|
||
sendEncryptedWithSaltAndSeq(t, conn, cipher, auth, auth.ServerSalt, msgID, seqNo, body)
|
||
}
|
||
|
||
func sendEncryptedWithSaltAndSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, salt, msgID int64, seqNo int32, body []byte) {
|
||
t.Helper()
|
||
sendEncryptedWithSessionSaltAndSeq(t, conn, cipher, auth, auth.SessionID, salt, msgID, seqNo, body)
|
||
}
|
||
|
||
func sendEncryptedWithSessionSaltAndSeq(t *testing.T, conn transport.Conn, cipher crypto.Cipher, auth exchange.ClientExchangeResult, sessionID, salt, msgID int64, seqNo int32, body []byte) {
|
||
t.Helper()
|
||
var buf bin.Buffer
|
||
if err := cipher.Encrypt(auth.AuthKey, crypto.EncryptedMessageData{
|
||
Salt: salt,
|
||
SessionID: sessionID,
|
||
MessageID: msgID,
|
||
SeqNo: seqNo,
|
||
MessageDataLen: int32(len(body)),
|
||
MessageDataWithPadding: body,
|
||
}, &buf); err != nil {
|
||
t.Fatalf("encrypt: %v", err)
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
if err := conn.Send(ctx, &buf); err != nil {
|
||
t.Fatalf("send: %v", err)
|
||
}
|
||
}
|
||
|
||
func encodeClientMessageForTest(t *testing.T, msg bin.Encoder) ([]byte, int32) {
|
||
t.Helper()
|
||
raw := encodeClientMessageBodyForTest(t, msg)
|
||
typeID, err := (&bin.Buffer{Buf: raw}).PeekID()
|
||
if err != nil {
|
||
t.Fatalf("peek encrypted message type: %v", err)
|
||
}
|
||
if container, ok := msg.(*proto.MessageContainer); ok {
|
||
return raw, clientContainerSeqNoForTest(container)
|
||
}
|
||
if clientMessageContentPolicyFor(typeID) == clientMessageContentRequired {
|
||
return raw, 1
|
||
}
|
||
return raw, 0
|
||
}
|
||
|
||
func encodeClientMessageBodyForTest(t *testing.T, msg bin.Encoder) []byte {
|
||
t.Helper()
|
||
var body bin.Buffer
|
||
if err := msg.Encode(&body); err != nil {
|
||
t.Fatalf("encode encrypted message: %v", err)
|
||
}
|
||
return body.Copy()
|
||
}
|
||
|
||
func clientContainerSeqNoForTest(container *proto.MessageContainer) int32 {
|
||
var maxSeq int32
|
||
for _, msg := range container.Messages {
|
||
if seq := int32(msg.SeqNo); seq > maxSeq {
|
||
maxSeq = seq
|
||
}
|
||
}
|
||
if maxSeq%2 != 0 {
|
||
maxSeq++
|
||
}
|
||
return maxSeq
|
||
}
|
||
|
||
// collectReplies 读取并解密 server 回发的消息,按 TypeID 收集明文 buffer,
|
||
// 直到见到 wantID(含)或达到上限。用于断言一次请求触发的多条响应
|
||
// (new_session_created / 业务响应 / msgs_ack)。
|
||
func collectReplies(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey, wantID uint32) map[uint32]*bin.Buffer {
|
||
t.Helper()
|
||
got := make(map[uint32]*bin.Buffer)
|
||
for i := 0; i < 8; i++ {
|
||
_, id, plain := readServerMessage(t, conn, cipher, key)
|
||
got[id] = plain
|
||
if id == wantID {
|
||
break
|
||
}
|
||
}
|
||
return got
|
||
}
|
||
|
||
// serverReplyFrame preserves the wire order and encrypted envelope of a server
|
||
// reply. Tests which exercise session boundaries must not collapse replies into
|
||
// a TypeID-keyed map: both duplicate response types and their order are part of
|
||
// the observable protocol behavior.
|
||
type serverReplyFrame struct {
|
||
Message *crypto.EncryptedMessageData
|
||
TypeID uint32
|
||
Plain *bin.Buffer
|
||
}
|
||
|
||
// collectReplyFrames reads ordered server replies until every requested TypeID
|
||
// has been observed the requested number of times. Unrequested frames are kept
|
||
// in the returned slice so callers can assert ordering around control messages.
|
||
func collectReplyFrames(
|
||
t *testing.T,
|
||
conn transport.Conn,
|
||
cipher crypto.Cipher,
|
||
key crypto.AuthKey,
|
||
wantCounts map[uint32]int,
|
||
) []serverReplyFrame {
|
||
t.Helper()
|
||
|
||
remaining := make(map[uint32]int, len(wantCounts))
|
||
required := 0
|
||
for typeID, count := range wantCounts {
|
||
if count <= 0 {
|
||
continue
|
||
}
|
||
remaining[typeID] = count
|
||
required += count
|
||
}
|
||
if required == 0 {
|
||
return nil
|
||
}
|
||
|
||
// Keep the helper bounded while allowing unrelated control replies (notably
|
||
// msgs_ack) to be interleaved with the frames under test.
|
||
// One shared deadline bounds the whole collection. A per-frame deadline would
|
||
// multiply a missing-result failure by the maximum number of unrelated frames.
|
||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cancel()
|
||
frames := make([]serverReplyFrame, 0, required)
|
||
for i := 0; i < required+16; i++ {
|
||
message, typeID, plain := readServerMessageContext(t, ctx, conn, cipher, key)
|
||
frames = append(frames, serverReplyFrame{
|
||
Message: message,
|
||
TypeID: typeID,
|
||
Plain: plain,
|
||
})
|
||
if count, ok := remaining[typeID]; ok {
|
||
if count == 1 {
|
||
delete(remaining, typeID)
|
||
} else {
|
||
remaining[typeID] = count - 1
|
||
}
|
||
}
|
||
if len(remaining) == 0 {
|
||
return frames
|
||
}
|
||
}
|
||
|
||
t.Fatalf("missing reply counts after %d frames: %+v", len(frames), remaining)
|
||
return nil
|
||
}
|
||
|
||
func readServerMessage(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey) (*crypto.EncryptedMessageData, uint32, *bin.Buffer) {
|
||
t.Helper()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
return readServerMessageContext(t, ctx, conn, cipher, key)
|
||
}
|
||
|
||
func readServerMessageContext(t *testing.T, ctx context.Context, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey) (*crypto.EncryptedMessageData, uint32, *bin.Buffer) {
|
||
t.Helper()
|
||
var buf bin.Buffer
|
||
err := conn.Recv(ctx, &buf)
|
||
if err != nil {
|
||
t.Fatalf("recv server message: %v", err)
|
||
}
|
||
data, err := cipher.DecryptFromBuffer(key, &buf)
|
||
if err != nil {
|
||
t.Fatalf("decrypt server message: %v", err)
|
||
}
|
||
plain := append([]byte(nil), data.Data()...)
|
||
id, err := (&bin.Buffer{Buf: plain}).PeekID()
|
||
if err != nil {
|
||
t.Fatalf("peek server message: %v", err)
|
||
}
|
||
return data, id, &bin.Buffer{Buf: plain}
|
||
}
|
||
|
||
// mustHave 断言 replies 含指定 TypeID 的消息并返回其 buffer。
|
||
func mustHave(t *testing.T, replies map[uint32]*bin.Buffer, id uint32, name string) *bin.Buffer {
|
||
t.Helper()
|
||
b, ok := replies[id]
|
||
if !ok {
|
||
t.Fatalf("missing %s (%#x)", name, id)
|
||
}
|
||
return b
|
||
}
|