Client should validate request URI. (#181)

This patch adds checks for the request URI and rejects invalid URIs. In
the case of forwarding an HTTP 1.1 request with a path, an "http" pseudo
header is added to satisfy the HTTP/2.0 spec.

Closes #179
This commit is contained in:
Carl Lerche
2017-12-11 13:42:00 -06:00
committed by GitHub
parent 71888acea5
commit 9378846da8
10 changed files with 164 additions and 53 deletions

View File

@@ -1,12 +1,12 @@
//! HTTP2 client side. //! HTTP2 client side.
use {SendStream, RecvStream, ReleaseCapacity}; use {SendStream, RecvStream, ReleaseCapacity};
use codec::{Codec, RecvError}; use codec::{Codec, RecvError, SendError, UserError};
use frame::{Headers, Pseudo, Reason, Settings, StreamId}; use frame::{Headers, Pseudo, Reason, Settings, StreamId};
use proto; use proto;
use bytes::{Bytes, IntoBuf}; use bytes::{Bytes, IntoBuf};
use futures::{Async, Future, MapErr, Poll}; use futures::{Async, Future, MapErr, Poll};
use http::{Request, Response}; use http::{uri, Request, Response, Method, Version};
use tokio_io::{AsyncRead, AsyncWrite}; use tokio_io::{AsyncRead, AsyncWrite};
use tokio_io::io::WriteAll; use tokio_io::io::WriteAll;
@@ -46,7 +46,11 @@ pub struct ResponseFuture {
/// Build a Client. /// Build a Client.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Builder { pub struct Builder {
/// Initial `Settings` frame to send as part of the handshake.
settings: Settings, settings: Settings,
/// The stream ID of the first (lowest) stream. Subsequent streams will use
/// monotonically increasing stream IDs.
stream_id: StreamId, stream_id: StreamId,
} }
@@ -359,20 +363,12 @@ impl Future for ResponseFuture {
// ===== impl Peer ===== // ===== impl Peer =====
impl proto::Peer for Peer { impl Peer {
type Send = Request<()>; pub fn convert_send_message(
type Poll = Response<()>; id: StreamId,
request: Request<()>,
end_of_stream: bool) -> Result<Headers, SendError>
fn dyn() -> proto::DynPeer { {
proto::DynPeer::Client
}
fn is_server() -> bool {
false
}
fn convert_send_message(id: StreamId, request: Self::Send, end_of_stream: bool) -> Headers {
use http::request::Parts; use http::request::Parts;
let ( let (
@@ -380,14 +376,45 @@ impl proto::Peer for Peer {
method, method,
uri, uri,
headers, headers,
version,
.. ..
}, },
_, _,
) = request.into_parts(); ) = request.into_parts();
let is_connect = method == Method::CONNECT;
// Build the set pseudo header set. All requests will include `method` // Build the set pseudo header set. All requests will include `method`
// and `path`. // and `path`.
let pseudo = Pseudo::request(method, uri); let mut pseudo = Pseudo::request(method, uri);
if pseudo.scheme.is_none() {
// If the scheme is not set, then there are a two options.
//
// 1) Authority is not set. In this case, a request was issued with
// a relative URI. This is permitted **only** when forwarding
// HTTP 1.x requests. If the HTTP version is set to 2.0, then
// this is an error.
//
// 2) Authority is set, then the HTTP method *must* be CONNECT.
//
// It is not possible to have a scheme but not an authority set (the
// `http` crate does not allow it).
//
if pseudo.authority.is_none() {
if version == Version::HTTP_2 {
return Err(UserError::MissingUriSchemeAndAuthority.into());
} else {
// This is acceptable as per the above comment. However,
// HTTP/2.0 requires that a scheme is set. Since we are
// forwarding an HTTP 1.1 request, the scheme is set to
// "http".
pseudo.set_scheme(uri::Scheme::HTTP);
}
} else if !is_connect {
// TODO: Error
}
}
// Create the HEADERS frame // Create the HEADERS frame
let mut frame = Headers::new(id, pseudo, headers); let mut frame = Headers::new(id, pseudo, headers);
@@ -396,7 +423,19 @@ impl proto::Peer for Peer {
frame.set_end_stream() frame.set_end_stream()
} }
frame Ok(frame)
}
}
impl proto::Peer for Peer {
type Poll = Response<()>;
fn dyn() -> proto::DynPeer {
proto::DynPeer::Client
}
fn is_server() -> bool {
false
} }
fn convert_poll_message(headers: Headers) -> Result<Self::Poll, RecvError> { fn convert_poll_message(headers: Headers) -> Result<Self::Poll, RecvError> {

View File

@@ -47,6 +47,9 @@ pub enum UserError {
/// Illegal headers, such as connection-specific headers. /// Illegal headers, such as connection-specific headers.
MalformedHeaders, MalformedHeaders,
/// Request submitted with relative URI.
MissingUriSchemeAndAuthority,
} }
// ===== impl RecvError ===== // ===== impl RecvError =====
@@ -125,6 +128,7 @@ impl error::Error for UserError {
ReleaseCapacityTooBig => "release capacity too big", ReleaseCapacityTooBig => "release capacity too big",
OverflowedStreamId => "stream ID overflowed", OverflowedStreamId => "stream ID overflowed",
MalformedHeaders => "malformed headers", MalformedHeaders => "malformed headers",
MissingUriSchemeAndAuthority => "request URI missing scheme and authority",
} }
} }
} }

