Skip to content

Commit 4a181fb

Browse files
committed
gRPC: add compression support
1 parent 8749adb commit 4a181fb

5 files changed

Lines changed: 90 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mitmproxy-contentviews/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde_yaml = "0.9"
2222
rmp-serde = "1.1"
2323
protobuf = "3.7.2"
2424
regex = "1.10.3"
25+
flate2 = "1.0.28"
2526

2627
[dev-dependencies]
2728
criterion = "0.5.1"

mitmproxy-contentviews/src/grpc.rs

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::{Metadata, Prettify, Protobuf, Reencode};
22
use anyhow::{bail, Context, Result};
3+
use flate2::read::{DeflateDecoder, GzDecoder};
34
use mitmproxy_highlight::Language;
45
use serde::Deserialize;
56
use serde_yaml::Value;
7+
use std::io::Read;
68

79
pub struct GRPC;
810

@@ -29,20 +31,34 @@ impl Prettify for GRPC {
2931
_ => bail!("invalid gRPC: first byte is not a boolean"),
3032
};
3133
let Some(proto) = data.get(5..5 + len) else {
32-
bail!("Invald gRPC: not enough data")
34+
bail!("Invalid gRPC: not enough data")
3335
};
34-
if compressed {
35-
todo!();
36-
}
37-
protos.push(proto);
36+
37+
let mut decompressed = Vec::new();
38+
let proto = if compressed {
39+
let encoding = metadata.get_header("grpc-encoding").unwrap_or_default();
40+
match encoding.as_str() {
41+
"deflate" => {
42+
let mut decoder = DeflateDecoder::new(proto);
43+
decoder.read_to_end(&mut decompressed)?;
44+
&decompressed
45+
}
46+
"gzip" => {
47+
let mut decoder = GzDecoder::new(proto);
48+
decoder.read_to_end(&mut decompressed)?;
49+
&decompressed
50+
}
51+
"identity" => proto,
52+
_ => bail!("unsupported compression: {}", encoding),
53+
}
54+
} else {
55+
proto
56+
};
57+
protos.push(Protobuf.prettify(proto, metadata)?);
3858
data = &data[5 + len..];
3959
}
4060

41-
let prettified = protos
42-
.into_iter()
43-
.map(|proto| Protobuf.prettify(proto, metadata))
44-
.collect::<Result<Vec<String>>>()?;
45-
Ok(prettified.join("\n---\n\n"))
61+
Ok(protos.join("\n---\n\n"))
4662
}
4763

