Skip to main content

ntex_grpc/
utils.rs

1use std::{borrow::Cow, fmt::Write, mem};
2
3use base64::engine::Engine;
4use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
5use ntex_bytes::{BytePages, Bytes, BytesMut};
6#[cfg(not(feature = "compression"))]
7use ntex_http::HeaderMap;
8use ntex_http::HeaderValue;
9use urly::quoting::{Component, unquote};
10
11use crate::GrpcStatus;
12
13/// Message compression, an uninhabited type without the `compression`
14/// feature so callers do not need their own `cfg`s.
15#[cfg(feature = "compression")]
16pub(crate) use crate::Compression as Enc;
17
18#[cfg(not(feature = "compression"))]
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
20pub(crate) enum Enc {}
21
22#[cfg(not(feature = "compression"))]
23#[allow(clippy::unused_async)]
24impl Enc {
25    pub(crate) async fn compress(
26        self,
27        _: &mut BytePages,
28    ) -> Result<bool, (GrpcStatus, HeaderValue)> {
29        match self {}
30    }
31
32    pub(crate) fn header(self) -> HeaderValue {
33        match self {}
34    }
35
36    pub(crate) fn of_response(_: &HeaderMap) -> Option<Self> {
37        None
38    }
39}
40
41pub(crate) enum Data {
42    Chunk(Bytes),
43    MutChunk(BytesMut),
44    Empty,
45}
46
47impl Data {
48    pub(crate) fn get(&mut self) -> Bytes {
49        match mem::replace(self, Data::Empty) {
50            Data::Chunk(data) => data,
51            Data::MutChunk(data) => data.freeze(),
52            Data::Empty => Bytes::new(),
53        }
54    }
55
56    pub(crate) fn as_slice(&self) -> &[u8] {
57        match self {
58            Data::Chunk(data) => data,
59            Data::MutChunk(data) => data,
60            Data::Empty => &[],
61        }
62    }
63
64    pub(crate) fn truncate(&mut self, len: usize) {
65        match self {
66            Data::Chunk(data) => data.truncate(len),
67            Data::MutChunk(data) => data.truncate(len),
68            Data::Empty => {}
69        }
70    }
71
72    /// Appends a chunk of a grpc message.
73    ///
74    /// A single chunk is kept as it is. Once the length prefix has arrived
75    /// and the message is at most `limit` bytes, the buffer is sized for the
76    /// whole message, otherwise it grows as data arrives.
77    pub(crate) fn push(&mut self, data: Bytes, limit: usize) {
78        if data.is_empty() {
79            return;
80        }
81        let buf = match mem::replace(self, Data::Empty) {
82            Data::Chunk(cur) => {
83                // the first chunk is copied once, into a buffer for the message
84                let len = cur.len() + data.len();
85                let size = message_size(&cur, &data, limit).map_or(len, |s| s.max(len));
86                let mut buf = BytesMut::with_capacity(size);
87                buf.extend_from_slice(&cur);
88                buf.extend_from_slice(&data);
89                buf
90            }
91            Data::MutChunk(mut buf) => {
92                if let Some(size) = message_size(&buf, &data, limit) {
93                    buf.reserve_exact(size.saturating_sub(buf.len()));
94                }
95                buf.extend_from_slice(&data);
96                buf
97            }
98            Data::Empty => {
99                *self = Data::Chunk(data);
100                return;
101            }
102        };
103        *self = Data::MutChunk(buf);
104    }
105}
106
107/// The size of a message with its length prefix, if the prefix has arrived
108/// and the message is at most `limit` bytes.
109fn message_size(cur: &[u8], data: &[u8], limit: usize) -> Option<usize> {
110    let mut prefix = cur.iter().chain(data).copied();
111    let (_, a, b, c, d) = (
112        prefix.next()?,
113        prefix.next()?,
114        prefix.next()?,
115        prefix.next()?,
116        prefix.next()?,
117    );
118    let len = u32::from_be_bytes([a, b, c, d]) as usize;
119    (len <= limit).then_some(5 + len)
120}
121
122/// Puts the message prefix, the compressed flag and the length, in front of
123/// `msg` as a page of its own, the message is not copied.
124pub(crate) fn prepend_prefix(msg: &mut BytePages, compressed: bool, len: u32) {
125    let [a, b, c, d] = len.to_be_bytes();
126    msg.prepend(Bytes::copy_from_slice(&[u8::from(compressed), a, b, c, d]));
127}
128
129/// Returns the length prefix of a message, or the error message if the
130/// message must not be sent.
131pub(crate) fn send_size(len: usize, max_size: usize) -> Result<u32, HeaderValue> {
132    let msg = if len > max_size {
133        format!("grpc: trying to send message larger than max ({len} vs. {max_size})")
134    } else if let Ok(len) = u32::try_from(len) {
135        return Ok(len);
136    } else {
137        format!("grpc: message too large ({len} bytes)")
138    };
139    Err(HeaderValue::try_from(msg)
140        .unwrap_or_else(|_| HeaderValue::from_static("grpc: message too large")))
141}
142
143/// Compresses an encoded message if `enc` is set and puts the message prefix
144/// in front of it.
145///
146/// A small message, or one that does not get smaller, is sent uncompressed.
147/// The message must not be larger than `max_size` after compression.
148pub(crate) async fn frame_message(
149    mut msg: BytePages,
150    max_size: usize,
151    enc: Option<Enc>,
152) -> Result<BytePages, (GrpcStatus, HeaderValue)> {
153    let compressed = match enc {
154        Some(enc) => {
155            // the uncompressed message must fit the length prefix too
156            send_size(msg.len(), usize::MAX).map_err(|m| (GrpcStatus::ResourceExhausted, m))?;
157            enc.compress(&mut msg).await?
158        }
159        None => false,
160    };
161    let len = send_size(msg.len(), max_size).map_err(|m| (GrpcStatus::ResourceExhausted, m))?;
162    prepend_prefix(&mut msg, compressed, len);
163    Ok(msg)
164}
165
166/// Encode a binary metadata value.
167///
168/// gRPC sends values of headers whose name ends with `-bin` as base64,
169/// without padding.
170///
171/// ```
172/// let val = ntex_grpc::encode_binary_header(&[0xff, 0x00]);
173/// assert_eq!(val, "/wA");
174/// ```
175pub fn encode_binary_header(value: &[u8]) -> HeaderValue {
176    let encoded = STANDARD_NO_PAD.encode(value);
177    // SAFETY: base64 output is visible ASCII, which is a valid header value
178    unsafe { HeaderValue::from_shared_unchecked(encoded.into()) }
179}
180
181/// Decode a binary metadata value, the value of a header whose name ends
182/// with `-bin`.
183///
184/// Accepts base64 with or without padding. Returns `None` if the value is
185/// not valid base64, that includes several values joined with `,`, use
186/// [`decode_binary_header_values()`] for those.
187///
188/// ```
189/// use ntex_grpc::{HeaderValue, decode_binary_header};
190///
191/// let val = HeaderValue::from_static("/wA=");
192/// assert_eq!(decode_binary_header(&val).unwrap(), [0xff, 0x00]);
193/// ```
194pub fn decode_binary_header(value: &HeaderValue) -> Option<Vec<u8>> {
195    decode_base64(value.as_bytes())
196}
197
198/// Decode all binary metadata values of a header whose name ends with
199/// `-bin`.
200///
201/// A peer or proxy may join several values of a header into one, separated
202/// by `,`. Each is decoded like [`decode_binary_header()`], spaces around
203/// them are ignored. Returns `None` if any of them is not valid base64.
204///
205/// ```
206/// use ntex_grpc::{HeaderValue, decode_binary_header_values};
207///
208/// let val = HeaderValue::from_static("/wA=, AQ");
209/// assert_eq!(decode_binary_header_values(&val).unwrap(), [vec![0xff, 0x00], vec![0x01]]);
210/// ```
211pub fn decode_binary_header_values(value: &HeaderValue) -> Option<Vec<Vec<u8>>> {
212    value
213        .as_bytes()
214        .split(|b| *b == b',')
215        .map(|val| decode_base64(val.trim_ascii()))
216        .collect()
217}
218
219/// Decodes base64 with or without padding.
220fn decode_base64(value: &[u8]) -> Option<Vec<u8>> {
221    // padded base64 is a multiple of 4 bytes long
222    if value.len().is_multiple_of(4) {
223        STANDARD.decode(value).ok()
224    } else {
225        STANDARD_NO_PAD.decode(value).ok()
226    }
227}
228
229/// Why a message's compressed flag cannot be handled.
230enum FlagError {
231    /// The message is compressed with an encoding we do not support.
232    Unsupported(HeaderValue),
233    /// The flag is invalid, or set without a `grpc-encoding`.
234    Invalid(HeaderValue),
235}
236
237/// Checks the compressed flag of a message, only identity is supported.
238fn check_compressed_flag(flag: u8, enc: Option<&HeaderValue>) -> Result<(), FlagError> {
239    match flag {
240        0 => Ok(()),
241        1 => match enc {
242            Some(enc) if !enc.as_bytes().eq_ignore_ascii_case(b"identity") => Err(
243                FlagError::Unsupported(grpc_message("Unsupported grpc-encoding", enc)),
244            ),
245            _ => Err(FlagError::Invalid(HeaderValue::from_static(
246                "Compressed message without grpc-encoding",
247            ))),
248        },
249        _ => Err(FlagError::Invalid(
250            HeaderValue::try_from(format!("Invalid compressed flag {flag}"))
251                .unwrap_or_else(|_| HeaderValue::from_static("Invalid compressed flag")),
252        )),
253    }
254}
255
256/// Returns the message, decompressed if needed.
257///
258/// `unsupported` is the status of a message in an encoding we do not
259/// support. `enc` is the `grpc-encoding` header value. The decompressed
260/// message must not be larger than `max_size`.
261#[cfg_attr(not(feature = "compression"), allow(clippy::unused_async))]
262pub(crate) async fn read_message(
263    flag: u8,
264    enc: Option<HeaderValue>,
265    block: Bytes,
266    max_size: usize,
267    unsupported: GrpcStatus,
268) -> Result<Bytes, (GrpcStatus, HeaderValue)> {
269    #[cfg(feature = "compression")]
270    if flag == 1
271        && let Some(enc) = enc.as_ref().and_then(Enc::from_header)
272    {
273        return enc.decompress(block, max_size).await;
274    }
275    #[cfg(not(feature = "compression"))]
276    let _ = max_size;
277
278    match check_compressed_flag(flag, enc.as_ref()) {
279        Ok(()) => Ok(block),
280        Err(FlagError::Unsupported(msg)) => Err((unsupported, msg)),
281        Err(FlagError::Invalid(msg)) => Err((GrpcStatus::Internal, msg)),
282    }
283}
284
285/// `application/grpc`, optionally with a `+` or `;` suffix, in any case.
286pub(crate) fn is_grpc_content_type(ct: &[u8]) -> bool {
287    let prefix = b"application/grpc";
288    ct.len() >= prefix.len()
289        && ct[..prefix.len()].eq_ignore_ascii_case(prefix)
290        && matches!(ct.get(prefix.len()), None | Some(b'+' | b';'))
291}
292
293/// Encode a `grpc-message` value.
294///
295/// The gRPC spec sends the message percent-encoded: `%` and every byte that
296/// is not printable ASCII are written as `%XX`, using the UTF-8 bytes of
297/// the text.
298///
299/// ```
300/// let val = ntex_grpc::encode_grpc_message("100% café");
301/// assert_eq!(val, "100%25 caf%C3%A9");
302/// ```
303pub fn encode_grpc_message(msg: &str) -> HeaderValue {
304    let mut encoded = String::with_capacity(msg.len());
305    for b in msg.bytes() {
306        if b == b'%' || !(b' '..=b'~').contains(&b) {
307            let _ = write!(encoded, "%{b:02X}");
308        } else {
309            encoded.push(char::from(b));
310        }
311    }
312    // SAFETY: the output is printable ASCII, which is a valid header value
313    unsafe { HeaderValue::from_shared_unchecked(encoded.into()) }
314}
315
316/// Builds a `grpc-message` of `prefix: value`, percent-encoded.
317///
318/// The value is left out if it is not UTF-8.
319pub(crate) fn grpc_message(prefix: &'static str, val: &HeaderValue) -> HeaderValue {
320    match val.to_str() {
321        Ok(v) => encode_grpc_message(&format!("{prefix}: {v}")),
322        Err(_) => encode_grpc_message(prefix),
323    }
324}
325
326/// Decodes a percent-encoded `grpc-message`.
327///
328/// Invalid escapes are kept as they are, the spec says a bad message must
329/// not be dropped.
330pub(crate) fn percent_decode(val: &HeaderValue) -> Cow<'_, str> {
331    match val.to_str() {
332        Ok(s) => unquote(s, Component::Opaque),
333        // not printable ascii, so not encoded as the spec says
334        Err(_) => String::from_utf8_lossy(val.as_bytes()),
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn prefix() {
344        let mut msg = BytePages::default();
345        msg.extend_from_slice(&[7; 100]);
346        let body = msg.take().unwrap();
347        let ptr = body.as_ref().as_ptr();
348        msg.append(body);
349
350        prepend_prefix(&mut msg, true, 0x0102_0304);
351        assert_eq!(msg.len(), 105);
352        assert_eq!(msg.take().unwrap().as_ref(), &[1, 1, 2, 3, 4]);
353        // the message is not copied
354        let body = msg.take().unwrap();
355        assert_eq!(body.as_ref().as_ptr(), ptr);
356        assert_eq!(body.as_ref(), &[7; 100]);
357        assert!(msg.take().is_none());
358    }
359
360    fn push(data: &mut Data, chunk: &[u8]) {
361        data.push(Bytes::copy_from_slice(chunk), 100);
362    }
363
364    fn capacity(data: &Data) -> usize {
365        let Data::MutChunk(buf) = data else {
366            panic!("not a buffer")
367        };
368        buf.capacity()
369    }
370
371    #[test]
372    fn data_push() {
373        let msg = [&[0, 0, 0, 0, 100][..], &[1; 100]].concat();
374
375        // a single chunk is not copied
376        let chunk = Bytes::copy_from_slice(&msg);
377        let mut data = Data::Empty;
378        data.push(chunk.clone(), 100);
379        data.push(Bytes::new(), 100);
380        assert_eq!(data.get().as_ptr(), chunk.as_ptr());
381
382        // the buffer is sized once for the whole message
383        for split in [&[10, 50][..], &[2, 4, 50], &[1, 1, 1, 1, 1, 50]] {
384            let mut data = Data::Empty;
385            let mut pos = 0;
386            for &end in split {
387                push(&mut data, &msg[pos..end]);
388                pos = end;
389            }
390            assert_eq!(capacity(&data), 105);
391            let ptr = data.as_slice().as_ptr();
392            push(&mut data, &msg[pos..]);
393            assert_eq!(capacity(&data), 105);
394            assert_eq!(data.as_slice().as_ptr(), ptr);
395            assert_eq!(data.get(), msg);
396        }
397
398        // the prefix is not complete yet
399        let mut data = Data::Empty;
400        push(&mut data, &msg[..2]);
401        push(&mut data, &msg[2..4]);
402        assert_eq!(capacity(&data), 4);
403
404        // the reserved size does not depend on how the buffer grows
405        let mut data = Data::Empty;
406        push(&mut data, &msg[..60]);
407        push(&mut data, &msg[60..70]);
408        assert_eq!(capacity(&data), 105);
409
410        // a message over the limit is not reserved for
411        let big = [&[0, 0, 0, 0, 101][..], &[1; 101]].concat();
412        let mut data = Data::Empty;
413        push(&mut data, &big[..10]);
414        push(&mut data, &big[10..20]);
415        assert!(capacity(&data) < 106);
416        push(&mut data, &big[20..]);
417        assert_eq!(data.get(), big);
418
419        // data after the message
420        let msg = [&msg[..], &[1; 20]].concat();
421        let mut data = Data::Empty;
422        push(&mut data, &msg[..10]);
423        push(&mut data, &msg[10..]);
424        assert_eq!(capacity(&data), 125);
425        assert_eq!(data.get(), msg);
426    }
427
428    #[test]
429    fn binary_header() {
430        assert_eq!(encode_binary_header(b""), "");
431        assert_eq!(encode_binary_header(b"a"), "YQ");
432        assert_eq!(encode_binary_header(b"ab"), "YWI");
433        assert_eq!(encode_binary_header(b"abc"), "YWJj");
434
435        let dec = |v: &'static str| decode_binary_header(&HeaderValue::from_static(v));
436        assert_eq!(dec("YQ").unwrap(), b"a");
437        assert_eq!(dec("YQ==").unwrap(), b"a");
438        assert_eq!(dec("YWI=").unwrap(), b"ab");
439        assert_eq!(dec("YWJj").unwrap(), b"abc");
440        assert_eq!(dec("").unwrap(), b"");
441        assert_eq!(dec("/+8").unwrap(), [0xff, 0xef]);
442        // url-safe alphabet and bad padding are rejected
443        assert!(dec("_-8").is_none());
444        assert!(dec("YQ=").is_none());
445        assert!(dec("Y").is_none());
446        assert!(dec("YQ=\t").is_none());
447        assert!(dec("YQ,YQ").is_none());
448
449        let dec = |v: &'static str| decode_binary_header_values(&HeaderValue::from_static(v));
450        assert_eq!(dec("").unwrap(), [b""]);
451        assert_eq!(dec("YQ").unwrap(), [b"a"]);
452        assert_eq!(dec("YQ==,YWI").unwrap(), [&b"a"[..], b"ab"]);
453        assert_eq!(dec("YQ , YWJj,\tYWI=").unwrap(), [&b"a"[..], b"abc", b"ab"]);
454        assert_eq!(dec("YQ,").unwrap(), [&b"a"[..], b""]);
455        assert!(dec("YQ,Y").is_none());
456        assert!(dec("YQ;YQ").is_none());
457    }
458
459    #[test]
460    fn encode_message() {
461        assert_eq!(encode_grpc_message(""), "");
462        assert_eq!(encode_grpc_message("plain text ~!"), "plain text ~!");
463        assert_eq!(encode_grpc_message("100%"), "100%25");
464        assert_eq!(encode_grpc_message("a\tb\r\n\x7f"), "a%09b%0D%0A%7F");
465        assert_eq!(encode_grpc_message("\u{20ac}"), "%E2%82%AC");
466
467        // decoding gives the text back
468        for msg in ["", "100% done", "a\nb", "caf\u{e9} \u{20ac}", "%41%zz"] {
469            assert_eq!(percent_decode(&encode_grpc_message(msg)), msg);
470        }
471    }
472
473    #[test]
474    fn message_with_value() {
475        let val = |v: &'static [u8]| grpc_message("bad", &HeaderValue::from_bytes(v).unwrap());
476        assert_eq!(val(b"text/html"), "bad: text/html");
477        assert_eq!(val(b"100%\tx"), "bad: 100%25%09x");
478        assert_eq!(val("caf\u{e9}".as_bytes()), "bad: caf%C3%A9");
479        // not utf-8
480        assert_eq!(val(b"caf\xe9"), "bad");
481    }
482
483    #[test]
484    fn percent_decode_message() {
485        let dec = |v: &'static str| percent_decode(&HeaderValue::from_static(v)).into_owned();
486        let plain = HeaderValue::from_static("plain text");
487        assert!(matches!(
488            percent_decode(&plain),
489            Cow::Borrowed("plain text")
490        ));
491        assert_eq!(dec("100%25 done"), "100% done");
492        assert_eq!(dec("a%0Ab%0a"), "a\nb\n");
493        assert_eq!(dec("%E2%82%AC"), "\u{20ac}");
494        // `+` is not a space here
495        assert_eq!(dec("a+b"), "a+b");
496        // bad escapes stay as they are
497        assert_eq!(dec("%"), "%");
498        assert_eq!(dec("%4"), "%4");
499        assert_eq!(dec("%zz%41"), "%zzA");
500        assert_eq!(dec("%FFok"), "%FFok");
501        // obs-text is not valid, but still readable
502        let raw = HeaderValue::from_bytes(b"caf\xc3\xa9").unwrap();
503        assert_eq!(percent_decode(&raw), "caf\u{e9}");
504        // not utf-8 either
505        let raw = HeaderValue::from_bytes(b"caf\xe9").unwrap();
506        assert_eq!(percent_decode(&raw), "caf\u{fffd}");
507    }
508
509    #[test]
510    fn data_slice() {
511        let mut data = Data::Empty;
512        assert!(data.as_slice().is_empty());
513        data.truncate(5);
514        assert!(data.get().is_empty());
515
516        let mut data = Data::Chunk(Bytes::from_static(b"abcdef"));
517        assert_eq!(data.as_slice(), b"abcdef");
518        data.truncate(3);
519        assert_eq!(data.get(), Bytes::from_static(b"abc"));
520
521        let mut data = Data::Empty;
522        push(&mut data, b"abcdef");
523        data.truncate(2);
524        assert_eq!(data.as_slice(), b"ab");
525        assert_eq!(data.get(), Bytes::from_static(b"ab"));
526    }
527}