Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 96 additions & 17 deletions crates/wasi-http/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,80 @@ pub enum Prepared<'a, T: 'static> {
},
}

/// A request authority is `uri-host [ ":" port ]`, without userinfo
/// (RFC 3986 section 3.2, RFC 9110 section 7.2).
fn validate_authority(authority: &http::uri::Authority) -> Result<()> {
if authority.as_str().contains('@') {
return Err(bad_request("HTTP request has an invalid authority"));
}
let host = authority.host();
if host.is_empty() {
return Err(bad_request("HTTP request has an invalid authority"));
}
// `Authority` stores the port as a string; a non-numeric or empty port
// parses as an authority with no port, so check the suffix directly.
let rest = &authority.as_str()[host.len()..];
if !rest.is_empty() && (!rest.starts_with(':') || rest[1..].parse::<u16>().is_err()) {
return Err(bad_request("HTTP request has an invalid authority"));
}
Ok(())
}

/// Determine the authority of the incoming request's target, validating `Host`
/// (RFC 9112 section 3.2) and its relation to `:authority` (RFC 9113 section 8.3.1).
fn get_request_target(request: &Request) -> Result<http::uri::Authority> {
use http::header::HOST;

let mut hosts = request.headers().get_all(HOST).iter();
let host = hosts.next();
if hosts.next().is_some() {
return Err(bad_request("HTTP request has multiple Host header fields"));
}
let host = match host {
Some(host) => {
let host = host
.to_str()
.map_err(|_| bad_request("HTTP request has a non-ASCII Host header value"))?;
let host = host
.parse::<http::uri::Authority>()
.map_err(|_| bad_request("HTTP request has an invalid Host header value"))?;
validate_authority(&host)?;
Some(host)
}
None => None,
};
let authority = request.uri().authority().cloned();
if let Some(authority) = &authority {
validate_authority(authority)?;
}
match (authority, host) {
(Some(authority), Some(host)) => {
// When both are present HTTP/2 requires them to agree; HTTP/1.1
// absolute-form instead ignores `Host` (RFC 9112 section 3.2.2).
if request.version() == http::Version::HTTP_2
&& !authority.as_str().eq_ignore_ascii_case(host.as_str())
{
return Err(bad_request("HTTP request has an invalid Host header value"));
}
Ok(authority)
}
(Some(authority), None) => {
// HTTP/1.1 requires `Host` even for absolute-form; HTTP/2 does not.
if request.version() == http::Version::HTTP_2 {
Ok(authority)
} else {
Err(bad_request("HTTP request is missing a Host header field"))
}
}
(None, Some(host)) => Ok(host),
(None, None) => Err(bad_request("HTTP request is missing a Host header field")),
}
}

fn bad_request(message: &'static str) -> wasmtime::Error {
wasmtime::Error::msg(message).context(crate::ErrorResponse::new(http::StatusCode::BAD_REQUEST))
}

