263 lines
7.8 KiB
Go
263 lines
7.8 KiB
Go
package layerwire
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/gotd/td/bin"
|
|
"github.com/gotd/td/tg"
|
|
)
|
|
|
|
func TestValidateCanonicalRequestFlaggedHotPathAllocatesNothing(t *testing.T) {
|
|
var body bin.Buffer
|
|
req := &tg.MessagesSendMessageRequest{
|
|
Peer: &tg.InputPeerSelf{},
|
|
Message: "hello",
|
|
RandomID: 7,
|
|
}
|
|
if err := req.Encode(&body); err != nil {
|
|
t.Fatalf("encode request: %v", err)
|
|
}
|
|
if err := ValidateCanonicalRequest(body.Buf); err != nil {
|
|
t.Fatalf("validate request: %v", err)
|
|
}
|
|
if allocs := testing.AllocsPerRun(1000, func() {
|
|
if err := ValidateCanonicalRequest(body.Buf); err != nil {
|
|
panic(err)
|
|
}
|
|
}); allocs != 0 {
|
|
t.Fatalf("canonical request preflight allocations = %.2f, want 0", allocs)
|
|
}
|
|
}
|
|
|
|
func TestValidateCanonicalRequestVectorLimits(t *testing.T) {
|
|
editCloseFriends := canonical.byName["contacts.editCloseFriends"]
|
|
if editCloseFriends == nil {
|
|
t.Fatal("contacts.editCloseFriends missing from canonical schema")
|
|
}
|
|
|
|
t.Run("explicit_5000_override", func(t *testing.T) {
|
|
var body bin.Buffer
|
|
body.PutID(editCloseFriends.crc)
|
|
body.PutVectorHeader(5000)
|
|
for i := 0; i < 5000; i++ {
|
|
body.PutLong(int64(i))
|
|
}
|
|
if err := ValidateCanonicalRequest(body.Buf); err != nil {
|
|
t.Fatalf("validate legal 5000-element close-friends request: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("override_stops_at_5000", func(t *testing.T) {
|
|
var body bin.Buffer
|
|
body.PutID(editCloseFriends.crc)
|
|
body.PutVectorHeader(5001)
|
|
err := ValidateCanonicalRequest(body.Buf)
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
|
|
t.Run("default_4096", func(t *testing.T) {
|
|
getMessages := canonical.byName["messages.getMessages"]
|
|
var body bin.Buffer
|
|
body.PutID(getMessages.crc)
|
|
body.PutVectorHeader(defaultMaxVectorElements + 1)
|
|
err := ValidateCanonicalRequest(body.Buf)
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
|
|
t.Run("max_int32_count_rejected_before_iteration", func(t *testing.T) {
|
|
var body bin.Buffer
|
|
body.PutID(editCloseFriends.crc)
|
|
body.PutID(vectorTypeID)
|
|
body.PutInt32(math.MaxInt32)
|
|
err := ValidateCanonicalRequest(body.Buf)
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateCanonicalRequestDepthLimit(t *testing.T) {
|
|
invoke := canonical.byName["invokeWithoutUpdates"]
|
|
leaf := canonical.byName["help.getConfig"]
|
|
if invoke == nil || leaf == nil {
|
|
t.Fatal("generic wrapper methods missing from canonical schema")
|
|
}
|
|
request := func(wrappers int) []byte {
|
|
var body bin.Buffer
|
|
for i := 0; i < wrappers; i++ {
|
|
body.PutID(invoke.crc)
|
|
}
|
|
body.PutID(leaf.crc)
|
|
return body.Buf
|
|
}
|
|
if err := ValidateCanonicalRequest(request(defaultMaxWalkDepth - 1)); err != nil {
|
|
t.Fatalf("depth exactly %d rejected: %v", defaultMaxWalkDepth, err)
|
|
}
|
|
err := ValidateCanonicalRequest(request(defaultMaxWalkDepth))
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("depth %d error = %v, want ErrResourceLimit", defaultMaxWalkDepth+1, err)
|
|
}
|
|
}
|
|
|
|
func TestTLBytesSkipIsZeroCopyAndBounded(t *testing.T) {
|
|
var encoded bin.Buffer
|
|
encoded.PutBytes([]byte("payload"))
|
|
fieldLen := len(encoded.Buf)
|
|
raw := append(encoded.Copy(), 0xaa, 0xbb, 0xcc, 0xdd)
|
|
b := &bin.Buffer{Buf: raw}
|
|
walk := newWalkState()
|
|
if err := walk.skipTLBytes(b, "bytes"); err != nil {
|
|
t.Fatalf("skip bytes: %v", err)
|
|
}
|
|
if len(b.Buf) != 4 || &b.Buf[0] != &raw[fieldLen] {
|
|
t.Fatalf("walker did not retain the original backing buffer")
|
|
}
|
|
|
|
t.Run("per_field_budget", func(t *testing.T) {
|
|
limited := newWalkState()
|
|
limited.limits.maxFieldBytes = 3
|
|
probe := &bin.Buffer{Buf: encoded.Copy()}
|
|
err := limited.skipTLBytes(probe, "bytes")
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
|
|
t.Run("aggregate_budget", func(t *testing.T) {
|
|
limited := newWalkState()
|
|
limited.limits.maxTotalBytes = 10
|
|
first := &bin.Buffer{Buf: encoded.Copy()}
|
|
if err := limited.skipTLBytes(first, "bytes"); err != nil {
|
|
t.Fatalf("first field: %v", err)
|
|
}
|
|
second := &bin.Buffer{Buf: encoded.Copy()}
|
|
err := limited.skipTLBytes(second, "bytes")
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("second error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
|
|
t.Run("truncated_payload_is_malformed", func(t *testing.T) {
|
|
importAuth := canonical.byName["auth.importAuthorization"]
|
|
var body bin.Buffer
|
|
body.PutID(importAuth.crc)
|
|
body.PutLong(1)
|
|
body.Put([]byte{5, 'a', 'b'}) // declares five bytes, lacks payload/padding
|
|
err := ValidateCanonicalRequest(body.Buf)
|
|
if !errors.Is(err, ErrMalformed) || errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want only ErrMalformed", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInboundTransformsShareWalkerBudgets(t *testing.T) {
|
|
t.Run("canonical_alias", func(t *testing.T) {
|
|
var body bin.Buffer
|
|
body.PutID(0x41d41ade) // DrKLO messages.forwardMessages alias
|
|
body.PutUint32(0)
|
|
body.PutID(canonical.byName["inputPeerEmpty"].crc)
|
|
body.PutID(vectorTypeID)
|
|
body.PutInt32(math.MaxInt32)
|
|
_, ok, err := UpgradeInbound(0x41d41ade, &body)
|
|
if !ok || !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("ok=%v error=%v, want matched ErrResourceLimit", ok, err)
|
|
}
|
|
})
|
|
|
|
t.Run("drift_body_transform", func(t *testing.T) {
|
|
var body bin.Buffer
|
|
body.PutID(0x2e1ee318) // DrKLO langpack.getStrings body transform
|
|
body.PutString("en")
|
|
body.PutID(vectorTypeID)
|
|
body.PutInt32(math.MaxInt32)
|
|
_, ok, err := UpgradeInbound(0x2e1ee318, &body)
|
|
if !ok || !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("ok=%v error=%v, want matched ErrResourceLimit", ok, err)
|
|
}
|
|
})
|
|
|
|
t.Run("outbound_structural_transform", func(t *testing.T) {
|
|
poll := canonical.byName["pollAnswerVoters"]
|
|
var body bin.Buffer
|
|
body.PutID(poll.crc)
|
|
body.PutUint32(1 << 2)
|
|
body.PutBytes(nil)
|
|
body.PutInt(1)
|
|
body.PutID(vectorTypeID)
|
|
body.PutInt32(math.MaxInt32)
|
|
_, err := Transcode(body.Buf, CanonicalLayer-1)
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWalkerArithmeticAndMalformedClassification(t *testing.T) {
|
|
if defaultMaxWalkUnits != 131072 {
|
|
t.Fatalf("default constructor/vector budget = %d, want 131072", defaultMaxWalkUnits)
|
|
}
|
|
if _, ok := checkedMulInt(math.MaxInt, 2); ok {
|
|
t.Fatal("checkedMulInt accepted overflow")
|
|
}
|
|
if _, ok := checkedAddUint64(math.MaxUint64, 1); ok {
|
|
t.Fatal("checkedAddUint64 accepted overflow")
|
|
}
|
|
|
|
t.Run("aggregate_constructor_and_vector_units", func(t *testing.T) {
|
|
editCloseFriends := canonical.byName["contacts.editCloseFriends"]
|
|
var body bin.Buffer
|
|
body.PutID(editCloseFriends.crc)
|
|
body.PutVectorHeader(4)
|
|
for i := 0; i < 4; i++ {
|
|
body.PutLong(int64(i))
|
|
}
|
|
walk := newWalkState()
|
|
walk.limits.maxUnits = 4 // top constructor + four elements needs five
|
|
probe := &bin.Buffer{Buf: body.Buf}
|
|
err := walk.skipObject(canonical, probe, 1)
|
|
if !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want ErrResourceLimit", err)
|
|
}
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
}{
|
|
{name: "empty"},
|
|
{name: "unknown_constructor", body: []byte{1, 2, 3, 4}},
|
|
{name: "trailing_bytes", body: append(methodIDBytes(canonical.byName["help.getConfig"].crc), 0, 0, 0, 0)},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidateCanonicalRequest(tt.body)
|
|
if !errors.Is(err, ErrMalformed) || errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("error = %v, want only ErrMalformed", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func methodIDBytes(id uint32) []byte {
|
|
var b bin.Buffer
|
|
b.PutID(id)
|
|
return b.Buf
|
|
}
|
|
|
|
func FuzzValidateCanonicalRequest(f *testing.F) {
|
|
f.Add(methodIDBytes(canonical.byName["help.getConfig"].crc))
|
|
f.Add([]byte{})
|
|
f.Add([]byte{1, 2, 3, 4})
|
|
f.Fuzz(func(t *testing.T, body []byte) {
|
|
err := ValidateCanonicalRequest(body)
|
|
if err != nil && !errors.Is(err, ErrMalformed) && !errors.Is(err, ErrResourceLimit) {
|
|
t.Fatalf("unclassified walker error: %v", err)
|
|
}
|
|
})
|
|
}
|