View File

@@ -63,6 +63,7 @@ pub struct Continuation {
headers: Iter, headers: Iter,
} }
// TODO: These fields shouldn't be `pub`
#[derive(Debug, Default, Eq, PartialEq)] #[derive(Debug, Default, Eq, PartialEq)]
pub struct Pseudo { pub struct Pseudo {
// Request // Request
@@ -405,10 +406,6 @@ impl Pseudo {
pub fn request(method: Method, uri: Uri) -> Self { pub fn request(method: Method, uri: Uri) -> Self {
let parts = uri::Parts::from(uri); let parts = uri::Parts::from(uri);
fn to_string(src: Bytes) -> String<Bytes> {
unsafe { String::from_utf8_unchecked(src) }
}
let path = parts let path = parts
.path_and_query .path_and_query
.map(|v| v.into()) .map(|v| v.into())
@@ -426,7 +423,7 @@ impl Pseudo {
// //
// TODO: Scheme must be set... // TODO: Scheme must be set...
if let Some(scheme) = parts.scheme { if let Some(scheme) = parts.scheme {
pseudo.set_scheme(to_string(scheme.into())); pseudo.set_scheme(scheme);
} }
// If the URI includes an authority component, add it to the pseudo // If the URI includes an authority component, add it to the pseudo
@@ -448,8 +445,8 @@ impl Pseudo {
} }
} }
pub fn set_scheme(&mut self, scheme: String<Bytes>) { pub fn set_scheme(&mut self, scheme: uri::Scheme) {
self.scheme = Some(scheme); self.scheme = Some(to_string(scheme.into()));
} }
pub fn set_authority(&mut self, authority: String<Bytes>) { pub fn set_authority(&mut self, authority: String<Bytes>) {
@@ -457,6 +454,10 @@ impl Pseudo {
} }
} }
fn to_string(src: Bytes) -> String<Bytes> {
unsafe { String::from_utf8_unchecked(src) }
}
// ===== impl Iter ===== // ===== impl Iter =====
impl Iterator for Iter { impl Iterator for Iter {

View File

@@ -8,9 +8,6 @@ use std::fmt;
/// Either a Client or a Server /// Either a Client or a Server
pub trait Peer { pub trait Peer {
/// Message type sent into the transport
type Send;
/// Message type polled from the transport /// Message type polled from the transport
type Poll: fmt::Debug; type Poll: fmt::Debug;
@@ -18,8 +15,6 @@ pub trait Peer {
fn is_server() -> bool; fn is_server() -> bool;
fn convert_send_message(id: StreamId, headers: Self::Send, end_of_stream: bool) -> Headers;
fn convert_poll_message(headers: Headers) -> Result<Self::Poll, RecvError>; fn convert_poll_message(headers: Headers) -> Result<Self::Poll, RecvError>;
fn is_local_init(id: StreamId) -> bool { fn is_local_init(id: StreamId) -> bool {

View File

@@ -74,8 +74,6 @@ impl Send {
} }
} }
let end_stream = frame.is_end_stream(); let end_stream = frame.is_end_stream();
// Update the state // Update the state

View File

@@ -494,7 +494,8 @@ where
} }
// Convert the message // Convert the message
let headers = client::Peer::convert_send_message(stream_id, request, end_of_stream); let headers = client::Peer::convert_send_message(
stream_id, request, end_of_stream)?;
let mut stream = me.store.insert(stream.id, stream); let mut stream = me.store.insert(stream.id, stream);

View File

@@ -375,23 +375,12 @@ impl<T, B> fmt::Debug for Handshake<T, B>
} }
} }
impl proto::Peer for Peer { impl Peer {
type Send = Response<()>; pub fn convert_send_message(
type Poll = Request<()>;
fn is_server() -> bool {
true
}
fn dyn() -> proto::DynPeer {
proto::DynPeer::Server
}
fn convert_send_message(
id: StreamId, id: StreamId,
response: Self::Send, response: Response<()>,
end_of_stream: bool, end_of_stream: bool) -> frame::Headers
) -> frame::Headers { {
use http::response::Parts; use http::response::Parts;
// Extract the components of the HTTP request // Extract the components of the HTTP request
@@ -417,6 +406,18 @@ impl proto::Peer for Peer {
frame frame
} }
}
impl proto::Peer for Peer {
type Poll = Request<()>;
fn is_server() -> bool {
true
}
fn dyn() -> proto::DynPeer {
proto::DynPeer::Server
}
fn convert_poll_message(headers: frame::Headers) -> Result<Self::Poll, RecvError> { fn convert_poll_message(headers: frame::Headers) -> Result<Self::Poll, RecvError> {
use http::{uri, Version}; use http::{uri, Version};

View File

@@ -121,7 +121,6 @@ fn request_stream_id_overflows() {
.body(()) .body(())
.unwrap(); .unwrap();
// second cannot use the next stream id, it's over // second cannot use the next stream id, it's over
let poll_err = client.poll_ready().unwrap_err(); let poll_err = client.poll_ready().unwrap_err();
@@ -279,8 +278,71 @@ fn request_over_max_concurrent_streams_errors() {
} }
#[test] #[test]
#[ignore] fn http_11_request_without_scheme_or_authority() {
fn request_without_scheme() {} let _ = ::env_logger::init();
let (io, srv) = mock::new();
let srv = srv.assert_client_handshake()
.unwrap()
.recv_settings()
.recv_frame(
frames::headers(1)
.request("GET", "/")
.scheme("http")
.eos(),
)
.send_frame(frames::headers(1).response(200).eos())
.close();
let h2 = Client::handshake(io)
.expect("handshake")
.and_then(|(mut client, h2)| {
// we send a simple req here just to drive the connection so we can
// receive the server settings.
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(())
.unwrap();
// first request is allowed
let (response, _) = client.send_request(request, true).unwrap();
h2.drive(response)
.map(move |(h2, _)| (client, h2))
});
h2.join(srv).wait().expect("wait");
}
#[test]
fn http_2_request_without_scheme_or_authority() {
let _ = ::env_logger::init();
let (io, srv) = mock::new();
let srv = srv.assert_client_handshake()
.unwrap()
.recv_settings()
.close();
let h2 = Client::handshake(io)
.expect("handshake")
.and_then(|(mut client, h2)| {
// we send a simple req here just to drive the connection so we can
// receive the server settings.
let request = Request::builder()
.version(Version::HTTP_2)
.method(Method::GET)
.uri("/")
.body(())
.unwrap();
// first request is allowed
assert!(client.send_request(request, true).is_err());
h2.expect("h2")
});
h2.join(srv).wait().expect("wait");
}
#[test] #[test]
#[ignore] #[ignore]

View File

@@ -137,6 +137,16 @@ impl Mock<frame::Headers> {
Mock(frame) Mock(frame)
} }
pub fn scheme(self, value: &str) -> Self
{
let (id, mut pseudo, fields) = self.into_parts();
let value = value.parse().unwrap();
pseudo.set_scheme(value);
Mock(frame::Headers::new(id, pseudo, fields))
}
pub fn eos(mut self) -> Self { pub fn eos(mut self) -> Self {
self.0.set_end_stream(); self.0.set_end_stream();
self self

View File

@@ -32,7 +32,7 @@ pub use self::futures::{Future, IntoFuture, Sink, Stream};
pub use super::future_ext::{FutureExt, Unwrap}; pub use super::future_ext::{FutureExt, Unwrap};
// Re-export HTTP types // Re-export HTTP types
pub use self::http::{HeaderMap, Method, Request, Response, StatusCode}; pub use self::http::{HeaderMap, Method, Request, Response, StatusCode, Version};
pub use self::bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf}; pub use self::bytes::{Buf, BufMut, Bytes, BytesMut, IntoBuf};