Skip to content

Commit 5442b6f

Browse files
authored
feat(http2): Implement Client-side CONNECT support over HTTP/2 (#2523)
Closes #2508
1 parent be9677a commit 5442b6f

File tree

10 files changed

+833
-78
lines changed

10 files changed

+833
-78
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ http = "0.2"
3131
http-body = "0.4"
3232
httpdate = "1.0"
3333
httparse = "1.4"
34-
h2 = { version = "0.3", optional = true }
34+
h2 = { version = "0.3.3", optional = true }
3535
itoa = "0.4.1"
3636
tracing = { version = "0.1", default-features = false, features = ["std"] }
3737
pin-project = "1.0"

src/body/length.rs

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ use std::fmt;
33
#[derive(Clone, Copy, PartialEq, Eq)]
44
pub(crate) struct DecodedLength(u64);
55

6+
#[cfg(any(feature = "http1", feature = "http2"))]
7+
impl From<Option<u64>> for DecodedLength {
8+
fn from(len: Option<u64>) -> Self {
9+
len.and_then(|len| {
10+
// If the length is u64::MAX, oh well, just reported chunked.
11+
Self::checked_new(len).ok()
12+
})
13+
.unwrap_or(DecodedLength::CHUNKED)
14+
}
15+
}
16+
617
#[cfg(any(feature = "http1", feature = "http2", test))]
718
const MAX_LEN: u64 = std::u64::MAX - 2;
819

src/client/client.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,9 @@ where
254254
absolute_form(req.uri_mut());
255255
} else {
256256
origin_form(req.uri_mut());
257-
};
257+
}
258258
} else if req.method() == Method::CONNECT {
259-
debug!("client does not support CONNECT requests over HTTP2");
260-
return Err(ClientError::Normal(
261-
crate::Error::new_user_unsupported_request_method(),
262-
));
259+
authority_form(req.uri_mut());
263260
}
264261

265262
let fut = pooled

