diff --git a/Cargo.lock b/Cargo.lock index 50a7f52..38aaef9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,7 +1548,9 @@ dependencies = [ name = "router" version = "0.1.0" dependencies = [ + "anyhow", "bitflags", + "spin-http", "spin-manifest", "toml", "wit-bindgen-rt", @@ -1774,7 +1776,7 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spin-common" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "dirs", @@ -1787,7 +1789,7 @@ dependencies = [ [[package]] name = "spin-componentize" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "wasm-encoder 0.200.0", @@ -1810,7 +1812,7 @@ dependencies = [ [[package]] name = "spin-expressions" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "async-trait", @@ -1821,10 +1823,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spin-http" +version = "2.5.0-pre0" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" +dependencies = [ + "anyhow", + "http", + "http-body-util", + "hyper", + "indexmap 1.9.3", + "percent-encoding", + "serde", + "spin-locked-app", + "tracing", +] + [[package]] name = "spin-locked-app" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "async-trait", @@ -1851,7 +1869,7 @@ dependencies = [ [[package]] name = "spin-manifest" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -1866,7 +1884,7 @@ dependencies = [ [[package]] name = "spin-outbound-networking" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "anyhow", "http", @@ -1903,7 +1921,7 @@ dependencies = [ [[package]] name = "spin-serde" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "base64", "serde", @@ -2083,7 +2101,7 @@ dependencies = [ [[package]] name = "terminal" version = "2.5.0-pre0" -source = "git+https://github.com/fermyon/spin#5a449eb12186a535570dc5623d0563ce1b70688d" +source = "git+https://github.com/fermyon/spin#fb45f59f92348cf6ddb2a6405af53168223b3a52" dependencies = [ "atty", "once_cell", @@ -2215,6 +2233,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 956a4b0..19454f3 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -4,10 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = { workspace = true } bitflags = "2.5.0" wit-bindgen-rt = "0.24.0" toml = { workspace = true } spin-manifest = { workspace = true } +spin-http = { git = "https://github.com/fermyon/spin", default-features = false } [lib] crate-type = ["cdylib"] diff --git a/crates/router/src/bindings.rs b/crates/router/src/bindings.rs index 9c085a5..d9d50df 100644 --- a/crates/router/src/bindings.rs +++ b/crates/router/src/bindings.rs @@ -1,5 +1,8 @@ // Generated by `wit-bindgen` 0.24.0. DO NOT EDIT! // Options used: +pub type OutgoingRequest = wasi::http::types::OutgoingRequest; +pub type IncomingRequest = wasi::http::types::IncomingRequest; +pub type IncomingBody = wasi::http::types::IncomingBody; #[allow(unused_unsafe, clippy::all)] pub fn get_manifest() -> _rt::String { unsafe { @@ -47,6 +50,32 @@ pub fn set_component_id(component_id: &str) { wit_import(ptr0.cast_mut(), len0); } } +#[allow(unused_unsafe, clippy::all)] +/// See `fermyon:spin-test/http-helper/new-request` for documentation on this function +pub fn new_request( + request: OutgoingRequest, + incoming_body: Option, +) -> IncomingRequest { + unsafe { + let (result0_0, result0_1) = match &incoming_body { + Some(e) => (1i32, (e).take_handle() as i32), + None => (0i32, 0i32), + }; + #[cfg(target_arch = "wasm32")] + #[link(wasm_import_module = "$root")] + extern "C" { + #[link_name = "new-request"] + fn wit_import(_: i32, _: i32, _: i32) -> i32; + } + + #[cfg(not(target_arch = "wasm32"))] + fn wit_import(_: i32, _: i32, _: i32) -> i32 { + unreachable!() + } + let ret = wit_import((&request).take_handle() as i32, result0_0, result0_1); + wasi::http::types::IncomingRequest::from_handle(ret as u32) + } +} #[allow(dead_code)] pub mod wasi { #[allow(dead_code)] @@ -7662,21 +7691,21 @@ pub(crate) use __export_router_impl as export; #[cfg(target_arch = "wasm32")] #[link_section = "component-type:wit-bindgen:0.24.0:router:encoded world"] #[doc(hidden)] -pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 6523] = *b"\ -\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xfe1\x01A\x02\x01A\x19\ -\x01B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\x04\0\x16[\ -method]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[method]pollab\ -le.block\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04poll\x01\x06\x03\ -\x01\x12wasi:io/poll@0.2.0\x05\0\x02\x03\0\0\x08pollable\x01B\x0f\x02\x03\x02\x01\ -\x01\x04\0\x08pollable\x03\0\0\x01w\x04\0\x07instant\x03\0\x02\x01w\x04\0\x08dur\ -ation\x03\0\x04\x01@\0\0\x03\x04\0\x03now\x01\x06\x01@\0\0\x05\x04\0\x0aresoluti\ -on\x01\x07\x01i\x01\x01@\x01\x04when\x03\0\x08\x04\0\x11subscribe-instant\x01\x09\ -\x01@\x01\x04when\x05\0\x08\x04\0\x12subscribe-duration\x01\x0a\x03\x01!wasi:clo\ -cks/monotonic-clock@0.2.0\x05\x02\x01B\x04\x04\0\x05error\x03\x01\x01h\0\x01@\x01\ -\x04self\x01\0s\x04\0\x1d[method]error.to-debug-string\x01\x02\x03\x01\x13wasi:i\ -o/error@0.2.0\x05\x03\x02\x03\0\x02\x05error\x01B(\x02\x03\x02\x01\x04\x04\0\x05\ -error\x03\0\0\x02\x03\x02\x01\x01\x04\0\x08pollable\x03\0\x02\x01i\x01\x01q\x02\x15\ -last-operation-failed\x01\x04\0\x06closed\0\0\x04\0\x0cstream-error\x03\0\x05\x04\ +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 6682] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x9d3\x01A\x02\x01A$\x01\ +B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\x04\0\x16[meth\ +od]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[method]pollable.b\ +lock\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04poll\x01\x06\x03\x01\ +\x12wasi:io/poll@0.2.0\x05\0\x02\x03\0\0\x08pollable\x01B\x0f\x02\x03\x02\x01\x01\ +\x04\0\x08pollable\x03\0\0\x01w\x04\0\x07instant\x03\0\x02\x01w\x04\0\x08duratio\ +n\x03\0\x04\x01@\0\0\x03\x04\0\x03now\x01\x06\x01@\0\0\x05\x04\0\x0aresolution\x01\ +\x07\x01i\x01\x01@\x01\x04when\x03\0\x08\x04\0\x11subscribe-instant\x01\x09\x01@\ +\x01\x04when\x05\0\x08\x04\0\x12subscribe-duration\x01\x0a\x03\x01!wasi:clocks/m\ +onotonic-clock@0.2.0\x05\x02\x01B\x04\x04\0\x05error\x03\x01\x01h\0\x01@\x01\x04\ +self\x01\0s\x04\0\x1d[method]error.to-debug-string\x01\x02\x03\x01\x13wasi:io/er\ +ror@0.2.0\x05\x03\x02\x03\0\x02\x05error\x01B(\x02\x03\x02\x01\x04\x04\0\x05erro\ +r\x03\0\0\x02\x03\x02\x01\x01\x04\0\x08pollable\x03\0\x02\x01i\x01\x01q\x02\x15l\ +ast-operation-failed\x01\x04\0\x06closed\0\0\x04\0\x0cstream-error\x03\0\x05\x04\ \0\x0cinput-stream\x03\x01\x04\0\x0doutput-stream\x03\x01\x01h\x07\x01p}\x01j\x01\ \x0a\x01\x06\x01@\x02\x04self\x09\x03lenw\0\x0b\x04\0\x19[method]input-stream.re\ ad\x01\x0c\x04\0\"[method]input-stream.blocking-read\x01\x0c\x01j\x01w\x01\x06\x01\ @@ -7785,14 +7814,17 @@ e\x01\x8c\x01\x03\x01\x15wasi:http/types@0.2.0\x05\x09\x02\x03\0\x04\x10incoming -request\x02\x03\0\x04\x11response-outparam\x01B\x08\x02\x03\x02\x01\x0a\x04\0\x10\ incoming-request\x03\0\0\x02\x03\x02\x01\x0b\x04\0\x11response-outparam\x03\0\x02\ \x01i\x01\x01i\x03\x01@\x02\x07request\x04\x0cresponse-out\x05\x01\0\x04\0\x06ha\ -ndle\x01\x06\x03\x01\x20wasi:http/incoming-handler@0.2.0\x05\x0c\x01@\0\0s\x03\0\ -\x0cget-manifest\x01\x0d\x01@\x01\x0ccomponent-ids\x01\0\x03\0\x10set-component-\ -id\x01\x0e\x01B\x08\x02\x03\x02\x01\x0a\x04\0\x10incoming-request\x03\0\0\x02\x03\ -\x02\x01\x0b\x04\0\x11response-outparam\x03\0\x02\x01i\x01\x01i\x03\x01@\x02\x07\ -request\x04\x0cresponse-out\x05\x01\0\x04\0\x06handle\x01\x06\x04\x01\x20wasi:ht\ -tp/incoming-handler@0.2.0\x05\x0f\x04\x01\x15fermyon:router/router\x04\0\x0b\x0c\ -\x01\0\x06router\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-componen\ -t\x070.202.0\x10wit-bindgen-rust\x060.24.0"; +ndle\x01\x06\x03\x01\x20wasi:http/incoming-handler@0.2.0\x05\x0c\x02\x03\0\x04\x10\ +outgoing-request\x03\0\x10outgoing-request\x03\0\x0d\x03\0\x10incoming-request\x03\ +\0\x0a\x02\x03\0\x04\x0dincoming-body\x03\0\x0dincoming-body\x03\0\x10\x01@\0\0s\ +\x03\0\x0cget-manifest\x01\x12\x01@\x01\x0ccomponent-ids\x01\0\x03\0\x10set-comp\ +onent-id\x01\x13\x01i\x0e\x01i\x11\x01k\x15\x01i\x0f\x01@\x02\x07request\x14\x0d\ +incoming-body\x16\0\x17\x03\0\x0bnew-request\x01\x18\x01B\x08\x02\x03\x02\x01\x0a\ +\x04\0\x10incoming-request\x03\0\0\x02\x03\x02\x01\x0b\x04\0\x11response-outpara\ +m\x03\0\x02\x01i\x01\x01i\x03\x01@\x02\x07request\x04\x0cresponse-out\x05\x01\0\x04\ +\0\x06handle\x01\x06\x04\x01\x20wasi:http/incoming-handler@0.2.0\x05\x19\x04\x01\ +\x15fermyon:router/router\x04\0\x0b\x0c\x01\0\x06router\x03\0\0\0G\x09producers\x01\ +\x0cprocessed-by\x02\x0dwit-component\x070.202.0\x10wit-bindgen-rust\x060.24.0"; #[inline(never)] #[doc(hidden)] diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 1999cae..e03c877 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,14 +1,13 @@ #[allow(warnings)] mod bindings; +use anyhow::Context as _; use bindings::{ - exports::wasi::http::incoming_handler::{IncomingRequest, ResponseOutparam}, - wasi::http::types::ErrorCode, -}; - -use crate::bindings::{ exports::wasi::http::incoming_handler::Guest, wasi::http::incoming_handler::handle as downstream, + wasi::http::types::{ + ErrorCode, Headers, IncomingRequest, OutgoingRequest, ResponseOutparam, Scheme, + }, }; struct Component; @@ -46,11 +45,20 @@ impl Guest for Component { .split_once('?') .map(|(path, _)| path) .unwrap_or(&path_with_query); - let component_id = routes + let routing = routes .iter() - .find(|(route, _)| path.starts_with(route) || *route == "/...") - .map(|(_, comp)| comp); - if let Some(component_id) = component_id { + .find(|(route, _)| path.starts_with(route) || *route == "/..."); + if let Some((route, component_id)) = routing { + let request = match apply_request_transformations(request, (*route).to_owned()) { + Ok(request) => request, + Err(e) => { + ResponseOutparam::set( + response_out, + Err(ErrorCode::InternalError(Some(e.to_string()))), + ); + return; + } + }; bindings::set_component_id(component_id); downstream(request, response_out) } else { @@ -65,4 +73,83 @@ impl Guest for Component { } } +/// Apply any request transformations needed for the given route. +fn apply_request_transformations( + request: IncomingRequest, + raw_route: String, +) -> anyhow::Result { + let headers_to_add = calculate_default_headers(&request, raw_route) + .context("could not calculate default headers to for request")? + .into_iter() + .flat_map(|(k, v)| { + k.iter() + .map(move |s| (s.to_string(), v.clone().into_bytes())) + }) + .chain(request.headers().entries()); + let headers = Headers::new(); + for (key, value) in headers_to_add { + headers.append(&key, &value).unwrap(); + } + let new = OutgoingRequest::new(headers); + let _ = new.set_scheme(request.scheme().as_ref()); + let _ = new.set_authority(request.authority().as_deref()); + let _ = new.set_method(&request.method()); + let _ = new.set_path_with_query(request.path_with_query().as_deref()); + Ok(bindings::new_request(new, Some(request.consume().unwrap()))) +} + +const FULL_URL: &[&str] = &["SPIN-FULL-URL", "X-FULL-URL"]; +const PATH_INFO: &[&str] = &["SPIN-PATH-INFO", "PATH-INFO"]; +const MATCHED_ROUTE: &[&str] = &["SPIN-MATCHED-ROUTE", "X-MATCHED-ROUTE"]; +const COMPONENT_ROUTE: &[&str] = &["SPIN-COMPONENT-ROUTE", "X-COMPONENT-ROUTE"]; +const RAW_COMPONENT_ROUTE: &[&str] = &["SPIN-RAW-COMPONENT-ROUTE", "X-RAW-COMPONENT-ROUTE"]; +const BASE_PATH: &[&str] = &["SPIN-BASE-PATH", "X-BASE-PATH"]; +const CLIENT_ADDR: &[&str] = &["SPIN-CLIENT-ADDR", "X-CLIENT-ADDR"]; +/// Calculate the default headers for the given request. +fn calculate_default_headers<'a>( + req: &IncomingRequest, + raw_route: String, +) -> anyhow::Result> { + let mut res = vec![]; + // TODO: calculate base path from manifest + let base = "/".to_owned(); + let abs_path = req.path_with_query().context("cannot get path and query")?; + let scheme = req.scheme(); + let scheme = match scheme.as_ref().unwrap_or(&Scheme::Https) { + Scheme::Http => "http", + Scheme::Https => "https", + Scheme::Other(s) => s, + }; + let host = req + .headers() + .get(&"host".to_owned()) + .into_iter() + .find(|v| !v.is_empty()) + .map(String::from_utf8) + .transpose() + .context("expected 'host' header to be UTF-8 encoded but it was not")? + .unwrap_or_else(|| "localhost".to_owned()); + + let matched_route = + spin_http::routes::RoutePattern::sanitize_with_base(base.clone(), raw_route.clone()); + + let path_info = spin_http::routes::RoutePattern::from(base.clone(), raw_route.clone()) + .relative(&abs_path)?; + let full_url = format!("{}://{}{}", scheme, host, abs_path); + let component_route = raw_route + .strip_suffix("/...") + .unwrap_or(&raw_route) + .to_owned(); + + res.push((PATH_INFO, path_info)); + res.push((FULL_URL, full_url)); + res.push((MATCHED_ROUTE, matched_route)); + res.push((BASE_PATH, base)); + res.push((RAW_COMPONENT_ROUTE, raw_route)); + res.push((COMPONENT_ROUTE, component_route)); + res.push((CLIENT_ADDR, "127.0.0.1:0".to_owned())); + + Ok(res) +} + bindings::export!(Component with_types_in bindings); diff --git a/crates/router/wit/world.wit b/crates/router/wit/world.wit index 7d1681f..6e2db88 100644 --- a/crates/router/wit/world.wit +++ b/crates/router/wit/world.wit @@ -6,4 +6,8 @@ world router { import wasi:http/incoming-handler@0.2.0; import get-manifest: func() -> string; import set-component-id: func(component-id: string); + + use wasi:http/types@0.2.0.{outgoing-request, incoming-request, incoming-body}; + /// See `fermyon:spin-test/http-helper/new-request` for documentation on this function + import new-request: func(request: outgoing-request, incoming-body: option) -> incoming-request; } diff --git a/crates/spin-test-sdk/src/lib.rs b/crates/spin-test-sdk/src/lib.rs index fc00161..d142146 100644 --- a/crates/spin-test-sdk/src/lib.rs +++ b/crates/spin-test-sdk/src/lib.rs @@ -16,7 +16,7 @@ use bindings::wasi::http; /// Make a request to the Spin app and return the response. pub fn perform_request(request: http::types::OutgoingRequest) -> http::types::IncomingResponse { - let request = spin_test::http_helper::new_request(request); + let request = spin_test::http_helper::new_request(request, None); let (response_out, response_receiver) = spin_test::http_helper::new_response(); http::incoming_handler::handle(request, response_out); response_receiver.get().unwrap() diff --git a/host-wit/world.wit b/host-wit/world.wit index cf6fda3..8e289b3 100644 --- a/host-wit/world.wit +++ b/host-wit/world.wit @@ -30,14 +30,18 @@ world runner { interface http-helper { use wasi:http/types@0.2.0.{ incoming-request, incoming-response, outgoing-response, - outgoing-request, response-outparam, future-incoming-response + outgoing-request, response-outparam, future-incoming-response, + incoming-body }; /// A receiver of an `incoming-response` resource response-receiver { get: func() -> option; } /// Create an `incoming-request` from an `outgoing-request` - new-request: func(request: outgoing-request) -> incoming-request; + /// + /// An optional `incoming-body` can also be supplied which will be + /// used instead of the body of the `outgoing-request`. + new-request: func(request: outgoing-request, incoming-body: option) -> incoming-request; /// Get a pair of a `response-outparam` and a `response-receiver` new-response: func() -> tuple; /// Create a `future-incoming-response` from an `outgoing-response` diff --git a/src/lib.rs b/src/lib.rs index 9603fc0..c705387 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,6 +129,9 @@ pub fn encode_composition( .instantiate("app", &app_component.bytes, app_args) .context("failed to instantiate Spin app")?; + let new_request = http_helper + .export("new-request")? + .expect("internal error: `new-request` not found"); // Instantiate the `router` component with various exports from `spin-test-virt` and `app` instances let router_args = [ ("set-component-id", &virt, "`spin-test-virt`"), @@ -145,6 +148,7 @@ pub fn encode_composition( ) as _, )) }) + .chain([Ok(("new-request", Box::new(new_request) as _))]) .collect::>>()?; let router = composition .instantiate("router", ROUTER, router_args) diff --git a/src/runtime.rs b/src/runtime.rs index 48aab1e..a9c21f6 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -6,7 +6,10 @@ use wasmtime::component::Linker; use wasmtime_wasi_http::{ bindings::http::{incoming_handler::IncomingRequest, types::Scheme}, body::{HostIncomingBody, HyperOutgoingBody}, - types::{HostFutureIncomingResponse, HostOutgoingResponse, IncomingResponseInternal}, + types::{ + HostFutureIncomingResponse, HostIncomingRequest, HostOutgoingResponse, + IncomingResponseInternal, + }, WasiHttpView, }; @@ -136,6 +139,7 @@ impl bindings::fermyon::spin_test::http_helper::Host for Data { fn new_request( &mut self, request: wasmtime::component::Resource, + incoming_body: Option>, ) -> wasmtime::Result> { let req = self.table.get_mut(&request)?; use wasmtime_wasi_http::bindings::http::types::Method; @@ -168,7 +172,15 @@ impl bindings::fermyon::spin_test::http_helper::Host for Data { let req = builder .body(req.body.take().unwrap_or_else(body::empty)) .unwrap(); - self.new_incoming_request(req) + let (parts, body) = req.into_parts(); + let body = incoming_body + .map(|b| self.table.delete(b)) + .transpose()? + .unwrap_or_else(|| { + HostIncomingBody::new(body, std::time::Duration::from_millis(600 * 1000)) + }); + let incoming_req = HostIncomingRequest::new(self, parts, Some(body)); + Ok(self.table().push(incoming_req)?) } fn new_response(