impl<'a, T: Send> Prepared<'a, T> {
/// Creates a new prepared request.
pub fn new(
Expand All @@ -1004,8 +1078,17 @@ impl<'a, T: Send> Prepared<'a, T> {
match proxy {
#[cfg(feature = "p3")]
Proxy::P3(guest) => {
use http::header::HOST;
use http::uri::Authority;
let authority = match get_request_target(&request) {
Ok(authority) => authority,
Err(e) => {
// The request never reached the guest, so report the
// failure through `tx` like the p2 path does.
_ = tx.send(Err(e));
wasmtime::bail!(
"request was rejected before it could be turned into a guest request"
);
}
};

let (parts, body) = request.into_parts();
let http::request::Parts {
Expand All @@ -1014,28 +1097,15 @@ impl<'a, T: Send> Prepared<'a, T> {
headers: request_headers,
..
} = parts;
let http::uri::Parts {
authority,
path_and_query,
..
} = uri.into_parts();
// HTTP/1.1 requires an authority in the request target or the
// `Host` header (RFC 9112 section 3.2.2), so fall back to the latter.
let authority = match authority {
Some(authority) => Some(authority),
None => request_headers
.get(HOST)
.and_then(|host| host.to_str().ok())
.and_then(|host| host.parse::<Authority>().ok()),
};
let http::uri::Parts { path_and_query, .. } = uri.into_parts();
let hooks = view(store.data_mut()).hooks;
let headers = crate::FieldMap::new_immutable(hooks, request_headers);
// `serve` speaks plaintext HTTP, so that is the scheme of every
// request, whatever scheme its request target names (as in p2).
let (request, request_io_result) = p3::Request::new(
method,
Some(http::uri::Scheme::HTTP),
authority,
Some(authority),
path_and_query,
headers,
None,
Expand All @@ -1056,6 +1126,15 @@ impl<'a, T: Send> Prepared<'a, T> {
}
#[cfg(feature = "p2")]
Proxy::P2(guest) => {
// The p2 path builds its own copy of the request, so only the
// request-target checks of the helper are used here.
if let Err(e) = get_request_target(&request) {
_ = tx.send(Err(e));
wasmtime::bail!(
"request was rejected before it could be turned into a guest request"
);
}

// Here we wrap the sender in an `Arc<Mutex<Option<_>>>`, with one
// clone used in the `response-outparam` and the other used to send
// an error if the request expires or the handler returns without
Expand Down
192 changes: 192 additions & 0 deletions tests/all/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,20 @@ mod test_programs {
send: &mut hyper::client::conn::http1::SendRequest<String>,
req: http::Request<String>,
) -> Result<http::Response<String>> {
// RFC 9112 section 3.2: a client sends `Host` in every HTTP/1.1
// request, absolute-form included, so fill it in from the target.
let has_host = req.headers().contains_key(http::header::HOST);
let authority = req.uri().authority().cloned();
let req = match (has_host, authority) {
(false, Some(authority)) => {
let mut req = req;
let value = http::header::HeaderValue::from_str(authority.as_str())
.context("failed to make Host header value")?;
req.headers_mut().insert(http::header::HOST, value);
req
}
_ => req,
};
let response = send
.send_request(req)
.await
Expand Down Expand Up @@ -2210,6 +2224,127 @@ start a print 1234
Ok(())
}

/// Write a raw HTTP request to a serve instance and read the whole response.
async fn serve_raw_request(server: &WasmtimeServe, request: &[u8]) -> Result<Vec<u8>> {
let mut stream = TcpStream::connect(server.first_addr()).await?;
stream.write_all(request).await?;
let mut response = Vec::new();
tokio::time::timeout(Duration::from_secs(10), stream.read_to_end(&mut response))
.await
.expect("timed out waiting for a response")?;
Ok(response)
}

Comment thread
xia-chao marked this conversation as resolved.
Resolved
/// Send the RFC 9112 section 3.2 request table to `server`: one request with
/// a valid `Host` and the requests that must be rejected with a 400.
async fn test_rfc_9112_sec_3_2(server: &WasmtimeServe) -> Result<()> {
let cases: &[(&str, &[u8], &[u8])] = &[
(
"valid host",
b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 200",
),
(
"two hosts",
b"GET / HTTP/1.1\r\nHost: a.example\r\nHost: b.example\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"non-ascii host",
b"GET / HTTP/1.1\r\nHost: \x80\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"host with userinfo",
b"GET / HTTP/1.1\r\nHost: user@evil.com\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"no host",
b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"absolute-form without host",
b"GET http://localhost/ HTTP/1.1\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"absolute-form",
b"GET http://localhost/ HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 200",
),
(
"host with bad port",
b"GET / HTTP/1.1\r\nHost: example.com:abc\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"host with empty port",
b"GET / HTTP/1.1\r\nHost: example.com:\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"host with out of range port",
b"GET / HTTP/1.1\r\nHost: example.com:65536\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"host with empty host",
b"GET / HTTP/1.1\r\nHost: :8080\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
(
"host with port",
b"GET / HTTP/1.1\r\nHost: example.com:8080\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 200",
),
(
"host with ipv6",
b"GET / HTTP/1.1\r\nHost: [::1]\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 200",
),
(
"absolute-form with userinfo",
b"GET http://user@localhost/ HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
b"HTTP/1.1 400",
),
];
for (name, request, expected) in cases {
let response = serve_raw_request(server, request).await?;
assert!(
response.starts_with(expected),
"{name}: expected {:?}, got {:?}",
String::from_utf8_lossy(expected),
String::from_utf8_lossy(&response),
);
}

Ok(())
}

#[tokio::test]
async fn p2_cli_serve_rfc_9112_sec_3_2() -> Result<()> {
let server = WasmtimeServe::new(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, |cmd| {
cmd.arg("-Scli");
})?;
test_rfc_9112_sec_3_2(&server).await?;
server.finish()?;
Ok(())
}

#[tokio::test]
#[cfg_attr(not(feature = "component-model-async"), ignore)]
async fn p3_cli_serve_rfc_9112_sec_3_2() -> Result<()> {
let server = WasmtimeServe::new(P3_CLI_SERVE_HELLO_WORLD_COMPONENT, |cmd| {
cmd.arg("-Wcomponent-model-async");
cmd.arg("-Sp3,cli");
})?;
test_rfc_9112_sec_3_2(&server).await?;
server.finish()?;
Ok(())
}

#[tokio::test]
async fn p2_cli_serve_missing_authority() -> Result<()> {
let server = WasmtimeServe::new(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, |cmd| {
Expand Down Expand Up @@ -2909,6 +3044,63 @@ start a print 1234
Ok(())
}

async fn h2_request_status(uri: &str, hosts: &[&str]) -> Result<u16> {
let server = WasmtimeServe::new(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, |cmd| {
cmd.arg("-Scli");
})?;
let tcp = TcpStream::connect(server.first_addr()).await?;
let (mut send, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor)
.handshake(wasmtime_wasi_http::io::TokioIo::new(tcp))
.await
.context("failed http2 handshake")?;
let mut conn_task = tokio::task::spawn(conn);
let mut request = hyper::Request::builder()
.uri(uri)
.body(String::new())
.context("failed to make request")?;
for host in hosts {
request.headers_mut().append("host", host.parse()?);
}
let status = send
.send_request(request)
.await
.context("error sending request")?
.status()
.as_u16();
drop(send);
if tokio::time::timeout(Duration::from_secs(5), &mut conn_task)
.await
.is_err()
{
conn_task.abort();
}
server.finish()?;
Ok(status)
}

#[tokio::test]
async fn p2_cli_serve_host_validation_http2() -> Result<()> {
assert_eq!(h2_request_status("http://a.example/", &[]).await?, 200);
assert_eq!(
h2_request_status("http://a.example/", &["a.example", "a.example"]).await?,
400
);
assert_eq!(
h2_request_status("http://a.example/", &["user@a.example"]).await?,
400
);
assert_eq!(
h2_request_status("http://a.example/", &["a.example"]).await?,
200
);
assert_eq!(
h2_request_status("http://a.example/", &["b.example"]).await?,
400
);
assert_eq!(h2_request_status("http://user@a.example/", &[]).await?, 400);
Ok(())
}

#[tokio::test]
async fn p2_cli_serve_hello_world() -> Result<()> {
cli_serve_hello_world(P2_CLI_SERVE_HELLO_WORLD_COMPONENT, 1, 1, |cmd| {
Expand Down
1 change: 1 addition & 0 deletions tests/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ async fn send_request(addr: SocketAddr, req: HttpRequest) -> Result<()> {
http::Request::builder()
.method(http::Method::from(req.method))
.uri(req.path)
.header(http::header::HOST, "localhost")
.body(String::new())
.unwrap(),
)
Expand Down
Loading