Skip to content

Commit 5547fd6

Browse files
rvolosatovsbongjunj
authored andcommitted
feat(p3): begin wasi:http implementation (bytecodealliance#11439)
* doc: fix typo in p3 filesystem ref Signed-off-by: Roman Volosatovs <[email protected]> * build: vendor `wasi:[email protected]` WIT Signed-off-by: Roman Volosatovs <[email protected]> * feat(p3-http): scaffold p3 implementation Signed-off-by: Roman Volosatovs <[email protected]> * test(p3-http): add tests Signed-off-by: Roman Volosatovs <[email protected]> * chore(p3-http): add utils used by p3 tests Signed-off-by: Roman Volosatovs <[email protected]> * chore: add `expect` reasons Signed-off-by: Roman Volosatovs <[email protected]> * address review comments Signed-off-by: Roman Volosatovs <[email protected]> * establish `conn` connections on drop Signed-off-by: Roman Volosatovs <[email protected]> --------- Signed-off-by: Roman Volosatovs <[email protected]>
1 parent 130d581 commit 5547fd6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+4613
-52
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ webpki-roots = "0.26.0"
412412
itertools = "0.14.0"
413413
base64 = "0.22.1"
414414
termcolor = "1.4.1"
415+
flate2 = "1.0.30"
415416

416417
# =============================================================================
417418
#

ci/vendor-wit.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ make_vendor "wasi/src/p3" "
7676
7777
"
7878

79+
make_vendor "wasi-http/src/p3" "
80+
81+
82+
filesystem@[email protected]
83+
84+
85+
86+
"
87+
7988
rm -rf $cache_dir
8089

8190
# Separately (for now), vendor the `wasi-nn` WIT files since their retrieval is
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use futures::join;
2+
use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Request, Response};
3+
use test_programs::p3::{wit_future, wit_stream};
4+
use wit_bindgen_rt::async_support::spawn;
5+
6+
struct T;
7+
8+
test_programs::p3::proxy::export!(T);
9+
10+
impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T {
11+
async fn handle(request: Request) -> Result<Response, ErrorCode> {
12+
assert!(request.get_scheme().is_some());
13+
assert!(request.get_authority().is_some());
14+
assert!(request.get_path_with_query().is_some());
15+
16+
// TODO: adapt below
17+
//test_filesystem();
18+
19+
let header = String::from("custom-forbidden-header");
20+
let req_hdrs = request.get_headers();
21+
22+
assert!(
23+
!req_hdrs.has(&header),
24+
"forbidden `custom-forbidden-header` found in request"
25+
);
26+
27+
assert!(req_hdrs.delete(&header).is_err());
28+
assert!(req_hdrs.append(&header, b"no".as_ref()).is_err());
29+
30+
assert!(
31+
!req_hdrs.has(&header),
32+
"append of forbidden header succeeded"
33+
);
34+
35+
let hdrs = Headers::new();
36+
let (mut contents_tx, contents_rx) = wit_stream::new();
37+
let (trailers_tx, trailers_rx) = wit_future::new(|| todo!());
38+
let (resp, transmit) = Response::new(hdrs, Some(contents_rx), trailers_rx);
39+
spawn(async {
40+
join!(
41+
async {
42+
let remaining = contents_tx.write_all(b"hello, world!".to_vec()).await;
43+
assert!(remaining.is_empty());
44+
drop(contents_tx);
45+
trailers_tx
46+
.write(Ok(None))
47+
.await
48+
.expect("failed to write trailers");
49+
},
50+
async { transmit.await.unwrap() }
51+
);
52+
});
53+
Ok(resp)
54+
}
55+
}
56+
57+
// Technically this should not be here for a proxy, but given the current
58+
// framework for tests it's required since this file is built as a `bin`
59+
fn main() {}
60+
61+
// TODO: adapt below
62+
//fn test_filesystem() {
63+
// assert!(std::fs::File::open(".").is_err());
64+
//}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use {
2+
test_programs::p3::{
3+
proxy::exports::wasi::http::handler::Guest as Handler,
4+
wasi::http::types::{ErrorCode, Request, Response},
5+
wit_future, wit_stream,
6+
},
7+
wit_bindgen_rt::async_support::{self, StreamResult},
8+
};
9+
10+
struct Component;
11+
12+
test_programs::p3::proxy::export!(Component);
13+
14+
impl Handler for Component {
15+
/// Return a response which echoes the request headers, body, and trailers.
16+
async fn handle(request: Request) -> Result<Response, ErrorCode> {
17+
let headers = request.get_headers();
18+
let (body, trailers) = request.consume_body().unwrap();
19+
20+
// let (headers, body) = Request::into_parts(request);
21+
22+
let (response, _result) = if false {
23+
// This is the easy and efficient way to do it...
24+
Response::new(headers, Some(body), trailers)
25+
} else {
26+
// ...but we do it the more difficult, less efficient way here to exercise various component model
27+
// features (e.g. `future`s, `stream`s, and post-return asynchronous execution):
28+
let (trailers_tx, trailers_rx) = wit_future::new(|| todo!());
29+
let (mut pipe_tx, pipe_rx) = wit_stream::new();
30+
31+
async_support::spawn(async move {
32+
let mut body_rx = body;
33+
let mut chunk = Vec::with_capacity(1024);
34+
loop {
35+
let (status, buf) = body_rx.read(chunk).await;
36+
chunk = buf;
37+
match status {
38+
StreamResult::Complete(_) => {
39+
chunk = pipe_tx.write_all(chunk).await;
40+
assert!(chunk.is_empty());
41+
}
42+
StreamResult::Dropped => break,
43+
StreamResult::Cancelled => unreachable!(),
44+
}
45+
}
46+
47+
drop(pipe_tx);
48+
49+
trailers_tx.write(trailers.await).await.unwrap();
50+
drop(request);
51+
});
52+
53+
Response::new(headers, Some(pipe_rx), trailers_rx)
54+
};
55+
56+
Ok(response)
57+
}
58+
}
59+
60+
// Unused function; required since this file is built as a `bin`:
61+
fn main() {}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use {
2+
flate2::{
3+
Compression,
4+
write::{DeflateDecoder, DeflateEncoder},
5+
},
6+
std::{io::Write, mem},
7+
test_programs::p3::{
8+
proxy::exports::wasi::http::handler::Guest as Handler,
9+
wasi::http::{
10+
handler,
11+
types::{ErrorCode, Headers, Request, Response},
12+
},
13+
wit_future, wit_stream,
14+
},
15+
wit_bindgen_rt::async_support::{self, StreamResult},
16+
};
17+
18+
struct Component;
19+
20+
test_programs::p3::proxy::export!(Component);
21+
22+
impl Handler for Component {
23+
/// Forward the specified request to the imported `wasi:http/handler`, transparently decoding the request body
24+
/// if it is `deflate`d and then encoding the response body if the client has provided an `accept-encoding:
25+
/// deflate` header.
26+
async fn handle(request: Request) -> Result<Response, ErrorCode> {
27+
// First, extract the parts of the request and check for (and remove) headers pertaining to body encodings.
28+
let method = request.get_method();
29+
let scheme = request.get_scheme();
30+
let path_with_query = request.get_path_with_query();
31+
let authority = request.get_authority();
32+
let mut accept_deflated = false;
33+
let mut content_deflated = false;
34+
let headers = request.get_headers();
35+
let mut headers = headers.copy_all();
36+
headers.retain(|(k, v)| match (k.as_str(), v.as_slice()) {
37+
("accept-encoding", value)
38+
if std::str::from_utf8(value)
39+
.map(|v| v.contains("deflate"))
40+
.unwrap_or(false) =>
41+
{
42+
accept_deflated = true;
43+
false
44+
}
45+
("content-encoding", b"deflate") => {
46+
content_deflated = true;
47+
false
48+
}
49+
_ => true,
50+
});
51+
let (mut body, trailers) = request.consume_body().unwrap();
52+
53+
let (body, trailers) = if content_deflated {
54+
// Next, spawn a task to pipe and decode the original request body and trailers into a new request
55+
// we'll create below. This will run concurrently with any code in the imported `wasi:http/handler`.
56+
let (trailers_tx, trailers_rx) = wit_future::new(|| todo!());
57+
let (mut pipe_tx, pipe_rx) = wit_stream::new();
58+
59+
async_support::spawn(async move {
60+
{
61+
let mut decoder = DeflateDecoder::new(Vec::new());
62+
63+
let (mut status, mut chunk) = body.read(Vec::with_capacity(64 * 1024)).await;
64+
while let StreamResult::Complete(_) = status {
65+
decoder.write_all(&chunk).unwrap();
66+
let remaining = pipe_tx.write_all(mem::take(decoder.get_mut())).await;
67+
assert!(remaining.is_empty());
68+
*decoder.get_mut() = remaining;
69+
chunk.clear();
70+
(status, chunk) = body.read(chunk).await;
71+
}
72+
73+
let remaining = pipe_tx.write_all(decoder.finish().unwrap()).await;
74+
assert!(remaining.is_empty());
75+
76+
drop(pipe_tx);
77+
}
78+
79+
trailers_tx.write(trailers.await).await.unwrap();
80+
81+
drop(request);
82+
});
83+
84+
(pipe_rx, trailers_rx)
85+
} else {
86+
(body, trailers)
87+
};
88+
89+
// While the above task (if any) is running, synthesize a request from the parts collected above and pass
90+
// it to the imported `wasi:http/handler`.
91+
let (my_request, _request_complete) = Request::new(
92+
Headers::from_list(&headers).unwrap(),
93+
Some(body),
94+
trailers,
95+
None,
96+
);
97+
my_request.set_method(&method).unwrap();
98+
my_request.set_scheme(scheme.as_ref()).unwrap();
99+
my_request
100+
.set_path_with_query(path_with_query.as_deref())
101+
.unwrap();
102+
my_request.set_authority(authority.as_deref()).unwrap();
103+
104+
let response = handler::handle(my_request).await?;
105+
106+
// Now that we have the response, extract the parts, adding an extra header if we'll be encoding the body.
107+
let status_code = response.get_status_code();
108+
let mut headers = response.get_headers().copy_all();
109+
if accept_deflated {
110+
headers.push(("content-encoding".into(), b"deflate".into()));
111+
}
112+
113+
let (mut body, trailers) = response.consume_body().unwrap();
114+
let (body, trailers) = if accept_deflated {
115+
headers.retain(|(name, _value)| name != "content-length");
116+
117+
// Spawn another task; this one is to pipe and encode the original response body and trailers into a
118+
// new response we'll create below. This will run concurrently with the caller's code (i.e. it won't
119+
// necessarily complete before we return a value).
120+
let (trailers_tx, trailers_rx) = wit_future::new(|| todo!());
121+
let (mut pipe_tx, pipe_rx) = wit_stream::new();
122+
123+
async_support::spawn(async move {
124+
{
125+
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::fast());
126+
let (mut status, mut chunk) = body.read(Vec::with_capacity(64 * 1024)).await;
127+
128+
while let StreamResult::Complete(_) = status {
129+
encoder.write_all(&chunk).unwrap();
130+
let remaining = pipe_tx.write_all(mem::take(encoder.get_mut())).await;
131+
assert!(remaining.is_empty());
132+
*encoder.get_mut() = remaining;
133+
chunk.clear();
134+
(status, chunk) = body.read(chunk).await;
135+
}
136+
137+
let remaining = pipe_tx.write_all(encoder.finish().unwrap()).await;
138+
assert!(remaining.is_empty());
139+
140+
drop(pipe_tx);
141+
}
142+
143+
trailers_tx.write(trailers.await).await.unwrap();
144+
drop(response);
145+
});
146+
147+
(pipe_rx, trailers_rx)
148+
} else {
149+
(body, trailers)
150+
};
151+
152+
// While the above tasks (if any) are running, synthesize a response from the parts collected above and
153+
// return it.
154+
let (my_response, _response_complete) =
155+
Response::new(Headers::from_list(&headers).unwrap(), Some(body), trailers);
156+
my_response.set_status_code(status_code).unwrap();
157+
158+
Ok(my_response)
159+
}
160+
}
161+
162+
// Unused function; required since this file is built as a `bin`:
163+
fn main() {}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
mod bindings {
2+
wit_bindgen::generate!({
3+
path: "../wasi-http/src/p3/wit",
4+
world: "local:local/middleware-with-chain",
5+
inline: "
6+
package local:local;
7+
8+
world middleware-with-chain {
9+
include wasi:http/[email protected];
10+
11+
import chain-http;
12+
}
13+
14+
interface chain-http {
15+
use wasi:http/[email protected].{request, response, error-code};
16+
17+
handle: async func(request: request) -> result<response, error-code>;
18+
}
19+
",
20+
generate_all,
21+
});
22+
23+
use super::Component;
24+
export!(Component);
25+
}
26+
27+
use bindings::{
28+
exports::wasi::http::handler::Guest as Handler,
29+
local::local::chain_http,
30+
wasi::clocks::monotonic_clock,
31+
wasi::http::types::{ErrorCode, Request, Response},
32+
};
33+
use std::time::Duration;
34+
35+
struct Component;
36+
37+
impl Handler for Component {
38+
async fn handle(request: Request) -> Result<Response, ErrorCode> {
39+
// First, sleep briefly. This will ensure the next call happens via a
40+
// host->guest call to the `wit_bindgen_rt::async_support::callback`
41+
// function, which exercises different code paths in both the host and
42+
// the guest, which we want to test here.
43+
let duration = Duration::from_millis(10);
44+
monotonic_clock::wait_for(duration.as_nanos().try_into().unwrap()).await;
45+
46+
chain_http::handle(request).await
47+
}
48+
}
49+
50+
// Unused function; required since this file is built as a `bin`:
51+
fn main() {}

0 commit comments

Comments
 (0)