4864
fn render_priority(&self, _data: &[u8], metadata: &dyn Metadata) -> f64 {
@@ -61,7 +77,7 @@ impl Reencode for GRPC {
6177
for document in serde_yaml::Deserializer::from_str(data) {
6278
let value = Value::deserialize(document).context("Invalid YAML")?;
6379
let proto = super::protobuf::reencode::reencode_yaml(value, metadata)?;
64-
ret.push(0); // compressed
80+
ret.push(0); // uncompressed
6581
ret.extend(u32::to_be_bytes(proto.len() as u32));
6682
ret.extend(proto);
6783
}
@@ -76,13 +92,24 @@ mod tests {
7692

7793
const TEST_YAML: &str = "1: 150\n\n---\n\n1: 150\n";
7894
const TEST_GRPC: &[u8] = &[
79-
0, 0, 0, 0, 3, 8, 150, 1, // first message
80-
0, 0, 0, 0, 3, 8, 150, 1, // second message
95+
0, 0, 0, 0, 3, 8, 150, 1, // first message
96+
0, 0, 0, 0, 3, 8, 150, 1, // second message
97+
];
98+
99+
const TEST_GZIP: &[u8] = &[
100+
1, 0, 0, 0, 23, // compressed flag and length
101+
31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 227, 152, 198, 8, 0, 160, 149, 78, 161, 3, 0, 0,
102+
0, // gzip data
103+
];
104+
105+
const TEST_DEFLATE: &[u8] = &[
106+
1, 0, 0, 0, 5, // compressed flag and length
107+
227, 152, 198, 8, 0, // deflate data
81108
];
82109

83110
#[test]
84111
fn test_empty() {
85-
let res = GRPC.prettify(&vec![], &TestMetadata::default()).unwrap();
112+
let res = GRPC.prettify(&[], &TestMetadata::default()).unwrap();
86113
assert_eq!(res, "");
87114
}
88115

@@ -92,6 +119,20 @@ mod tests {
92119
assert_eq!(res, TEST_YAML);
93120
}
94121

122+
#[test]
123+
fn test_prettify_gzip() {
124+
let metadata = TestMetadata::default().with_header("grpc-encoding", "gzip");
125+
let res = GRPC.prettify(TEST_GZIP, &metadata).unwrap();
126+
assert_eq!(res, "1: 150\n");
127+
}
128+
129+
#[test]
130+
fn test_prettify_deflate() {
131+
let metadata = TestMetadata::default().with_header("grpc-encoding", "deflate");
132+
let res = GRPC.prettify(TEST_DEFLATE, &metadata).unwrap();
133+
assert_eq!(res, "1: 150\n");
134+
}
135+
95136
#[test]
96137
fn test_reencode_two_messages() {
97138
let res = GRPC.reencode(TEST_YAML, &TestMetadata::default()).unwrap();
@@ -100,7 +141,19 @@ mod tests {
100141

101142
#[test]
102143
fn test_render_priority() {
103-
assert_eq!(GRPC.render_priority(b"", &TestMetadata::default().with_content_type("application/grpc")), 1.0);
104-
assert_eq!(GRPC.render_priority(b"", &TestMetadata::default().with_content_type("text/plain")), 0.0);
144+
assert_eq!(
145+
GRPC.render_priority(
146+
b"",
147+
&TestMetadata::default().with_content_type("application/grpc")
148+
),
149+
1.0
150+
);
151+
assert_eq!(
152+
GRPC.render_priority(
153+
b"",
154+
&TestMetadata::default().with_content_type("text/plain")
155+
),
156+
0.0
157+
);
105158
}
106159
}

mitmproxy-contentviews/src/lib.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use mitmproxy_highlight::Language;
1616
pub trait Metadata {
1717
/// The HTTP `content-type` of this message.
1818
fn content_type(&self) -> Option<&str>;
19+
/// Get an HTTP header value by name.
20+
/// `name` is case-insensitive.
21+
fn get_header(&self, name: &str) -> Option<String>;
1922
}
2023

2124
/// See https://docs.mitmproxy.org/dev/api/mitmproxy/contentviews.html
@@ -56,20 +59,28 @@ pub mod test {
5659
#[derive(Default)]
5760
pub struct TestMetadata {
5861
pub content_type: Option<String>,
62+
pub headers: std::collections::HashMap<String, String>,
5963
}
6064

6165
impl TestMetadata {
6266
pub fn with_content_type(mut self, content_type: &str) -> Self {
6367
self.content_type = Some(content_type.to_string());
6468
self
6569
}
70+
71+
pub fn with_header(mut self, name: &str, value: &str) -> Self {
72+
self.headers.insert(name.to_lowercase(), value.to_string());
73+
self
74+
}
6675
}
6776

6877
impl Metadata for TestMetadata {
6978
fn content_type(&self) -> Option<&str> {
7079
self.content_type.as_deref()
7180
}
72-
}
73-
7481

82+
fn get_header(&self, name: &str) -> Option<String> {
83+
self.headers.get(&name.to_lowercase()).cloned()
84+
}
85+
}
7586
}

mitmproxy-rs/src/contentview.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ impl Metadata for PythonMetadata<'_> {
2828
})
2929
.as_deref()
3030
}
31+
32+
fn get_header(&self, name: &str) -> Option<String> {
33+
let http_message = self.inner.getattr("http_message").ok()?;
34+
let headers = http_message.getattr("headers").ok()?;
35+
headers.get_item(name).ok()?.extract::<String>().ok()
36+
}
3137
}
3238

3339
impl<'py> FromPyObject<'py> for PythonMetadata<'py> {

0 commit comments

Comments
 (0)