spin_factor_outbound_http/
lib.rs1pub mod intercept;
2pub mod runtime_config;
3mod spin;
4mod wasi;
5pub mod wasi_2023_10_18;
6pub mod wasi_2023_11_10;
7pub mod wasi_2026_03_15;
8
9use std::{net::SocketAddr, sync::Arc};
10
11use anyhow::Context;
12use http::{
13 HeaderValue, Uri,
14 uri::{Authority, Parts, PathAndQuery, Scheme},
15};
16use intercept::OutboundHttpInterceptor;
17use runtime_config::RuntimeConfig;
18use spin_factor_otel::OtelFactorState;
19use spin_factor_outbound_networking::{
20 ComponentTlsClientConfigs, ConnectionSemaphore, OutboundNetworkingFactor,
21 build_connection_semaphore,
22 config::{allowed_hosts::OutboundAllowedHosts, blocked_networks::BlockedNetworks},
23};
24use spin_factors::{
25 ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors, SelfInstanceBuilder,
26 anyhow,
27};
28use wasmtime_wasi_http::WasiHttpCtx;
29
30pub use wasmtime_wasi_http::p2::{
31 HttpResult,
32 bindings::http::types::ErrorCode,
33 body::HyperOutgoingBody,
34 types::{HostFutureIncomingResponse, OutgoingRequestConfig},
35};
36
37#[derive(Default)]
38pub struct OutboundHttpFactor {
39 _priv: (),
40}
41
42impl Factor for OutboundHttpFactor {
43 type RuntimeConfig = RuntimeConfig;
44 type AppState = AppState;
45 type InstanceBuilder = InstanceState;
46
47 fn init<T: spin_factors::InitContext<Self>>(&mut self, ctx: &mut T) -> anyhow::Result<()> {
48 ctx.link_bindings(spin_world::v1::http::add_to_linker::<_, FactorData<Self>>)?;
49 wasi::add_to_linker(ctx)?;
50 Ok(())
51 }
52
53 fn configure_app<T: RuntimeFactors>(
54 &self,
55 mut ctx: ConfigureAppContext<T, Self>,
56 ) -> anyhow::Result<Self::AppState> {
57 let config = ctx.take_runtime_config().unwrap_or_default();
58 let networking = ctx.app_state::<OutboundNetworkingFactor>().ok();
59
60 Ok(AppState {
61 wasi_http_clients: wasi::HttpClients::new(config.connection_pooling_enabled),
62 connection_pooling_enabled: config.connection_pooling_enabled,
63 semaphore: build_connection_semaphore(
64 networking,
65 "http",
66 config.max_concurrent_connections,
67 config.wait_timeout,
68 ),
69 })
70 }
71
72 fn prepare<T: RuntimeFactors>(
73 &self,
74 mut ctx: PrepareContext<T, Self>,
75 ) -> anyhow::Result<Self::InstanceBuilder> {
76 let outbound_networking = ctx.instance_builder::<OutboundNetworkingFactor>()?;
77 let allowed_hosts = outbound_networking.allowed_hosts();
78 let blocked_networks = outbound_networking.blocked_networks();
79 let component_tls_configs = outbound_networking.component_tls_configs();
80 let otel = OtelFactorState::from_prepare_context(&mut ctx)?;
81 Ok(InstanceState {
82 wasi_http_ctx: WasiHttpCtx::new(),
83 hooks: InstanceHttpHooks {
84 allowed_hosts,
85 blocked_networks,
86 component_tls_configs,
87 self_request_origin: None,
88 request_interceptor: None,
89 spin_http_client: None,
90 wasi_http_clients: ctx.app_state().wasi_http_clients.clone(),
91 connection_pooling_enabled: ctx.app_state().connection_pooling_enabled,
92 semaphore: ctx.app_state().semaphore.clone(),
93 otel,
94 },
95 })
96 }
97}
98
99pub struct InstanceState {
100 wasi_http_ctx: WasiHttpCtx,
101 hooks: InstanceHttpHooks,
102}
103
104struct InstanceHttpHooks {
105 allowed_hosts: OutboundAllowedHosts,
106 blocked_networks: BlockedNetworks,
107 component_tls_configs: ComponentTlsClientConfigs,
108 self_request_origin: Option<SelfRequestOrigin>,
109 request_interceptor: Option<Arc<dyn OutboundHttpInterceptor>>,
110 spin_http_client: Option<reqwest::Client>,
116 wasi_http_clients: wasi::HttpClients,
121 connection_pooling_enabled: bool,
123 semaphore: ConnectionSemaphore,
125 otel: OtelFactorState,
127}
128
129impl InstanceState {
130 pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
135 self.hooks.self_request_origin = Some(origin);
136 }
137
138 pub fn set_request_interceptor(
142 &mut self,
143 interceptor: impl OutboundHttpInterceptor + 'static,
144 ) -> anyhow::Result<()> {
145 if self.hooks.request_interceptor.is_some() {
146 anyhow::bail!("set_request_interceptor can only be called once");
147 }
148 self.hooks.request_interceptor = Some(Arc::new(interceptor));
149 Ok(())
150 }
151}
152
153impl SelfInstanceBuilder for InstanceState {}
154
155pub type Request = http::Request<wasmtime_wasi_http::p2::body::HyperOutgoingBody>;
156pub type Response = http::Response<wasmtime_wasi_http::p2::body::HyperIncomingBody>;
157
158#[derive(Clone, Debug)]
160pub struct SelfRequestOrigin {
161 pub scheme: Scheme,
162 pub authority: Authority,
163}
164
165impl SelfRequestOrigin {
166 pub fn create(scheme: Scheme, auth: &str) -> anyhow::Result<Self> {
167 Ok(SelfRequestOrigin {
168 scheme,
169 authority: auth
170 .parse()
171 .with_context(|| format!("address '{auth}' is not a valid authority"))?,
172 })
173 }
174
175 pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
176 Ok(Self {
177 scheme: uri.scheme().context("URI missing scheme")?.clone(),
178 authority: uri.authority().context("URI missing authority")?.clone(),
179 })
180 }
181
182 fn into_uri(self, path_and_query: Option<PathAndQuery>) -> Uri {
183 let mut parts = Parts::default();
184 parts.scheme = Some(self.scheme);
185 parts.authority = Some(self.authority);
186 parts.path_and_query = path_and_query;
187 Uri::from_parts(parts).unwrap()
188 }
189
190 fn use_tls(&self) -> bool {
191 self.scheme == Scheme::HTTPS
192 }
193
194 fn host_header(&self) -> HeaderValue {
195 HeaderValue::from_str(self.authority.as_str()).unwrap()
196 }
197}
198
199impl std::fmt::Display for SelfRequestOrigin {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 write!(f, "{}://{}", self.scheme, self.authority)
202 }
203}
204
205pub struct AppState {
206 wasi_http_clients: wasi::HttpClients,
208 connection_pooling_enabled: bool,
210 semaphore: ConnectionSemaphore,
212}
213
214fn remove_blocked_addrs(
218 blocked_networks: &BlockedNetworks,
219 addrs: &mut Vec<SocketAddr>,
220) -> Result<(), ErrorCode> {
221 if addrs.is_empty() {
222 return Ok(());
223 }
224 let blocked_addrs = blocked_networks.remove_blocked(addrs);
225 if addrs.is_empty() && !blocked_addrs.is_empty() {
226 tracing::error!(
227 "error.type" = "destination_ip_prohibited",
228 ?blocked_addrs,
229 "all destination IP(s) prohibited by runtime config"
230 );
231 return Err(ErrorCode::DestinationIpProhibited);
232 }
233 Ok(())
234}