Skip to main content

mysten_network/request_log/
body.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    borrow::Cow,
6    pin::Pin,
7    task::{Context, Poll, ready},
8};
9
10use base64::Engine as _;
11use bytes::Bytes;
12use http_body::Body;
13use pin_project_lite::pin_project;
14use tracing::{Span, trace};
15
16use super::TARGET;
17
18/// Length of the gRPC frame prefix: a 1-byte compressed flag and a 4-byte big-endian message
19/// length.
20const FRAME_PREFIX_LEN: usize = 5;
21
22/// Per-request cap on captured messages. Every RPC method this layer captures is unary or
23/// server-streaming, and reflection's own service isn't registered in the descriptor pool (so its
24/// multi-message streams are never captured here either) — a well-behaved client's request body
25/// always holds exactly one message. This instead guards against a hostile body: nothing stops a
26/// client from stuffing extra tiny frames into a "unary" request after the real message, so this
27/// stops that from amplifying into unbounded decode and log volume.
28const MAX_CAPTURED_MESSAGES: u32 = 1;
29
30pin_project! {
31    /// Request body for [`GrpcRequestLog`], teeing data chunks into a gRPC frame parser while
32    /// passing them through unchanged.
33    ///
34    /// [`GrpcRequestLog`]: super::GrpcRequestLog
35    pub struct RequestLogBody<B> {
36        #[pin]
37        inner: B,
38        capture: Option<CaptureState>,
39    }
40}
41
42/// Everything needed to emit the messages of one request: the span carrying the filterable
43/// `service`/`method` fields, and the frame parser's state.
44pub(crate) struct CaptureState {
45    span: Span,
46    path: String,
47    max_captured_message_size: usize,
48    parser: FrameParser,
49    message_index: u32,
50}
51
52/// Incremental parser for the gRPC message framing (a sequence of frames, each a
53/// `FRAME_PREFIX_LEN`-byte prefix followed by that message's bytes). Chunk-boundary-agnostic: one
54/// chunk may contain several frames or a fraction of one.
55enum FrameParser {
56    /// Accumulating the frame prefix.
57    Prefix {
58        buf: [u8; FRAME_PREFIX_LEN],
59        filled: usize,
60    },
61    /// Accumulating `remaining` message bytes into `buf`.
62    Message { remaining: usize, buf: Vec<u8> },
63    /// Counting down a message that is not being buffered (compressed or over the size cap).
64    Skip { remaining: usize },
65    /// Parsing is over for this request — the framing was structurally invalid, or the
66    /// per-request capture limit was reached. The rest of the stream passes through unparsed.
67    Stopped,
68}
69
70/// A frame boundary [`FrameParser::feed`] reached.
71enum FrameEvent {
72    /// A complete, uncompressed message within the size cap.
73    Message(Vec<u8>),
74    /// A message whose bytes were counted but not buffered.
75    Skipped {
76        len: usize,
77        reason: Cow<'static, str>,
78    },
79}
80
81impl<B> RequestLogBody<B> {
82    pub(crate) fn new(inner: B, capture: Option<CaptureState>) -> Self {
83        Self { inner, capture }
84    }
85}
86
87impl CaptureState {
88    pub(crate) fn new(span: Span, path: String, max_captured_message_size: usize) -> Self {
89        Self {
90            span,
91            path,
92            max_captured_message_size,
93            parser: FrameParser::new(),
94            message_index: 0,
95        }
96    }
97
98    /// Feed the next body chunk to the frame parser, emitting one event per completed frame.
99    fn observe(&mut self, mut chunk: &[u8]) {
100        while !chunk.is_empty() {
101            // Checked before feeding the next frame, not after it completes, so an excess
102            // frame's header never reaches the `Prefix` arm's `Vec::with_capacity` allocation in
103            // `feed` — once the cap is hit, `feed` is never called again for this request. Gated
104            // on `Prefix` (a fresh frame boundary) so this only fires once a message beyond the
105            // cap actually arrives; a body with exactly `MAX_CAPTURED_MESSAGES` messages and
106            // nothing more must not end in a spurious "too many messages" event.
107            if self.message_index >= MAX_CAPTURED_MESSAGES
108                && matches!(self.parser, FrameParser::Prefix { .. })
109            {
110                let _span = self.span.enter();
111                trace!(
112                    target: TARGET,
113                    method = %self.path,
114                    message_count = self.message_index,
115                    "Captured request (capture stopped: too many messages)",
116                );
117                self.parser = FrameParser::Stopped;
118                return;
119            }
120
121            let (consumed, completed) = self.parser.feed(chunk, self.max_captured_message_size);
122            chunk = &chunk[consumed..];
123
124            let Some(event) = completed else {
125                continue;
126            };
127
128            // Enter the span so `EnvFilter` span-field directives (service/method) apply to the
129            // event.
130            let _span = self.span.enter();
131
132            match event {
133                FrameEvent::Message(message) => trace!(
134                    target: TARGET,
135                    method = %self.path,
136                    message_index = self.message_index,
137                    payload = %base64::engine::general_purpose::STANDARD.encode(&message),
138                    "Captured request",
139                ),
140                FrameEvent::Skipped { len, reason } => trace!(
141                    target: TARGET,
142                    method = %self.path,
143                    message_index = self.message_index,
144                    message_len = len,
145                    skipped = %reason,
146                    "Captured request (payload skipped)",
147                ),
148            }
149            self.message_index += 1;
150        }
151    }
152}
153
154impl FrameParser {
155    fn new() -> Self {
156        Self::Prefix {
157            buf: [0; FRAME_PREFIX_LEN],
158            filled: 0,
159        }
160    }
161
162    /// Feed the parser up to `chunk.len()` bytes; returns how many bytes were consumed and, if a
163    /// frame boundary was reached, what completed. Always consumes at least one byte of a
164    /// non-empty chunk, so callers can loop until the chunk is exhausted.
165    fn feed(&mut self, chunk: &[u8], max_message_size: usize) -> (usize, Option<FrameEvent>) {
166        match self {
167            Self::Prefix { buf, filled } => {
168                let n = (FRAME_PREFIX_LEN - *filled).min(chunk.len());
169                buf[*filled..*filled + n].copy_from_slice(&chunk[..n]);
170                *filled += n;
171                if *filled < FRAME_PREFIX_LEN {
172                    return (n, None);
173                }
174
175                let flag = buf[0];
176                let len = u32::from_be_bytes(buf[1..].try_into().unwrap()) as usize;
177                if flag > 1 {
178                    *self = Self::Stopped;
179                    return (
180                        n,
181                        Some(FrameEvent::Skipped {
182                            len,
183                            reason: format!("invalid flag {flag}").into(),
184                        }),
185                    );
186                }
187                if flag == 1 {
188                    *self = Self::skip(len);
189                    return (
190                        n,
191                        Some(FrameEvent::Skipped {
192                            len,
193                            reason: "compressed message".into(),
194                        }),
195                    );
196                }
197                if len > max_message_size {
198                    *self = Self::skip(len);
199                    return (
200                        n,
201                        Some(FrameEvent::Skipped {
202                            len,
203                            reason: "message too large".into(),
204                        }),
205                    );
206                }
207                if len == 0 {
208                    *self = Self::new();
209                    return (n, Some(FrameEvent::Message(Vec::new())));
210                }
211                *self = Self::Message {
212                    remaining: len,
213                    // Reserve only what this chunk already has in hand, not the full claimed
214                    // `len` (an unverified remote-supplied value) — `buf` grows via
215                    // `extend_from_slice` as further chunks arrive, so a client that sends just
216                    // the prefix and stalls costs ~0 bytes here, regardless of `len`.
217                    buf: Vec::with_capacity((chunk.len() - n).min(len)),
218                };
219                (n, None)
220            }
221            Self::Message { remaining, buf } => {
222                let n = (*remaining).min(chunk.len());
223                buf.extend_from_slice(&chunk[..n]);
224                *remaining -= n;
225                if *remaining > 0 {
226                    return (n, None);
227                }
228                let message = std::mem::take(buf);
229                *self = Self::new();
230                (n, Some(FrameEvent::Message(message)))
231            }
232            Self::Skip { remaining } => {
233                let n = (*remaining).min(chunk.len());
234                *remaining -= n;
235                if *remaining == 0 {
236                    *self = Self::new();
237                }
238                (n, None)
239            }
240            Self::Stopped => (chunk.len(), None),
241        }
242    }
243
244    /// Enter [`FrameParser::Skip`] for `len` bytes (or start the next prefix immediately for an
245    /// empty message).
246    fn skip(len: usize) -> Self {
247        if len == 0 {
248            Self::new()
249        } else {
250            Self::Skip { remaining: len }
251        }
252    }
253}
254
255impl<B> Body for RequestLogBody<B>
256where
257    B: Body<Data = Bytes>,
258{
259    type Data = Bytes;
260    type Error = B::Error;
261
262    fn poll_frame(
263        self: Pin<&mut Self>,
264        cx: &mut Context<'_>,
265    ) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
266        let this = self.project();
267        let result = ready!(this.inner.poll_frame(cx));
268
269        if let (Some(capture), Some(Ok(frame))) = (this.capture.as_mut(), result.as_ref())
270            && let Some(chunk) = frame.data_ref()
271        {
272            capture.observe(chunk.as_ref());
273        }
274
275        Poll::Ready(result)
276    }
277
278    fn is_end_stream(&self) -> bool {
279        self.inner.is_end_stream()
280    }
281
282    fn size_hint(&self) -> http_body::SizeHint {
283        self.inner.size_hint()
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    /// Feed `chunks` through a fresh parser with `max` as the size cap, returning the events in
292    /// order. `Message` events are mapped to `Ok(bytes)`, `Skipped` events to `Err(reason)`.
293    fn parse(chunks: &[&[u8]], max: usize) -> Vec<Result<Vec<u8>, Cow<'static, str>>> {
294        let mut parser = FrameParser::new();
295        let mut events = Vec::new();
296        for mut chunk in chunks.iter().copied() {
297            while !chunk.is_empty() {
298                let (consumed, completed) = parser.feed(chunk, max);
299                chunk = &chunk[consumed..];
300                match completed {
301                    Some(FrameEvent::Message(message)) => events.push(Ok(message)),
302                    Some(FrameEvent::Skipped { reason, .. }) => events.push(Err(reason)),
303                    None => {}
304                }
305            }
306        }
307        events
308    }
309
310    fn frame(flag: u8, message: &[u8]) -> Vec<u8> {
311        let mut frame = vec![flag];
312        frame.extend_from_slice(&(message.len() as u32).to_be_bytes());
313        frame.extend_from_slice(message);
314        frame
315    }
316
317    #[test]
318    fn single_frame_single_chunk() {
319        let events = parse(&[&frame(0, b"hello")], 1024);
320        assert_eq!(events, vec![Ok(b"hello".to_vec())]);
321    }
322
323    #[test]
324    fn empty_message() {
325        let events = parse(&[&frame(0, b"")], 1024);
326        assert_eq!(events, vec![Ok(Vec::new())]);
327    }
328
329    #[test]
330    fn frame_split_across_chunks() {
331        let bytes = frame(0, b"hello");
332        let chunks: Vec<&[u8]> = bytes.chunks(1).collect();
333        let events = parse(&chunks, 1024);
334        assert_eq!(events, vec![Ok(b"hello".to_vec())]);
335    }
336
337    #[test]
338    fn multiple_frames_in_one_chunk() {
339        let mut bytes = frame(0, b"one");
340        bytes.extend_from_slice(&frame(0, b"two"));
341        let events = parse(&[&bytes], 1024);
342        assert_eq!(events, vec![Ok(b"one".to_vec()), Ok(b"two".to_vec())]);
343    }
344
345    #[test]
346    fn oversized_message_skipped_next_frame_still_parses() {
347        let mut bytes = frame(0, b"too big for the cap");
348        bytes.extend_from_slice(&frame(0, b"ok"));
349        let events = parse(&[&bytes], 4);
350        assert_eq!(
351            events,
352            vec![Err("message too large".into()), Ok(b"ok".to_vec())]
353        );
354    }
355
356    #[test]
357    fn compressed_message_skipped_next_frame_still_parses() {
358        let mut bytes = frame(1, b"compressed");
359        bytes.extend_from_slice(&frame(0, b"ok"));
360        let events = parse(&[&bytes], 1024);
361        assert_eq!(
362            events,
363            vec![Err("compressed message".into()), Ok(b"ok".to_vec())]
364        );
365    }
366
367    #[test]
368    fn empty_compressed_message_skipped() {
369        let mut bytes = frame(1, b"");
370        bytes.extend_from_slice(&frame(0, b"ok"));
371        let events = parse(&[&bytes], 1024);
372        assert_eq!(
373            events,
374            vec![Err("compressed message".into()), Ok(b"ok".to_vec())]
375        );
376    }
377
378    #[test]
379    fn invalid_flag_poisons_parser() {
380        let mut bytes = frame(2, b"garbage");
381        bytes.extend_from_slice(&frame(0, b"never parsed"));
382        let events = parse(&[&bytes], 1024);
383        assert_eq!(events, vec![Err("invalid flag 2".into())]);
384    }
385
386    #[test]
387    fn truncated_trailing_bytes_emit_nothing() {
388        let bytes = frame(0, b"hello");
389        let events = parse(&[&bytes[..bytes.len() - 1]], 1024);
390        assert_eq!(events, Vec::new());
391    }
392}