diff --git a/Cargo.lock b/Cargo.lock index 4cd5b57135..3ba65858c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6135,6 +6135,7 @@ dependencies = [ "hyper", "indexmap", "num_cpus", + "outbound-http", "percent-encoding", "rustls-pemfile 0.3.0", "serde", diff --git a/crates/outbound-http/src/allowed_http_hosts.rs b/crates/outbound-http/src/allowed_http_hosts.rs index 4378ac1cdd..18604190d1 100644 --- a/crates/outbound-http/src/allowed_http_hosts.rs +++ b/crates/outbound-http/src/allowed_http_hosts.rs @@ -26,6 +26,13 @@ impl AllowedHttpHosts { Self::AllowSpecific(hosts) => hosts.iter().any(|h| h.allow(url)), } } + + pub fn allow_relative_url(&self) -> bool { + match self { + Self::AllowAll => true, + Self::AllowSpecific(hosts) => hosts.contains(&AllowedHttpHost::host("self")), + } + } } /// An HTTP host allow-list entry. @@ -214,6 +221,14 @@ mod test { ); } + #[test] + fn test_allowed_hosts_accepts_self() { + assert_eq!( + AllowedHttpHost::host("self"), + parse_allowed_http_host("self").unwrap() + ); + } + #[test] fn test_allowed_hosts_accepts_localhost_addresses() { assert_eq!( @@ -303,4 +318,18 @@ mod test { assert!(!allowed.allow(&Url::parse("http://example.com/").unwrap())); assert!(!allowed.allow(&Url::parse("http://google.com/").unwrap())); } + + #[test] + fn test_allowed_hosts_allow_relative_url() { + let allowed = + parse_allowed_http_hosts(&to_vec_owned(&["self", "http://example.com:8383"])).unwrap(); + assert!(allowed.allow_relative_url()); + + let not_allowed = + parse_allowed_http_hosts(&to_vec_owned(&["http://example.com:8383"])).unwrap(); + assert!(!not_allowed.allow_relative_url()); + + let allow_all = parse_allowed_http_hosts(&to_vec_owned(&["insecure:allow-all"])).unwrap(); + assert!(allow_all.allow_relative_url()); + } } diff --git a/crates/outbound-http/src/lib.rs b/crates/outbound-http/src/lib.rs index cafde2c310..f24e2a27f9 100644 --- a/crates/outbound-http/src/lib.rs +++ b/crates/outbound-http/src/lib.rs @@ -21,15 +21,23 @@ pub const ALLOWED_HTTP_HOSTS_KEY: MetadataKey> = MetadataKey::new("a pub struct OutboundHttp { /// List of hosts guest modules are allowed to make requests to. pub allowed_hosts: AllowedHttpHosts, + /// During an incoming HTTP request, origin is set to the host of that incoming HTTP request. + /// This is used to direct outbound requests to the same host when allowed. + pub origin: String, client: Option, } impl OutboundHttp { /// Check if guest module is allowed to send request to URL, based on the list of - /// allowed hosts defined by the runtime. If the list of allowed hosts contains + /// allowed hosts defined by the runtime. If the url passed in is a relative path, + /// only allow if allowed_hosts contains `self`. If the list of allowed hosts contains /// `insecure:allow-all`, then all hosts are allowed. /// If `None` is passed, the guest module is not allowed to send the request. - fn is_allowed(&self, url: &str) -> Result { + fn is_allowed(&mut self, url: &str) -> Result { + if url.starts_with('/') { + return Ok(self.allowed_hosts.allow_relative_url()); + } + let url = Url::parse(url).map_err(|_| HttpError::InvalidUrl)?; Ok(self.allowed_hosts.allow(&url)) } @@ -49,7 +57,15 @@ impl outbound_http::Host for OutboundHttp { } let method = method_from(req.method); - let url = Url::parse(&req.uri).map_err(|_| HttpError::InvalidUrl)?; + + let abs_url = if req.uri.starts_with('/') { + format!("{}{}", self.origin, req.uri) + } else { + req.uri.clone() + }; + + let req_url = Url::parse(&abs_url).map_err(|_| HttpError::InvalidUrl)?; + let headers = request_headers(req.headers).map_err(|_| HttpError::RuntimeError)?; let body = req.body.unwrap_or_default().to_vec(); @@ -62,7 +78,7 @@ impl outbound_http::Host for OutboundHttp { let client = self.client.get_or_insert_with(Default::default); let resp = client - .request(method, url) + .request(method, req_url) .headers(headers) .body(body) .send() diff --git a/crates/trigger-http/Cargo.toml b/crates/trigger-http/Cargo.toml index 10a4908c5c..597efb4e2d 100644 --- a/crates/trigger-http/Cargo.toml +++ b/crates/trigger-http/Cargo.toml @@ -16,6 +16,7 @@ futures-util = "0.3.8" http = "0.2" hyper = { version = "0.14", features = ["full"] } indexmap = "1" +outbound-http = { path = "../outbound-http" } percent-encoding = "2" rustls-pemfile = "0.3.0" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/trigger-http/src/spin.rs b/crates/trigger-http/src/spin.rs index 598cf4540b..a3d9054480 100644 --- a/crates/trigger-http/src/spin.rs +++ b/crates/trigger-http/src/spin.rs @@ -4,9 +4,11 @@ use crate::{HttpExecutor, HttpTrigger, Store}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use hyper::{Body, Request, Response}; +use outbound_http::OutboundHttpComponent; use spin_core::Instance; use spin_trigger::{EitherInstance, TriggerAppEngine}; use spin_world::http_types; +use std::sync::Arc; #[derive(Clone)] pub struct SpinHttpExecutor; @@ -27,11 +29,13 @@ impl HttpExecutor for SpinHttpExecutor { component_id ); - let (instance, store) = engine.prepare_instance(component_id).await?; + let (instance, mut store) = engine.prepare_instance(component_id).await?; let EitherInstance::Component(instance) = instance else { unreachable!() }; + set_http_origin_from_request(&mut store, engine, &req); + let resp = Self::execute_impl(store, instance, base, raw_route, req, client_addr) .await .map_err(contextualise_err)?; @@ -182,6 +186,27 @@ impl SpinHttpExecutor { } } +fn set_http_origin_from_request( + store: &mut Store, + engine: &TriggerAppEngine, + req: &Request, +) { + if let Some(authority) = req.uri().authority() { + if let Some(scheme) = req.uri().scheme_str() { + if let Some(outbound_http_handle) = engine + .engine + .find_host_component_handle::>() + { + let mut outbound_http_data = store + .host_components_data() + .get_or_insert(outbound_http_handle); + + outbound_http_data.origin = format!("{}://{}", scheme, authority); + } + } + } +} + fn contextualise_err(e: anyhow::Error) -> anyhow::Error { if e.to_string() .contains("failed to find function export `canonical_abi_free`") diff --git a/examples/http-rust-outbound-http/Cargo.lock b/examples/http-rust-outbound-http/Cargo.lock deleted file mode 100644 index ad74e02424..0000000000 --- a/examples/http-rust-outbound-http/Cargo.lock +++ /dev/null @@ -1,504 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" - -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "http" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-rust-outbound-http" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "http", - "spin-sdk", -] - -[[package]] -name = "id-arena" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "proc-macro2" -version = "1.0.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pulldown-cmark" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" -dependencies = [ - "bitflags 1.3.2", - "memchr", - "unicase", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "routefinder" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f8f99b10dedd317514253dda1fa7c14e344aac96e1f78149a64879ce282aca" -dependencies = [ - "smartcow", - "smartstring", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - -[[package]] -name = "smartcow" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" -dependencies = [ - "smartstring", -] - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "spin-macro" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "http", - "proc-macro2", - "quote", - "syn 1.0.92", -] - -[[package]] -name = "spin-sdk" -version = "1.4.0-pre0" -dependencies = [ - "anyhow", - "bytes", - "form_urlencoded", - "http", - "routefinder", - "spin-macro", - "thiserror", - "wit-bindgen", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "syn" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "syn" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.92", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "url" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasm-encoder" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c41dbd92eaebf3612a39be316540b8377c871cb9bde6b064af962984912881" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-metadata" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e5156581ff4a302405c44ca7c85347563ca431d15f1a773f12c9c7b9a6cdc9" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.107.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e3ac9b780c7dda0cac7a52a5d6d2d6707cc6e3451c9db209b6c758f40d7acb" -dependencies = [ - "indexmap", - "semver", -] - -[[package]] -name = "wit-bindgen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392d16e9e46cc7ca98125bc288dd5e4db469efe8323d3e0dac815ca7f2398522" -dependencies = [ - "bitflags 2.3.2", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d422d36cbd78caa0e18c3371628447807c66ee72466b69865ea7e33682598158" -dependencies = [ - "anyhow", - "wit-component", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b76db68264f5d2089dc4652581236d8e75c5b89338de6187716215fd0e68ba3" -dependencies = [ - "heck", - "wasm-metadata", - "wit-bindgen-core", - "wit-bindgen-rust-lib", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-lib" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c50f334bc08b0903a43387f6eea6ef6aa9eb2a085729f1677b29992ecef20ba" -dependencies = [ - "heck", - "wit-bindgen-core", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced38a5e174940c6a41ae587babeadfd2e2c2dc32f3b6488bcdca0e8922cf3f3" -dependencies = [ - "anyhow", - "proc-macro2", - "syn 2.0.18", - "wit-bindgen-core", - "wit-bindgen-rust", - "wit-component", -] - -[[package]] -name = "wit-component" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbd4c7f8f400327c482c88571f373844b7889e61460650d650fc5881bb3575c" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "indexmap", - "log", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6daec9f093dbaea0e94043eeb92ece327bbbe70c86b1f41aca9bbfefd7f050f0" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "pulldown-cmark", - "semver", - "unicode-xid", - "url", -] diff --git a/examples/http-rust-outbound-http/http-hello/.cargo/config.toml b/examples/http-rust-outbound-http/http-hello/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/examples/http-rust-outbound-http/http-hello/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/examples/http-rust-outbound-http/http-hello/.gitignore b/examples/http-rust-outbound-http/http-hello/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/examples/http-rust-outbound-http/http-hello/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/examples/http-rust-outbound-http/http-hello/Cargo.toml b/examples/http-rust-outbound-http/http-hello/Cargo.toml new file mode 100644 index 0000000000..7db45bda22 --- /dev/null +++ b/examples/http-rust-outbound-http/http-hello/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "http-hello" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +# The Spin SDK. +spin-sdk = { path = "../../../sdk/rust" } +[workspace] diff --git a/examples/http-rust-outbound-http/http-hello/src/lib.rs b/examples/http-rust-outbound-http/http-hello/src/lib.rs new file mode 100644 index 0000000000..ff2cc7c880 --- /dev/null +++ b/examples/http-rust-outbound-http/http-hello/src/lib.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use spin_sdk::{ + http::{Request, Response}, + http_component, +}; + +/// A simple Spin HTTP component. +#[http_component] +fn hello_world(_req: Request) -> Result { + Ok(http::Response::builder() + .status(200) + .header("foo", "bar") + .body(Some("Hello, Fermyon!\n".into()))?) +} diff --git a/examples/http-rust-outbound-http/.cargo/config.toml b/examples/http-rust-outbound-http/outbound-http-to-same-app/.cargo/config.toml similarity index 100% rename from examples/http-rust-outbound-http/.cargo/config.toml rename to examples/http-rust-outbound-http/outbound-http-to-same-app/.cargo/config.toml diff --git a/examples/http-rust-outbound-http/outbound-http-to-same-app/Cargo.toml b/examples/http-rust-outbound-http/outbound-http-to-same-app/Cargo.toml new file mode 100644 index 0000000000..665de8b45c --- /dev/null +++ b/examples/http-rust-outbound-http/outbound-http-to-same-app/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "outbound-http-to-same-app" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +# The Spin SDK. +spin-sdk = { path = "../../../sdk/rust" } +url = "2" +[workspace] diff --git a/examples/http-rust-outbound-http/outbound-http-to-same-app/src/lib.rs b/examples/http-rust-outbound-http/outbound-http-to-same-app/src/lib.rs new file mode 100644 index 0000000000..2a809a3ccc --- /dev/null +++ b/examples/http-rust-outbound-http/outbound-http-to-same-app/src/lib.rs @@ -0,0 +1,21 @@ +use anyhow::Result; +use spin_sdk::{ + http::{Request, Response}, + http_component, +}; + +/// Send an HTTP request and return the response. +#[http_component] +fn send_outbound(_req: Request) -> Result { + let mut res = spin_sdk::outbound_http::send_request( + http::Request::builder() + .method("GET") + .uri("/hello") // relative routes are not yet supported in cloud + .body(None) + .unwrap(), + )?; + res.headers_mut() + .insert("spin-component", "rust-outbound-http".try_into()?); + println!("{:?}", res); + Ok(res) +} diff --git a/examples/http-rust-outbound-http/outbound-http/.cargo/config.toml b/examples/http-rust-outbound-http/outbound-http/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/examples/http-rust-outbound-http/outbound-http/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/examples/http-rust-outbound-http/Cargo.toml b/examples/http-rust-outbound-http/outbound-http/Cargo.toml similarity index 77% rename from examples/http-rust-outbound-http/Cargo.toml rename to examples/http-rust-outbound-http/outbound-http/Cargo.toml index 5e29a4f20c..28a9232eb5 100644 --- a/examples/http-rust-outbound-http/Cargo.toml +++ b/examples/http-rust-outbound-http/outbound-http/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = [ "cdylib" ] +crate-type = ["cdylib"] [dependencies] # Useful crate to handle errors. @@ -14,5 +14,6 @@ bytes = "1" # General-purpose crate with common HTTP types. http = "0.2" # The Spin SDK. -spin-sdk = { path = "../../sdk/rust" } -[workspace] \ No newline at end of file +spin-sdk = { path = "../../../sdk/rust" } + +[workspace] diff --git a/examples/http-rust-outbound-http/src/lib.rs b/examples/http-rust-outbound-http/outbound-http/src/lib.rs similarity index 100% rename from examples/http-rust-outbound-http/src/lib.rs rename to examples/http-rust-outbound-http/outbound-http/src/lib.rs diff --git a/examples/http-rust-outbound-http/spin.toml b/examples/http-rust-outbound-http/spin.toml index 67c0c4e975..efa11da228 100644 --- a/examples/http-rust-outbound-http/spin.toml +++ b/examples/http-rust-outbound-http/spin.toml @@ -1,24 +1,48 @@ spin_manifest_version = "1" authors = ["Fermyon Engineering "] -description = "A simple application that makes an outbound http call." +description = "Demonstrates outbound HTTP calls" name = "spin-outbound-http" trigger = { type = "http", base = "/" } version = "1.0.0" [[component]] -id = "rust-outbound-http" -source = "target/wasm32-wasi/release/http_rust_outbound_http.wasm" +id = "outbound-http" +source = "outbound-http/target/wasm32-wasi/release/http_rust_outbound_http.wasm" allowed_http_hosts = ["https://random-data-api.fermyon.app"] [component.trigger] route = "/outbound" [component.build] +workdir = "outbound-http" command = "cargo build --target wasm32-wasi --release" [[component]] -id = "rust-outbound-http-wildcard" -source = "target/wasm32-wasi/release/http_rust_outbound_http.wasm" +id = "outbound-http-wildcard" +source = "outbound-http/target/wasm32-wasi/release/http_rust_outbound_http.wasm" allowed_http_hosts = ["insecure:allow-all"] [component.trigger] route = "/wildcard" [component.build] +workdir = "outbound-http" +command = "cargo build --target wasm32-wasi --release" + +[[component]] +id = "outbound-http-to-same-app" +source = "outbound-http-to-same-app/target/wasm32-wasi/release/outbound_http_to_same_app.wasm" +# To make outbound calls to components in the same Spin app, use the special value self. +# This is not yet supported in cloud. +allowed_http_hosts = ["self"] +[component.trigger] +route = "/outbound-to-hello-component" +[component.build] +workdir = "outbound-http-to-same-app" +command = "cargo build --target wasm32-wasi --release" + +[[component]] +id = "hello-component" +source = "http-hello/target/wasm32-wasi/release/http_hello.wasm" +description = "A simple component that returns hello." +[component.trigger] +route = "/hello" +[component.build] +workdir = "http-hello" command = "cargo build --target wasm32-wasi --release" diff --git a/examples/http-tinygo-outbound-http/outbound-http-to-same-app/go.mod b/examples/http-tinygo-outbound-http/outbound-http-to-same-app/go.mod new file mode 100644 index 0000000000..296ca67dbe --- /dev/null +++ b/examples/http-tinygo-outbound-http/outbound-http-to-same-app/go.mod @@ -0,0 +1,9 @@ +module outbound-http-to-same-app + +go 1.17 + +require github.com/fermyon/spin/sdk/go v0.0.0 + +require github.com/julienschmidt/httprouter v1.3.0 // indirect + +replace github.com/fermyon/spin/sdk/go v0.0.0 => ../../../sdk/go/ diff --git a/examples/http-tinygo-outbound-http/go.sum b/examples/http-tinygo-outbound-http/outbound-http-to-same-app/go.sum similarity index 100% rename from examples/http-tinygo-outbound-http/go.sum rename to examples/http-tinygo-outbound-http/outbound-http-to-same-app/go.sum diff --git a/examples/http-tinygo-outbound-http/outbound-http-to-same-app/main.go b/examples/http-tinygo-outbound-http/outbound-http-to-same-app/main.go new file mode 100644 index 0000000000..009df6921a --- /dev/null +++ b/examples/http-tinygo-outbound-http/outbound-http-to-same-app/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "net/http" + + spinhttp "github.com/fermyon/spin/sdk/go/http" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + // Because we included self in `allowed_http_hosts`, we can make outbound + // HTTP requests to our own app using a relative path. + // This is not yet supported in cloud. + resp, err := spinhttp.Get("/hello") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprintln(w, resp.Body) + fmt.Fprintln(w, resp.Header.Get("content-type")) + }) +} + +func main() {} diff --git a/examples/http-tinygo-outbound-http/spin.toml b/examples/http-tinygo-outbound-http/spin.toml index d8a37a9461..ab01306c22 100644 --- a/examples/http-tinygo-outbound-http/spin.toml +++ b/examples/http-tinygo-outbound-http/spin.toml @@ -7,9 +7,25 @@ version = "1.0.0" [[component]] id = "tinygo-hello" -source = "main.wasm" -allowed_http_hosts = ["https://random-data-api.fermyon.app", "https://postman-echo.com"] +source = "tinygo-hello/main.wasm" +allowed_http_hosts = [ + "https://random-data-api.fermyon.app", + "https://postman-echo.com", +] [component.trigger] route = "/hello" [component.build] +workdir = "tinygo-hello" +command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" + +[[component]] +id = "outbound-http-to-same-app" +source = "outbound-http-to-same-app/main.wasm" +# Use self to make outbound requests to components in the same Spin application. +# `self` is not yet supported in cloud +allowed_http_hosts = ["self"] +[component.trigger] +route = "/outbound-http-to-same-app" +[component.build] +workdir = "outbound-http-to-same-app" command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go" diff --git a/examples/http-tinygo-outbound-http/go.mod b/examples/http-tinygo-outbound-http/tinygo-hello/go.mod similarity index 74% rename from examples/http-tinygo-outbound-http/go.mod rename to examples/http-tinygo-outbound-http/tinygo-hello/go.mod index 2c1b9e24bf..92fbf0342d 100644 --- a/examples/http-tinygo-outbound-http/go.mod +++ b/examples/http-tinygo-outbound-http/tinygo-hello/go.mod @@ -6,4 +6,4 @@ require github.com/fermyon/spin/sdk/go v0.0.0 require github.com/julienschmidt/httprouter v1.3.0 // indirect -replace github.com/fermyon/spin/sdk/go v0.0.0 => ../../sdk/go/ +replace github.com/fermyon/spin/sdk/go v0.0.0 => ../../../sdk/go/ diff --git a/examples/http-tinygo-outbound-http/tinygo-hello/go.sum b/examples/http-tinygo-outbound-http/tinygo-hello/go.sum new file mode 100644 index 0000000000..096c54e630 --- /dev/null +++ b/examples/http-tinygo-outbound-http/tinygo-hello/go.sum @@ -0,0 +1,2 @@ +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/examples/http-tinygo-outbound-http/main.go b/examples/http-tinygo-outbound-http/tinygo-hello/main.go similarity index 100% rename from examples/http-tinygo-outbound-http/main.go rename to examples/http-tinygo-outbound-http/tinygo-hello/main.go diff --git a/tests/spinup_tests.rs b/tests/spinup_tests.rs index 0cfa3121ea..5de715d3d0 100644 --- a/tests/spinup_tests.rs +++ b/tests/spinup_tests.rs @@ -7,6 +7,11 @@ mod spinup_tests { use {e2e_testing::controller::Controller, e2e_testing::spin_controller::SpinUp}; const CONTROLLER: &dyn Controller = &SpinUp {}; + #[tokio::test] + async fn component_outbound_http_works() { + testcases::component_outbound_http_works(CONTROLLER).await + } + #[tokio::test] async fn config_variables_default_works() { testcases::config_variables_default_works(CONTROLLER).await diff --git a/tests/testcases/mod.rs b/tests/testcases/mod.rs index b027192e25..7cd663e841 100644 --- a/tests/testcases/mod.rs +++ b/tests/testcases/mod.rs @@ -14,6 +14,53 @@ fn get_url(base: &str, path: &str) -> String { format!("{}{}", base, path) } +pub async fn component_outbound_http_works(controller: &dyn Controller) { + async fn checks( + metadata: AppMetadata, + _: Option>>, + _: Option>>, + ) -> Result<()> { + assert_http_response( + get_url(metadata.base.as_str(), "/test/outbound-allowed").as_str(), + Method::GET, + "", + 200, + &[], + Some("Hello, Fermyon!\n"), + ) + .await?; + + assert_http_response( + get_url(metadata.base.as_str(), "/test/outbound-not-allowed").as_str(), + Method::GET, + "", + 500, + &[], + Some("destination-not-allowed (error 1)"), + ) + .await?; + + Ok(()) + } + + let tc = TestCaseBuilder::default() + .name("outbound-http-to-same-app".to_string()) + //the appname should be same as dir where this app exists + .appname(Some("outbound-http-to-same-app".to_string())) + .template(None) + .assertions( + |metadata: AppMetadata, + stdout_stream: Option>>, + stderr_stream: Option>>| { + Box::pin(checks(metadata, stdout_stream, stderr_stream)) + }, + ) + .build() + .unwrap(); + + tc.run(controller).await.unwrap() +} + pub async fn config_variables_default_works(controller: &dyn Controller) { async fn checks( metadata: AppMetadata, diff --git a/tests/testcases/outbound-http-to-same-app/http-component/.cargo/config.toml b/tests/testcases/outbound-http-to-same-app/http-component/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/http-component/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/tests/testcases/outbound-http-to-same-app/http-component/Cargo.toml b/tests/testcases/outbound-http-to-same-app/http-component/Cargo.toml new file mode 100644 index 0000000000..2d778a64c0 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/http-component/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "http-component" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +# The Spin SDK. +spin-sdk = { path = "../../../../sdk/rust" } + +[workspace] diff --git a/tests/testcases/outbound-http-to-same-app/http-component/src/lib.rs b/tests/testcases/outbound-http-to-same-app/http-component/src/lib.rs new file mode 100644 index 0000000000..fe34f4338c --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/http-component/src/lib.rs @@ -0,0 +1,15 @@ +use anyhow::Result; +use spin_sdk::{ + http::{Request, Response}, + http_component, +}; + +/// A simple Spin HTTP component. +#[http_component] +fn hello_world(req: Request) -> Result { + println!("{:?}", req.headers()); + Ok(http::Response::builder() + .status(200) + .header("foo", "bar") + .body(Some("Hello, Fermyon!\n".into()))?) +} diff --git a/tests/testcases/outbound-http-to-same-app/outbound-http-component/.cargo/config.toml b/tests/testcases/outbound-http-to-same-app/outbound-http-component/.cargo/config.toml new file mode 100644 index 0000000000..6b77899cb3 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/outbound-http-component/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/tests/testcases/outbound-http-to-same-app/outbound-http-component/Cargo.toml b/tests/testcases/outbound-http-to-same-app/outbound-http-component/Cargo.toml new file mode 100644 index 0000000000..5e13e10410 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/outbound-http-component/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "outbound-http-component" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Useful crate to handle errors. +anyhow = "1" +# Crate to simplify working with bytes. +bytes = "1" +# General-purpose crate with common HTTP types. +http = "0.2" +# The Spin SDK. +spin-sdk = { path = "../../../../sdk/rust" } + +[workspace] diff --git a/tests/testcases/outbound-http-to-same-app/outbound-http-component/src/lib.rs b/tests/testcases/outbound-http-to-same-app/outbound-http-component/src/lib.rs new file mode 100644 index 0000000000..30eaf71375 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/outbound-http-component/src/lib.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use spin_sdk::{ + http::{Request, Response}, + http_component, +}; + +/// Send an HTTP request and return the response. +#[http_component] +fn send_outbound(_req: Request) -> Result { + let mut res = spin_sdk::outbound_http::send_request( + http::Request::builder() + .method("GET") + .uri("/test/hello") + .body(None)?, + )?; + res.headers_mut() + .insert("spin-component", "outbound-http-component".try_into()?); + println!("{:?}", res); + Ok(res) +} diff --git a/tests/testcases/outbound-http-to-same-app/spin.toml b/tests/testcases/outbound-http-to-same-app/spin.toml new file mode 100644 index 0000000000..1b9dea8466 --- /dev/null +++ b/tests/testcases/outbound-http-to-same-app/spin.toml @@ -0,0 +1,34 @@ +spin_version = "1" +authors = ["Fermyon Engineering "] +description = "An application that demonstates a component making an outbound http request to another component in the same application." +name = "local-outbound-http" +trigger = { type = "http", base = "/test" } +version = "1.0.0" + +[[component]] +id = "hello" +source = "http-component/target/wasm32-wasi/release/http_component.wasm" +[component.trigger] +route = "/hello/..." +[component.build] +workdir = "http-component" +command = "cargo build --target wasm32-wasi --release" + +[[component]] +id = "outbound-http-allowed" +source = "outbound-http-component/target/wasm32-wasi/release/outbound_http_component.wasm" +allowed_http_hosts = ["self"] +[component.trigger] +route = "/outbound-allowed/..." +[component.build] +workdir = "outbound-http-component" +command = "cargo build --target wasm32-wasi --release" + +[[component]] +id = "outbound-http-not-allowed" +source = "outbound-http-component/target/wasm32-wasi/release/outbound_http_component.wasm" +[component.trigger] +route = "/outbound-not-allowed/..." +[component.build] +workdir = "outbound-http-component" +command = "cargo build --target wasm32-wasi --release"