310 lines
11 KiB
Go
310 lines
11 KiB
Go
package rpc
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"github.com/iamxvbaba/td/bin"
|
|
"github.com/iamxvbaba/td/tg"
|
|
|
|
"github.com/iamxvbaba/td/tlprofile"
|
|
appfiles "telesrv/internal/app/files"
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
const tlVectorTypeID = uint32(0x1cb5c415)
|
|
|
|
type requestVectorPolicy struct {
|
|
vectorOffset int
|
|
max int
|
|
minElemBytes int
|
|
tooLong func() error
|
|
}
|
|
|
|
type rawRPCPreflightWire []byte
|
|
|
|
func (w rawRPCPreflightWire) WireSize() int { return len(w) }
|
|
|
|
func (w rawRPCPreflightWire) ByteAt(offset int) (byte, error) {
|
|
if offset < 0 || offset >= len(w) {
|
|
return 0, fmt.Errorf("byte offset %d outside request size %d", offset, len(w))
|
|
}
|
|
return w[offset], nil
|
|
}
|
|
|
|
func (w rawRPCPreflightWire) Uint32At(offset int) (uint32, error) {
|
|
if offset < 0 || offset > len(w) || len(w)-offset < 4 {
|
|
return 0, fmt.Errorf("uint32 offset %d outside request size %d", offset, len(w))
|
|
}
|
|
return binary.LittleEndian.Uint32(w[offset:]), nil
|
|
}
|
|
|
|
// requestVectorPolicies mirrors limits already enforced by typed handlers, but does so before
|
|
// gotd's generated decoder materializes attacker-controlled interface slices. users.getUsers is
|
|
// the one newly introduced cap: TDesktop's four current call sites all send exactly one user.
|
|
var requestVectorPolicies = map[uint32]requestVectorPolicy{
|
|
tg.UsersGetUsersRequestTypeID: {vectorOffset: 4, max: 100, minElemBytes: 4, tooLong: inputRequestTooLongErr},
|
|
tg.UsersGetRequirementsToContactRequestTypeID: {vectorOffset: 4, max: maxRequirementsToContactUsers, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.ContactsImportContactsRequestTypeID: {vectorOffset: 4, max: maxContactImportBatch, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.ContactsDeleteContactsRequestTypeID: {vectorOffset: 4, max: maxContactDeleteBatch, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.ContactsEditCloseFriendsRequestTypeID: {vectorOffset: 4, max: maxCloseFriendsCount, minElemBytes: 8, tooLong: limitInvalidErr},
|
|
tg.ContactsSetBlockedRequestTypeID: {vectorOffset: 8, max: maxContactSetBlocked, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.MessagesGetMessagesRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.MessagesGetChatsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 8, tooLong: limitInvalidErr},
|
|
tg.MessagesGetPeerDialogsRequestTypeID: {vectorOffset: 4, max: maxDialogInputPeers, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.MessagesReadMessageContentsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.MessagesGetCustomEmojiDocumentsRequestTypeID: {vectorOffset: 4, max: maxEmojiDocuments, minElemBytes: 8, tooLong: limitInvalidErr},
|
|
tg.MessagesDeleteMessagesRequestTypeID: {vectorOffset: 8, max: domain.MaxDeleteMessageIDs, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.MessagesCreateChatRequestTypeID: {vectorOffset: 8, max: 200, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
tg.ChannelsGetChannelsRequestTypeID: {vectorOffset: 4, max: maxGetMessagesIDs, minElemBytes: 4, tooLong: limitInvalidErr},
|
|
}
|
|
|
|
// layerRPCVectorPolicies bind resource limits to generated semantic field
|
|
// identities, not constructor CRCs, profile-specific field order, flags bits,
|
|
// or byte offsets. gotdgen proves at generation time that each registered
|
|
// field exposes the same metric in every routable profile. Importing a future
|
|
// schema therefore either extends these policies automatically or makes
|
|
// dispatcher construction fail closed until an explicit conversion policy is
|
|
// supplied.
|
|
var layerRPCVectorPolicies = []struct {
|
|
fieldID tlprofile.FieldID
|
|
max int
|
|
tooLong func() error
|
|
}{
|
|
{tlprofile.FieldUsersGetUsersID, 100, inputRequestTooLongErr},
|
|
{tlprofile.FieldUsersGetRequirementsToContactID, maxRequirementsToContactUsers, limitInvalidErr},
|
|
{tlprofile.FieldContactsImportContactsContacts, maxContactImportBatch, limitInvalidErr},
|
|
{tlprofile.FieldContactsDeleteContactsID, maxContactDeleteBatch, limitInvalidErr},
|
|
{tlprofile.FieldContactsEditCloseFriendsID, maxCloseFriendsCount, limitInvalidErr},
|
|
{tlprofile.FieldContactsSetBlockedID, maxContactSetBlocked, limitInvalidErr},
|
|
{tlprofile.FieldMessagesGetMessagesID, maxGetMessagesIDs, limitInvalidErr},
|
|
{tlprofile.FieldMessagesGetChatsID, maxGetMessagesIDs, limitInvalidErr},
|
|
{tlprofile.FieldMessagesGetPeerDialogsPeers, maxDialogInputPeers, limitInvalidErr},
|
|
{tlprofile.FieldMessagesReadMessageContentsID, maxGetMessagesIDs, limitInvalidErr},
|
|
{tlprofile.FieldMessagesGetCustomEmojiDocumentsDocumentID, maxEmojiDocuments, limitInvalidErr},
|
|
{tlprofile.FieldMessagesDeleteMessagesID, domain.MaxDeleteMessageIDs, limitInvalidErr},
|
|
{tlprofile.FieldMessagesCreateChatUsers, 200, limitInvalidErr},
|
|
{tlprofile.FieldChannelsGetChannelsID, maxGetMessagesIDs, limitInvalidErr},
|
|
}
|
|
|
|
func registerLayerRPCAdmissionFieldPreflights(d *tlprofile.Dispatcher) error {
|
|
if d == nil {
|
|
return fmt.Errorf("nil layer RPC dispatcher")
|
|
}
|
|
for _, policy := range layerRPCVectorPolicies {
|
|
policy := policy
|
|
if err := d.OnFieldPreflight(policy.fieldID, func(view tlprofile.FieldView) error {
|
|
length, ok := view.VectorLength()
|
|
if !ok {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if length <= policy.max {
|
|
return nil
|
|
}
|
|
if policy.tooLong != nil {
|
|
return policy.tooLong()
|
|
}
|
|
return inputRequestTooLongErr()
|
|
}); err != nil {
|
|
return fmt.Errorf("register vector field %#016x: %w", uint64(policy.fieldID), err)
|
|
}
|
|
}
|
|
|
|
for _, fieldID := range []tlprofile.FieldID{
|
|
tlprofile.FieldUploadSaveFilePartBytes,
|
|
tlprofile.FieldUploadSaveBigFilePartBytes,
|
|
} {
|
|
if err := d.OnFieldPreflight(fieldID, func(view tlprofile.FieldView) error {
|
|
length, ok := view.BytesLength()
|
|
if !ok {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if length > appfiles.MaxUploadPartBytes {
|
|
return filePartTooBigErr()
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("register upload bytes field %#016x: %w", uint64(fieldID), err)
|
|
}
|
|
}
|
|
|
|
if err := d.OnFieldPreflight(
|
|
tlprofile.FieldUploadSaveBigFilePartFileTotalParts,
|
|
func(view tlprofile.FieldView) error {
|
|
totalParts, ok := view.Int32()
|
|
if !ok {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if totalParts <= 0 || totalParts > int32(appfiles.MaxUploadParts) {
|
|
return filePartInvalidErr()
|
|
}
|
|
return nil
|
|
},
|
|
); err != nil {
|
|
return fmt.Errorf("register upload total-parts field: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func preflightRPCRequest(id uint32, b *bin.Buffer) error {
|
|
if b == nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
return preflightRPCWire(id, rawRPCPreflightWire(b.Buf))
|
|
}
|
|
|
|
func preflightRPCWire(id uint32, wire rawRPCPreflightWire) error {
|
|
if wire == nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if policy, ok := requestVectorPolicies[id]; ok {
|
|
if err := preflightFixedVector(wire, policy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
switch id {
|
|
case tg.UploadSaveFilePartRequestTypeID:
|
|
return preflightUploadPart(wire, 16, false)
|
|
case tg.UploadSaveBigFilePartRequestTypeID:
|
|
return preflightUploadPart(wire, 20, true)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// LayerRPCFlatBytesPayloadSize exposes the only two byte-heavy, flat request
|
|
// graphs to mtprotoedge's materialization budget. Returning ok is deliberately
|
|
// stricter than recognizing the constructor: the complete TL bytes tail,
|
|
// per-part byte ceiling and big-file total-parts policy must all pass the same
|
|
// allocation-free preflight used by generated exact admission. All other RPCs
|
|
// retain the generic worst-case graph charge.
|
|
func (r *Router) LayerRPCFlatBytesPayloadSize(wire []byte) (int, bool) {
|
|
if r == nil || len(wire) < 4 {
|
|
return 0, false
|
|
}
|
|
raw := rawRPCPreflightWire(wire)
|
|
id := binary.LittleEndian.Uint32(wire[:4])
|
|
bytesOffset := 0
|
|
switch id {
|
|
case tg.UploadSaveFilePartRequestTypeID:
|
|
bytesOffset = 16
|
|
case tg.UploadSaveBigFilePartRequestTypeID:
|
|
bytesOffset = 20
|
|
default:
|
|
return 0, false
|
|
}
|
|
if err := preflightRPCWire(id, raw); err != nil {
|
|
return 0, false
|
|
}
|
|
payloadBytes, encodedBytes, err := tlBytesSizeAt(raw, bytesOffset)
|
|
if err != nil || encodedBytes != len(wire)-bytesOffset {
|
|
return 0, false
|
|
}
|
|
return payloadBytes, true
|
|
}
|
|
|
|
func preflightFixedVector(wire rawRPCPreflightWire, policy requestVectorPolicy) error {
|
|
if policy.vectorOffset < 4 || policy.minElemBytes <= 0 || wire.WireSize() < policy.vectorOffset+8 {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
typeID, err := wire.Uint32At(policy.vectorOffset)
|
|
if err != nil || typeID != tlVectorTypeID {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
rawCount, err := wire.Uint32At(policy.vectorOffset + 4)
|
|
if err != nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
count := int64(int32(rawCount))
|
|
if count < 0 {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
remaining := int64(wire.WireSize() - policy.vectorOffset - 8)
|
|
// Check the cheapest possible encoding before the policy cap. A forged MaxInt32 count with
|
|
// a truncated body is malformed, not merely a large valid request, and is rejected O(1).
|
|
if count > remaining/int64(policy.minElemBytes) {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if count > int64(policy.max) {
|
|
if policy.tooLong != nil {
|
|
return policy.tooLong()
|
|
}
|
|
return inputRequestTooLongErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func preflightUploadPart(wire rawRPCPreflightWire, bytesOffset int, big bool) error {
|
|
if wire.WireSize() < bytesOffset {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
rawPart, err := wire.Uint32At(12)
|
|
if err != nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
part := int32(rawPart)
|
|
if part < 0 || part >= int32(appfiles.MaxUploadParts) {
|
|
return filePartInvalidErr()
|
|
}
|
|
if big {
|
|
rawTotalParts, err := wire.Uint32At(16)
|
|
if err != nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
totalParts := int32(rawTotalParts)
|
|
if totalParts <= 0 || totalParts > appfiles.MaxUploadParts {
|
|
return filePartInvalidErr()
|
|
}
|
|
}
|
|
n, encoded, err := tlBytesSizeAt(wire, bytesOffset)
|
|
if err != nil {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if encoded != wire.WireSize()-bytesOffset {
|
|
return inputRequestInvalidErr()
|
|
}
|
|
if n == 0 {
|
|
return filePartInvalidErr()
|
|
}
|
|
if n > appfiles.MaxUploadPartBytes {
|
|
return filePartTooBigErr()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tlBytesSizeAt parses a TL bytes prefix without copying the payload. encoded
|
|
// includes prefix, payload and 4-byte padding.
|
|
func tlBytesSizeAt(wire rawRPCPreflightWire, offset int) (n, encoded int, err error) {
|
|
if offset < 0 || offset >= wire.WireSize() {
|
|
return 0, 0, fmt.Errorf("bytes prefix out of range")
|
|
}
|
|
first, err := wire.ByteAt(offset)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
prefix := 1
|
|
switch {
|
|
case first < 254:
|
|
n = int(first)
|
|
case first == 254:
|
|
if wire.WireSize()-offset < 4 {
|
|
return 0, 0, fmt.Errorf("truncated long bytes prefix")
|
|
}
|
|
second, secondErr := wire.ByteAt(offset + 1)
|
|
third, thirdErr := wire.ByteAt(offset + 2)
|
|
fourth, fourthErr := wire.ByteAt(offset + 3)
|
|
if secondErr != nil || thirdErr != nil || fourthErr != nil {
|
|
return 0, 0, fmt.Errorf("truncated long bytes prefix")
|
|
}
|
|
n = int(second) | int(third)<<8 | int(fourth)<<16
|
|
prefix = 4
|
|
default:
|
|
return 0, 0, fmt.Errorf("invalid bytes prefix")
|
|
}
|
|
total := prefix + n
|
|
padding := (4 - total%4) % 4
|
|
if total > wire.WireSize()-offset-padding {
|
|
return 0, 0, fmt.Errorf("truncated bytes payload")
|
|
}
|
|
return n, total + padding, nil
|
|
}
|