src/error.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub(super) enum User {
9090
/// User tried to send a certain header in an unexpected context.
9191
///
9292
/// For example, sending both `content-length` and `transfer-encoding`.
93-
#[cfg(feature = "http1")]
93+
#[cfg(any(feature = "http1", feature = "http2"))]
9494
#[cfg(feature = "server")]
9595
UnexpectedHeader,
9696
/// User tried to create a Request with bad version.
@@ -290,7 +290,7 @@ impl Error {
290290
Error::new(Kind::User(user))
291291
}
292292

293-
#[cfg(feature = "http1")]
293+
#[cfg(any(feature = "http1", feature = "http2"))]
294294
#[cfg(feature = "server")]
295295
pub(super) fn new_user_header() -> Error {
296296
Error::new_user(User::UnexpectedHeader)
@@ -405,7 +405,7 @@ impl Error {
405405
Kind::User(User::MakeService) => "error from user's MakeService",
406406
#[cfg(any(feature = "http1", feature = "http2"))]
407407
Kind::User(User::Service) => "error from user's Service",
408-
#[cfg(feature = "http1")]
408+
#[cfg(any(feature = "http1", feature = "http2"))]
409409
#[cfg(feature = "server")]
410410
Kind::User(User::UnexpectedHeader) => "user sent unexpected header",
411411
#[cfg(any(feature = "http1", feature = "http2"))]

src/proto/h2/client.rs

+89-32
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@ use std::error::Error as StdError;
22
#[cfg(feature = "runtime")]
33
use std::time::Duration;
44

5+
use bytes::Bytes;
56
use futures_channel::{mpsc, oneshot};
67
use futures_util::future::{self, Either, FutureExt as _, TryFutureExt as _};
78
use futures_util::stream::StreamExt as _;
89
use h2::client::{Builder, SendRequest};
10+
use http::{Method, StatusCode};
911
use tokio::io::{AsyncRead, AsyncWrite};
1012

11-
use super::{decode_content_length, ping, PipeToSendStream, SendBuf};
13+
use super::{ping, H2Upgraded, PipeToSendStream, SendBuf};
1214
use crate::body::HttpBody;
1315
use crate::common::{exec::Exec, task, Future, Never, Pin, Poll};
1416
use crate::headers;
17+
use crate::proto::h2::UpgradedSendStream;
1518
use crate::proto::Dispatched;
19+
use crate::upgrade::Upgraded;
1620
use crate::{Body, Request, Response};
1721

1822
type ClientRx<B> = crate::client::dispatch::Receiver<Request<B>, Response<Body>>;
@@ -233,8 +237,25 @@ where
233237
headers::set_content_length_if_missing(req.headers_mut(), len);
234238
}
235239
}
240+
241+
let is_connect = req.method() == Method::CONNECT;
236242
let eos = body.is_end_stream();
237-
let (fut, body_tx) = match self.h2_tx.send_request(req, eos) {
243+
let ping = self.ping.clone();
244+
245+
if is_connect {
246+
if headers::content_length_parse_all(req.headers())
247+
.map_or(false, |len| len != 0)
248+
{
249+
warn!("h2 connect request with non-zero body not supported");
250+
cb.send(Err((
251+
crate::Error::new_h2(h2::Reason::INTERNAL_ERROR.into()),
252+
None,
253+
)));
254+
continue;
255+
}
256+
}
257+
258+
let (fut, body_tx) = match self.h2_tx.send_request(req, !is_connect && eos) {
238259
Ok(ok) => ok,
239260
Err(err) => {
240261
debug!("client send request error: {}", err);
@@ -243,45 +264,81 @@ where
243264
}
244265
};
245266

246-
let ping = self.ping.clone();
247-
if !eos {
248-
let mut pipe = Box::pin(PipeToSendStream::new(body, body_tx)).map(|res| {
249-
if let Err(e) = res {
250-
debug!("client request body error: {}", e);
251-
}
252-
});
253-
254-
// eagerly see if the body pipe is ready and
255-
// can thus skip allocating in the executor
256-
match Pin::new(&mut pipe).poll(cx) {
257-
Poll::Ready(_) => (),
258-
Poll::Pending => {
259-
let conn_drop_ref = self.conn_drop_ref.clone();
260-
// keep the ping recorder's knowledge of an
261-
// "open stream" alive while this body is
262-
// still sending...
263-
let ping = ping.clone();
264-
let pipe = pipe.map(move |x| {
265-
drop(conn_drop_ref);
266-
drop(ping);
267-
x
267+
let send_stream = if !is_connect {
268+
if !eos {
269+
let mut pipe =
270+
Box::pin(PipeToSendStream::new(body, body_tx)).map(|res| {
271+
if let Err(e) = res {
272+
debug!("client request body error: {}", e);
273+
}
268274
});
269-
self.executor.execute(pipe);
275+
276+
// eagerly see if the body pipe is ready and
277+
// can thus skip allocating in the executor
278+
match Pin::new(&mut pipe).poll(cx) {
279+
Poll::Ready(_) => (),
280+
Poll::Pending => {
281+
let conn_drop_ref = self.conn_drop_ref.clone();
282+
// keep the ping recorder's knowledge of an
283+
// "open stream" alive while this body is
284+
// still sending...
285+
let ping = ping.clone();
286+
let pipe = pipe.map(move |x| {
287+
drop(conn_drop_ref);
288+
drop(ping);
289+
x
290+
});
291+
self.executor.execute(pipe);
292+
}
270293
}
271294
}
272-
}
295+
296+
None
297+
} else {
298+
Some(body_tx)
299+
};
273300

274301
let fut = fut.map(move |result| match result {
275302
Ok(res) => {
276303
// record that we got the response headers
277304
ping.record_non_data();
278305

279-
let content_length = decode_content_length(res.headers());
280-
let res = res.map(|stream| {
281-
let ping = ping.for_stream(&stream);
282-
crate::Body::h2(stream, content_length, ping)
283-
});
284-
Ok(res)
306+
let content_length = headers::content_length_parse_all(res.headers());
307+
if let (Some(mut send_stream), StatusCode::OK) =
308+
(send_stream, res.status())
309+
{
310+
if content_length.map_or(false, |len| len != 0) {
311+
warn!("h2 connect response with non-zero body not supported");
312+
313+
send_stream.send_reset(h2::Reason::INTERNAL_ERROR);
314+
return Err((
315+
crate::Error::new_h2(h2::Reason::INTERNAL_ERROR.into()),
316+
None,
317+
));
318+
}
319+
let (parts, recv_stream) = res.into_parts();
320+
let mut res = Response::from_parts(parts, Body::empty());
321+
322+
let (pending, on_upgrade) = crate::upgrade::pending();
323+
let io = H2Upgraded {
324+
ping,
325+
send_stream: unsafe { UpgradedSendStream::new(send_stream) },
326+
recv_stream,
327+
buf: Bytes::new(),
328+
};
329+
let upgraded = Upgraded::new(io, Bytes::new());
330+
331+
pending.fulfill(upgraded);
332+
res.extensions_mut().insert(on_upgrade);
333+
334+
Ok(res)
335+
} else {
336+
let res = res.map(|stream| {
337+
let ping = ping.for_stream(&stream);
338+
crate::Body::h2(stream, content_length.into(), ping)
339+
});
340+
Ok(res)
341+
}
285342
}
286343
Err(err) => {
287344
ping.ensure_not_timed_out().map_err(|e| (e, None))?;

0 commit comments

Comments
 (0)