hyperium / h2

HTTP 2.0 client & server implementation for Rust.
MIT License
1.34k stars 265 forks source link

:scheme & :path pseudo-headers are included in CONNECT requests. #769

Closed mstyura closed 1 month ago

mstyura commented 2 months ago

CONNECT request sent over h2 connection has both :scheme and :path headers leading to protocol error response from server. Such requests are rejected by hyper on server side:

2024-04-23T14:25:25.873989Z DEBUG  actix-server worker 6 ThreadId(20) h2::server: 1567: malformed headers: :scheme in CONNECT
2024-04-23T14:25:25.874000Z TRACE  actix-server worker 6 ThreadId(20) h2::proto::streams::send: 185: send_reset(..., reason=PROTOCOL_ERROR, initiator=Library, stream=StreamId(1), ..., is_reset=false; is_closed=false; pending_send.is_empty=true; state=State { inner: Open { local: AwaitingHeaders, remote: Streaming } } 
2024-04-23T14:25:25.874020Z TRACE  actix-server worker 6 ThreadId(20) h2::proto::streams::send: 230: send_reset -- queueing; frame=Reset { stream_id: StreamId(1), error_code: PROTOCOL_ERROR }

This happen due to violation of 8.5. The CONNECT Method:

The ":scheme" and ":path" pseudo-header fields MUST be omitted

image

This was reproduced with roughly such code:

let proxy_io = hyper_util::rt::TokioIo::new(tcp_stream);

let (mut sender, conn) =
    hyper::client::conn::http2::handshake(hyper_util::rt::TokioExecutor::new(), proxy_io)
        .await
        .context("HTTP2 handshake created")?;

tokio::task::spawn(async move {
    if let Err(err) = conn.await {
        tracing::error!("Connection failed: {:?}", err);
    }
});

let uri = hyper::Uri::builder()
    .scheme("https".parse::<http::uri::Scheme>().unwrap())
    .authority("tunnel.example.com")
    .path_and_query("/")
    .build()
    .context("Gateway URI constructed")?;

let connect_req = hyper::Request::builder()
    .uri(uri)
    .method(hyper::Method::CONNECT)
    .body(http_body_util::Empty::<hyper::body::Bytes>::new())
    .context("Request constructed")?;

let connect_resp = sender
    .send_request(connect_req)
    .await
    .context("Request was sent")?;

Even when URI is constructed with "none" scheme and empty path_and_query the request on wire still has both fields:

let uri = hyper::Uri::builder()
    .scheme("".parse::<http::uri::Scheme>().unwrap())
    .authority("tunnel.example.com")
    .path_and_query("")
    .build()
    .context("Gateway URI constructed")?;
image
mstyura commented 2 months ago

Hello @seanmonstar! Could you please guide me to the appropriate place in the codebase where to write test for current misbehaviour which was fixed?