#![cfg(feature = "grpc")]
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::timeout;
mod helpers;
use helpers::mock_h2_server::{DecodedHeadersFrame, MockH2Connection, MockH2Server};
use helpers::tls::generate_cert_bundle;
use specter::grpc::{grpc_request, GrpcEncoding};
use specter::transport::h2::hpack_impl::Encoder;
use specter::Client;
fn h2_tls_setup() -> (boring::ssl::SslAcceptor, Client) {
let (mut builder, ca_cert) = generate_cert_bundle();
builder.set_alpn_select_callback(|_, client_protos| {
boring::ssl::select_next_proto(b"\x02h2", client_protos)
.ok_or(boring::ssl::AlpnError::NOACK)
});
let acceptor = builder.build();
let client = Client::builder()
.add_root_certificate(ca_cert)
.prefer_http2(true)
.build()
.unwrap();
(acceptor, client)
}
fn index_of(frame: &DecodedHeadersFrame, name: &str) -> Option<usize> {
frame
.headers
.iter()
.position(|(key, _)| key.eq_ignore_ascii_case(name))
}
fn assert_pseudo_order_and_target(frame: &DecodedHeadersFrame, expected_path: &str) {
assert_eq!(
frame.headers[0].0, ":method",
"first pseudo must be :method; got {:?}",
frame.headers
);
assert_eq!(frame.headers[0].1, "POST", "gRPC request must be POST");
assert_eq!(frame.headers[1].0, ":scheme");
assert_eq!(frame.headers[2].0, ":authority");
assert_eq!(frame.headers[3].0, ":path");
assert_eq!(
frame.headers[3].1, expected_path,
":path must be the service/method path"
);
}
async fn capture_request_headers(encoding: GrpcEncoding, path: &str) -> DecodedHeadersFrame {
let (acceptor, client) = h2_tls_setup();
let server = MockH2Server::new().await.unwrap();
let url = server.url_tls();
let (tx, mut rx) = mpsc::channel::<DecodedHeadersFrame>(1);
server.start_tls(acceptor, move |conn: MockH2Connection| {
let tx = tx.clone();
async move {
conn.read_preface().await.unwrap();
conn.send_settings(&[(0x01, 4096), (0x03, 100), (0x04, 65535)])
.await
.unwrap();
conn.send_settings_ack().await.unwrap();
let decoded = match conn.read_decoded_headers().await {
Ok(d) => d,
Err(_) => return,
};
let stream_id = decoded.stream_id;
let _ = tx.send(decoded).await;
let mut encoder = Encoder::new();
let response_headers = encoder.encode(&[
(b":status".as_slice(), b"200".as_slice()),
(
b"content-type".as_slice(),
b"application/grpc+proto".as_slice(),
),
]);
conn.send_headers(stream_id, &response_headers, false, true)
.await
.unwrap();
conn.send_data(stream_id, b"\x00\x00\x00\x00\x00", false)
.await
.unwrap();
let trailers = encoder.encode(&[(b"grpc-status".as_slice(), b"0".as_slice())]);
conn.send_headers(stream_id, &trailers, true, true)
.await
.unwrap();
}
});
let req_url = format!("{}{}", url, path);
let send = grpc_request(&client, &req_url, encoding).send_streaming();
let mut response = timeout(Duration::from_secs(5), send)
.await
.unwrap()
.unwrap();
assert_eq!(response.status().as_u16(), 200);
while let Some(frame) = response.body_mut().frame().await {
let _ = frame;
}
timeout(Duration::from_secs(5), rx.recv())
.await
.expect("server must observe the request HEADERS frame")
.expect("headers channel must yield the decoded frame")
}
#[tokio::test]
async fn grpc_request_identity_shape() {
let path = "/pkg.Svc/Method";
let frame = capture_request_headers(GrpcEncoding::Identity, path).await;
assert_pseudo_order_and_target(&frame, path);
assert_eq!(
frame.header("content-type"),
Some("application/grpc+proto"),
"decoded: {:?}",
frame.headers
);
assert_eq!(frame.header("te"), Some("trailers"));
let ct = index_of(&frame, "content-type").expect("content-type present");
let te = index_of(&frame, "te").expect("te present");
assert!(
ct < te,
"content-type must precede te; decoded: {:?}",
frame.headers
);
assert!(
!frame.has_header("grpc-encoding"),
"identity request must not carry grpc-encoding; decoded: {:?}",
frame.headers
);
}
#[tokio::test]
async fn grpc_request_gzip_shape() {
let path = "/helloworld.Greeter/SayHello";
let frame = capture_request_headers(GrpcEncoding::Gzip, path).await;
assert_pseudo_order_and_target(&frame, path);
assert_eq!(frame.header("content-type"), Some("application/grpc+proto"));
assert_eq!(frame.header("te"), Some("trailers"));
assert_eq!(frame.header("grpc-encoding"), Some("gzip"));
let ct = index_of(&frame, "content-type").expect("content-type present");
let te = index_of(&frame, "te").expect("te present");
let enc = index_of(&frame, "grpc-encoding").expect("grpc-encoding present");
assert!(
ct < te && te < enc,
"caller header order must be content-type < te < grpc-encoding; decoded: {:?}",
frame.headers
);
}