diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs new file mode 100644 index 0000000..ccad608 --- /dev/null +++ b/examples/http_server_proxy.rs @@ -0,0 +1,64 @@ +//! Run the example with: +//! ```sh +//! cargo build --example http_server_proxy --target=wasm32-wasip2 +//! wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com/ target/wasm32-wasip2/debug/examples/http_server_proxy.wasm +//! curl --no-buffer -v 127.0.0.1:8080/proxy/ +//! ``` +use wstd::http::body::Body; +use wstd::http::{Client, Error, Request, Response, StatusCode, Uri}; + +const PROXY_PREFIX: &str = "/proxy/"; + +#[wstd::http_server] +async fn main(server_req: Request) -> Result, Error> { + match server_req.uri().path_and_query().unwrap().as_str() { + api_prefixed_path if api_prefixed_path.starts_with(PROXY_PREFIX) => { + // Remove PROXY_PREFIX + let target_url = + std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL"); + let target_url: Uri = format!( + "{target_url}{}", + api_prefixed_path + .strip_prefix(PROXY_PREFIX) + .expect("checked above") + ) + .parse() + .expect("final target url should be parseable"); + println!("Proxying to {target_url}"); + proxy(server_req, target_url).await + } + _ => Ok(http_not_found(server_req)), + } +} + +async fn proxy(server_req: Request, target_url: Uri) -> Result, Error> { + let client = Client::new(); + let mut client_req = Request::builder(); + client_req = client_req.uri(target_url).method(server_req.method()); + + // Copy headers from `server_req` to the `client_req`. + for (key, value) in server_req.headers() { + client_req = client_req.header(key, value); + } + + // Stream the request body. + let client_req = client_req.body(server_req.into_body())?; + // Send the request. + let client_resp = client.send(client_req).await?; + // Copy headers from `client_resp` to `server_resp`. + let mut server_resp = Response::builder(); + for (key, value) in client_resp.headers() { + server_resp + .headers_mut() + .expect("no errors could be in ResponseBuilder") + .append(key, value.clone()); + } + Ok(server_resp.body(client_resp.into_body())?) +} + +fn http_not_found(_request: Request) -> Response { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap() +} diff --git a/test-programs/src/lib.rs b/test-programs/src/lib.rs index 86e6d9f..0d4a83f 100644 --- a/test-programs/src/lib.rs +++ b/test-programs/src/lib.rs @@ -6,6 +6,8 @@ use std::process::{Child, Command}; use std::thread::sleep; use std::time::Duration; +const DEFAULT_SERVER_PORT: u16 = 8081; + /// Manages exclusive access to port 8081, and kills the process when dropped pub struct WasmtimeServe { #[expect(dead_code, reason = "exists to live for as long as wasmtime process")] @@ -22,26 +24,35 @@ impl WasmtimeServe { /// /// Kills the wasmtime process, and releases the lock, once dropped. pub fn new(guest: &str) -> std::io::Result { + Self::new_with_config(guest, DEFAULT_SERVER_PORT, &[]) + } + + pub fn new_with_config(guest: &str, port: u16, env_vars: &[&str]) -> std::io::Result { let mut lockfile = std::env::temp_dir(); - lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.lock"); + lockfile.push(format!("TEST_PROGRAMS_WASMTIME_SERVE_{port}.lock")); let lockfile = File::create(&lockfile)?; lockfile.lock()?; // Run wasmtime serve. // Enable -Scli because we currently don't have a way to build with the // proxy adapter, so we build with the default adapter. - let process = Command::new("wasmtime") + let mut process = Command::new("wasmtime"); + let listening_addr = format!("127.0.0.1:{port}"); + process .arg("serve") .arg("-Scli") - .arg("--addr=127.0.0.1:8081") - .arg(guest) - .spawn()?; + .arg("--addr") + .arg(&listening_addr); + for env_var in env_vars { + process.arg("--env").arg(env_var); + } + let process = process.arg(guest).spawn()?; let w = WasmtimeServe { lockfile, process }; // Clumsily wait for the server to accept connections. 'wait: loop { sleep(Duration::from_millis(100)); - if TcpStream::connect("127.0.0.1:8081").is_ok() { + if TcpStream::connect(&listening_addr).is_ok() { break 'wait; } } diff --git a/test-programs/tests/http_server_proxy.rs b/test-programs/tests/http_server_proxy.rs new file mode 100644 index 0000000..705e2e0 --- /dev/null +++ b/test-programs/tests/http_server_proxy.rs @@ -0,0 +1,20 @@ +use anyhow::Result; + +#[test_log::test] +fn http_server_proxy() -> Result<()> { + // Run wasmtime serve for the proxy and the target HTTP server. + let _serve_target = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?; + let _serve_proxy = test_programs::WasmtimeServe::new_with_config( + test_programs::HTTP_SERVER_PROXY, + 8082, + &["TARGET_URL=http://127.0.0.1:8081"], + )?; + + // TEST / of the `http_server` example through the proxy + let body: String = ureq::get("http://127.0.0.1:8082/proxy/") + .call()? + .body_mut() + .read_to_string()?; + assert_eq!(body, "Hello, wasi:http/proxy world!\n"); + Ok(()) +}