Repository: bytecodealliance/wasmtime
Pull request #8035 (2024-03-01 10:26:48)

attempt to downcast a send_request error into an ErrorCode
This allows the plugin to respond to the error in the event of, for example, a policy-defined rejection rather than returning control to the host.

Please see https://github.com/bytecodealliance/wasmtime/issues/8033 for more context.



Patch:
diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/http_impl.rs
index 7804edffc..7c3f7507e 100644
--- a/crates/wasi-http/src/http_impl.rs
+++ b/crates/wasi-http/src/http_impl.rs
@@ -94,13 +94,23 @@ impl<T: WasiHttpView> outgoing_handler::Host for T {
             .body(body)
             .map_err(|err| internal_error(err.to_string()))?;
 
-        Ok(Ok(self.send_request(OutgoingRequest {
+        let result = self.send_request(OutgoingRequest {
             use_tls,
             authority,
             request,
             connect_timeout,
             first_byte_timeout,
             between_bytes_timeout,
-        })?))
+        });
+
+        // attempt to downcast the error to a ErrorCode
+        // so that the guest may handle it
+        match result {
+            Ok(response) => Ok(Ok(response)),
+            Err(err) => match err.downcast::<types::ErrorCode>() {
+                Ok(err) => Ok(Err(err)),
+                Err(err) => Err(err),
+            },
+        }
     }
 }


Test patch:
diff --git a/crates/wasi-http/tests/all/main.rs b/crates/wasi-http/tests/all/main.rs
index 95e51ac85..af1a4d457 100644
--- a/crates/wasi-http/tests/all/main.rs
+++ b/crates/wasi-http/tests/all/main.rs
@@ -35,6 +35,7 @@ struct Ctx {
     stdout: MemoryOutputPipe,
     stderr: MemoryOutputPipe,
     send_request: Option<RequestSender>,
+    rejected_authority: Option<String>,
 }
 
 impl WasiView for Ctx {
@@ -59,6 +60,12 @@ impl WasiHttpView for Ctx {
         &mut self,
         request: OutgoingRequest,
     ) -> wasmtime::Result<Resource<HostFutureIncomingResponse>> {
+        if let Some(rejected_authority) = &self.rejected_authority {
+            let (auth, _port) = request.authority.split_once(':').unwrap();
+            if auth == rejected_authority {
+                return Err(ErrorCode::HttpRequestDenied.into());
+            }
+        }
         if let Some(send_request) = self.send_request.clone() {
             send_request(self, request)
         } else {
@@ -87,6 +94,7 @@ fn store(engine: &Engine, server: &Server) -> Store<Ctx> {
         stderr,
         stdout,
         send_request: None,
+        rejected_authority: None,
     };
 
     Store::new(&engine, ctx)
@@ -121,6 +129,7 @@ async fn run_wasi_http(
     component_filename: &str,
     req: hyper::Request<HyperIncomingBody>,
     send_request: Option<RequestSender>,
+    rejected_authority: Option<String>,
 ) -> anyhow::Result<Result<hyper::Response<Collected<Bytes>>, ErrorCode>> {
     let stdout = MemoryOutputPipe::new(4096);
     let stderr = MemoryOutputPipe::new(4096);
@@ -146,6 +155,7 @@ async fn run_wasi_http(
         stderr,
         stdout,
         send_request,
+        rejected_authority,
     };
     let mut store = Store::new(&engine, ctx);
 
@@ -201,6 +211,7 @@ async fn wasi_http_proxy_tests() -> anyhow::Result<()> {
         test_programs_artifacts::API_PROXY_COMPONENT,
         req.body(body::empty())?,
         None,
+        None,
     )
     .await?;
 
@@ -329,6 +340,7 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> {
         test_programs_artifacts::API_PROXY_STREAMING_COMPONENT,
         request,
         send_request,
+        None,
     )
     .await??;
 
@@ -361,6 +373,39 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> {
     Ok(())
 }
 
+// ensure the runtime rejects the outgoing request
+#[test_log::test(tokio::test)]
+async fn wasi_http_hash_all_with_reject() -> Result<()> {
+    let request = hyper::Request::builder()
+        .method(http::Method::GET)
+        .uri("http://example.com:8080/hash-all");
+    let request = request.header("url", format!("http://forbidden.com"));
+    let request = request.header("url", format!("http://localhost"));
+    let request = request.body(body::empty())?;
+
+    let response = run_wasi_http(
+        test_programs_artifacts::API_PROXY_STREAMING_COMPONENT,
+        request,
+        None,
+        Some("forbidden.com".to_string()),
+    )
+    .await??;
+
+    let body = response.into_body().to_bytes();
+    let body = str::from_utf8(&body).unwrap();
+    for line in body.lines() {
+        println!("{}", line);
+        if line.contains("forbidden.com") {
+            assert!(line.contains("HttpRequestDenied"));
+        }
+        if line.contains("localhost") {
+            assert!(!line.contains("HttpRequestDenied"));
+        }
+    }
+
+    Ok(())
+}
+
 #[test_log::test(tokio::test)]
 async fn wasi_http_echo() -> Result<()> {
     do_wasi_http_echo("echo", None).await
@@ -461,6 +506,7 @@ async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> {
         test_programs_artifacts::API_PROXY_STREAMING_COMPONENT,
         request,
         None,
+        None,
     )
     .await??;