Begin implementation of wasi-http (#5929)
* Integrate experimental HTTP into wasmtime. * Reset Cargo.lock * Switch to bail!, plumb options partially. * Implement timeouts. * Remove generated files & wasm, add Makefile * Remove generated code textfile * Update crates/wasi-http/Cargo.toml Co-authored-by: Eduardo de Moura Rodrigues <16357187+eduardomourar@users.noreply.github.com> * Update crates/wasi-http/Cargo.toml Co-authored-by: Eduardo de Moura Rodrigues <16357187+eduardomourar@users.noreply.github.com> * Extract streams from request/response. * Fix read for len < buffer length. * Formatting. * types impl: swap todos for traps * streams_impl: idioms, and swap todos for traps * component impl: idioms, swap all unwraps for traps, swap all todos for traps * http impl: idiom * Remove an unnecessary mut. * Remove an unsupported function. * Switch to the tokio runtime for the HTTP request. * Add a rust example. * Update to latest wit definition * Remove example code. * wip: start writing a http test... * finish writing the outbound request example havent executed it yet * better debug output * wasi-http: some stubs required for rust rewrite of the example * add wasi_http tests to test-programs * CI: run the http tests * Fix some warnings. * bump new deps to latest releases (#3) * Add tests for wasi-http to test-programs (#2) * wip: start writing a http test... * finish writing the outbound request example havent executed it yet * better debug output * wasi-http: some stubs required for rust rewrite of the example * add wasi_http tests to test-programs * CI: run the http tests * bump new deps to latest releases h2 0.3.16 http 0.2.9 mio 0.8.6 openssl 0.10.48 openssl-sys 0.9.83 tokio 1.26.0 --------- Co-authored-by: Brendan Burns <bburns@microsoft.com> * Update crates/test-programs/tests/http_tests/runtime/wasi_http_tests.rs * Update crates/test-programs/tests/http_tests/runtime/wasi_http_tests.rs * Update crates/test-programs/tests/http_tests/runtime/wasi_http_tests.rs * wasi-http: fix cargo.toml file and publish script to work together (#4) unfortunately, the publish script doesn't use a proper toml parser (in order to not have any dependencies), so the whitespace has to be the trivial expected case. then, add wasi-http to the list of crates to publish. * Update crates/test-programs/build.rs * Switch to rustls * Cleanups. * Merge switch to rustls. * Formatting * Remove libssl install * Fix tests. * Rename wasi-http -> wasmtime-wasi-http * prtest:full Conditionalize TLS on riscv64gc. * prtest:full Fix formatting, also disable tls on s390x * prtest:full Add a path parameter to wit-bindgen, remove symlink. * prtest:full Fix tests for places where SSL isn't supported. * Update crates/wasi-http/Cargo.toml --------- Co-authored-by: Eduardo de Moura Rodrigues <16357187+eduardomourar@users.noreply.github.com> Co-authored-by: Pat Hickey <phickey@fastly.com> Co-authored-by: Pat Hickey <pat@moreproductive.org>
This commit is contained in:
211
crates/wasi-http/src/http_impl.rs
Normal file
211
crates/wasi-http/src/http_impl.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::r#struct::ActiveResponse;
|
||||
pub use crate::r#struct::WasiHttp;
|
||||
use crate::types::{RequestOptions, Scheme};
|
||||
#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
|
||||
use anyhow::anyhow;
|
||||
use anyhow::bail;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::Method;
|
||||
use hyper::Request;
|
||||
#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::time::timeout;
|
||||
#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
|
||||
use tokio_rustls::rustls::{self, OwnedTrustAnchor};
|
||||
|
||||
impl crate::default_outgoing_http::Host for WasiHttp {
|
||||
fn handle(
|
||||
&mut self,
|
||||
request_id: crate::default_outgoing_http::OutgoingRequest,
|
||||
options: Option<crate::default_outgoing_http::RequestOptions>,
|
||||
) -> wasmtime::Result<crate::default_outgoing_http::FutureIncomingResponse> {
|
||||
// TODO: Initialize this once?
|
||||
let rt = Runtime::new().unwrap();
|
||||
let _enter = rt.enter();
|
||||
|
||||
let f = self.handle_async(request_id, options);
|
||||
match rt.block_on(f) {
|
||||
Ok(r) => {
|
||||
println!("{} OK", r);
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} ERR", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn port_for_scheme(scheme: &Option<Scheme>) -> &str {
|
||||
match scheme {
|
||||
Some(s) => match s {
|
||||
Scheme::Http => ":80",
|
||||
Scheme::Https => ":443",
|
||||
// This should never happen.
|
||||
_ => panic!("unsupported scheme!"),
|
||||
},
|
||||
None => ":443",
|
||||
}
|
||||
}
|
||||
|
||||
impl WasiHttp {
|
||||
async fn handle_async(
|
||||
&mut self,
|
||||
request_id: crate::default_outgoing_http::OutgoingRequest,
|
||||
options: Option<crate::default_outgoing_http::RequestOptions>,
|
||||
) -> wasmtime::Result<crate::default_outgoing_http::FutureIncomingResponse> {
|
||||
let opts = options.unwrap_or(
|
||||
// TODO: Configurable defaults here?
|
||||
RequestOptions {
|
||||
connect_timeout_ms: Some(600 * 1000),
|
||||
first_byte_timeout_ms: Some(600 * 1000),
|
||||
between_bytes_timeout_ms: Some(600 * 1000),
|
||||
},
|
||||
);
|
||||
let connect_timeout =
|
||||
Duration::from_millis(opts.connect_timeout_ms.unwrap_or(600 * 1000).into());
|
||||
let first_bytes_timeout =
|
||||
Duration::from_millis(opts.first_byte_timeout_ms.unwrap_or(600 * 1000).into());
|
||||
let between_bytes_timeout =
|
||||
Duration::from_millis(opts.between_bytes_timeout_ms.unwrap_or(600 * 1000).into());
|
||||
|
||||
let request = match self.requests.get(&request_id) {
|
||||
Some(r) => r,
|
||||
None => bail!("not found!"),
|
||||
};
|
||||
|
||||
let method = match request.method {
|
||||
crate::types::Method::Get => Method::GET,
|
||||
crate::types::Method::Head => Method::HEAD,
|
||||
crate::types::Method::Post => Method::POST,
|
||||
crate::types::Method::Put => Method::PUT,
|
||||
crate::types::Method::Delete => Method::DELETE,
|
||||
crate::types::Method::Connect => Method::CONNECT,
|
||||
crate::types::Method::Options => Method::OPTIONS,
|
||||
crate::types::Method::Trace => Method::TRACE,
|
||||
crate::types::Method::Patch => Method::PATCH,
|
||||
_ => bail!("unknown method!"),
|
||||
};
|
||||
|
||||
let scheme = match request.scheme.as_ref().unwrap_or(&Scheme::Https) {
|
||||
Scheme::Http => "http://",
|
||||
Scheme::Https => "https://",
|
||||
// TODO: this is wrong, fix this.
|
||||
_ => panic!("Unsupported scheme!"),
|
||||
};
|
||||
|
||||
// Largely adapted from https://hyper.rs/guides/1/client/basic/
|
||||
let authority = match request.authority.find(":") {
|
||||
Some(_) => request.authority.clone(),
|
||||
None => request.authority.clone() + port_for_scheme(&request.scheme),
|
||||
};
|
||||
let mut sender = if scheme == "https://" {
|
||||
#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
|
||||
{
|
||||
let stream = TcpStream::connect(authority.clone()).await?;
|
||||
//TODO: uncomment this code and make the tls implementation a feature decision.
|
||||
//let connector = tokio_native_tls::native_tls::TlsConnector::builder().build()?;
|
||||
//let connector = tokio_native_tls::TlsConnector::from(connector);
|
||||
//let host = authority.split(":").next().unwrap_or(&authority);
|
||||
//let stream = connector.connect(&host, stream).await?;
|
||||
|
||||
// derived from https://github.com/tokio-rs/tls/blob/master/tokio-rustls/examples/client/src/main.rs
|
||||
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||
root_cert_store.add_server_trust_anchors(
|
||||
webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
|
||||
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
ta.subject,
|
||||
ta.spki,
|
||||
ta.name_constraints,
|
||||
)
|
||||
}),
|
||||
);
|
||||
let config = rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_cert_store)
|
||||
.with_no_client_auth();
|
||||
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
|
||||
let mut parts = authority.split(":");
|
||||
let host = parts.next().unwrap_or(&authority);
|
||||
let domain =
|
||||
rustls::ServerName::try_from(host).map_err(|_| anyhow!("invalid dnsname"))?;
|
||||
let stream = connector.connect(domain, stream).await?;
|
||||
|
||||
let t = timeout(
|
||||
connect_timeout,
|
||||
hyper::client::conn::http1::handshake(stream),
|
||||
)
|
||||
.await?;
|
||||
let (s, conn) = t?;
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
println!("Connection failed: {:?}", err);
|
||||
}
|
||||
});
|
||||
s
|
||||
}
|
||||
#[cfg(any(target_arch = "riscv64", target_arch = "s390x"))]
|
||||
bail!("unsupported architecture for SSL")
|
||||
} else {
|
||||
let tcp = TcpStream::connect(authority).await?;
|
||||
let t = timeout(connect_timeout, hyper::client::conn::http1::handshake(tcp)).await?;
|
||||
let (s, conn) = t?;
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(err) = conn.await {
|
||||
println!("Connection failed: {:?}", err);
|
||||
}
|
||||
});
|
||||
s
|
||||
};
|
||||
|
||||
let url = scheme.to_owned() + &request.authority + &request.path + &request.query;
|
||||
|
||||
let mut call = Request::builder()
|
||||
.method(method)
|
||||
.uri(url)
|
||||
.header(hyper::header::HOST, request.authority.as_str());
|
||||
|
||||
for (key, val) in request.headers.iter() {
|
||||
for item in val {
|
||||
call = call.header(key, item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let response_id = self.response_id_base;
|
||||
self.response_id_base = self.response_id_base + 1;
|
||||
let mut response = ActiveResponse::new(response_id);
|
||||
let body = Full::<Bytes>::new(
|
||||
self.streams
|
||||
.get(&request.body)
|
||||
.unwrap_or(&Bytes::new())
|
||||
.clone(),
|
||||
);
|
||||
let t = timeout(first_bytes_timeout, sender.send_request(call.body(body)?)).await?;
|
||||
let mut res = t?;
|
||||
response.status = res.status().try_into()?;
|
||||
for (key, value) in res.headers().iter() {
|
||||
let mut vec = std::vec::Vec::new();
|
||||
vec.push(value.to_str()?.to_string());
|
||||
response
|
||||
.response_headers
|
||||
.insert(key.as_str().to_string(), vec);
|
||||
}
|
||||
let mut buf = BytesMut::new();
|
||||
while let Some(next) = timeout(between_bytes_timeout, res.frame()).await? {
|
||||
let frame = next?;
|
||||
if let Some(chunk) = frame.data_ref() {
|
||||
buf.put(chunk.clone());
|
||||
}
|
||||
}
|
||||
response.body = self.streams_id_base;
|
||||
self.streams_id_base = self.streams_id_base + 1;
|
||||
self.streams.insert(response.body, buf.freeze());
|
||||
self.responses.insert(response_id, response);
|
||||
Ok(response_id)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user