1package sessionwire
2
3// Directional ceiling enforcement (P2.2/P3.3, P5.1 limits table).
4//
5// The preamble negotiates one immutable pair of ceilings per connection:
6// maxReceiveEnvelopeBytes bounds every envelope this side accepts, and
7// outputDataLimit bounds the uncompressed terminal bytes of one Output
8// message. Envelope ceilings are category-aware:
9//
10// - ordinary control envelopes never exceed wire.ControlEnvelopeLimit;
11// - payload-bearing Input, Output, ImagePush, PickerSnapshot, and
12// NavigationInventoryResponse variants may use the full negotiated
13// envelope ceiling, which
14// the preamble already bounds by wire.AbsoluteEnvelopeLimit.
15//
16// A negotiated ceiling below the applicable category ceiling always wins.
17// Category detection is centralized here and derives from the serialized
18// payload, so every typed send (normal, async, owned-synchronous) and every
19// typed receive shares one classification; unknown or malformed payloads
20// fall back to control, the strictest category. Carriage framing limits
21// stay owned by the concrete transports.
22
23import (
24 "errors"
25
26 "google.golang.org/protobuf/encoding/protowire"
27
28 "github.com/bnema/vev/internal/protocol/wire"
29)
30
31// errOutputDataExceedsLimit reports one Output whose uncompressed terminal
32// data is above the negotiated output-data ceiling.
33var errOutputDataExceedsLimit = errors.New("sessionwire: output exceeds negotiated data limit")
34
35// envelopeCategory selects the serialized-size ceiling for one directional
36// envelope.
37type envelopeCategory uint8
38
39const (
40 // categoryControl covers every envelope that is neither Output nor
41 // ImagePush; it never exceeds wire.ControlEnvelopeLimit.
42 categoryControl envelopeCategory = iota
43 // categoryOutput covers server Output; its serialized envelope rides
44 // the negotiated envelope ceiling and its uncompressed data is bounded
45 // separately by outputDataLimit.
46 categoryOutput
47 // categoryBulk covers bounded application payloads whose semantic limits
48 // exceed the ordinary 1 MiB control ceiling.
49 categoryBulk
50)
51
52// Envelope variant field numbers that select the ceiling-exempt categories;
53// see schema/envelope.proto. Every other variant, known or not, is control.
54const (
55 clientInputField = 2
56 clientImagePushField = 10
57 serverOutputField = 3
58 serverNavigationInventoryField = 19
59 serverPickerSnapshotField = 23
60)
61
62// envelopeLimitFor returns the serialized-size ceiling for one categorized
63// envelope: control is capped at wire.ControlEnvelopeLimit while bulk
64// payloads may use the full negotiated ceiling. A negotiated
65// ceiling below the category ceiling always wins.
66func envelopeLimitFor(category envelopeCategory, negotiated uint64) uint64 {
67 if category == categoryControl && negotiated > wire.ControlEnvelopeLimit {
68 return wire.ControlEnvelopeLimit
69 }
70 return negotiated
71}
72
73// checkEnvelopeCeiling rejects one serialized envelope above ceiling bytes.
74func checkEnvelopeCeiling(payload []byte, ceiling uint64) error {
75 if uint64(len(payload)) > ceiling {
76 return wire.ErrScanLength
77 }
78 return nil
79}
80
81// checkCategoryCeiling classifies one serialized directional payload and
82// applies its category ceiling. Sends and receives share this single
83// enforcement point.
84func checkCategoryCeiling(payload []byte, category envelopeCategory, negotiated uint64) error {
85 return checkEnvelopeCeiling(payload, envelopeLimitFor(category, negotiated))
86}
87
88// clientEnvelopeCategory classifies one serialized client envelope before
89// decode. Input and ImagePush are exempt from wire.ControlEnvelopeLimit; every
90// other variant, including unknown and malformed payloads, stays control so
91// the strictest ceiling applies.
92func clientEnvelopeCategory(payload []byte) envelopeCategory {
93 switch leadingField(payload) {
94 case clientInputField, clientImagePushField:
95 return categoryBulk
96 }
97 return categoryControl
98}
99
100// serverEnvelopeCategory classifies one serialized server envelope before
101// decode. Output and the semantically 4 MiB-bounded inventory/picker snapshots
102// are exempt from wire.ControlEnvelopeLimit; every other variant, including
103// unknown and malformed payloads, stays control.
104func serverEnvelopeCategory(payload []byte) envelopeCategory {
105 switch leadingField(payload) {
106 case serverOutputField:
107 return categoryOutput
108 case serverNavigationInventoryField, serverPickerSnapshotField:
109 return categoryBulk
110 }
111 return categoryControl
112}
113
114// leadingField returns the field number of payload's leading tag when it is
115// length-delimited, the wire type every envelope variant uses. It returns 0
116// for empty, truncated, or non-length-delimited payloads, which then
117// classify as control.
118func leadingField(payload []byte) protowire.Number {
119 number, wireType, consumed := protowire.ConsumeTag(payload)
120 if consumed <= 0 || wireType != protowire.BytesType {
121 return 0
122 }
123 return number
124}
125
126// checkOutputDataCeiling rejects one serialized Output envelope whose
127// declared uncompressed length is above the negotiated output-data ceiling.
128// It runs before decompression so hostile lengths never allocate.
129func checkOutputDataCeiling(envelope *wire.ServerEnvelope, limit uint64) error {
130 if envelope == nil {
131 return nil
132 }
133 output, ok := envelope.Payload.(*wire.ServerEnvelope_Output)
134 if !ok || output.Output == nil {
135 return nil
136 }
137 if output.Output.GetUncompressedLength() > limit {
138 return errOutputDataExceedsLimit
139 }
140 return nil
141}
142
143// sendEnvelopeWithin marshals one server envelope and enforces the
144// negotiated output-data and category envelope ceilings before any
145// transport call. All server send modes (normal, async, owned synchronous)
146// share it.
147func sendEnvelopeWithin(envelope *wire.ServerEnvelope, ceilings protoCeilings) (wire.Envelope, error) {
148 if err := checkOutputDataCeiling(envelope, ceilings.outputDataLimit); err != nil {
149 return wire.Envelope{}, err
150 }
151 raw, err := marshalServerEnvelope(envelope)
152 if err != nil {
153 return wire.Envelope{}, err
154 }
155 if err := checkCategoryCeiling(raw.Payload, serverEnvelopeCategory(raw.Payload), ceilings.maxReceiveEnvelopeBytes); err != nil {
156 return wire.Envelope{}, err
157 }
158 return raw, nil
159}