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;
7
8use std::sync::Arc;
9
10use anyhow::Context;
11use http::{
12 uri::{Authority, Parts, PathAndQuery, Scheme},
13 HeaderValue, Uri,
14};
15use intercept::OutboundHttpInterceptor;
16use runtime_config::RuntimeConfig;
17use spin_factor_outbound_networking::{
18 config::{allowed_hosts::OutboundAllowedHosts, blocked_networks::BlockedNetworks},
19 ComponentTlsClientConfigs, OutboundNetworkingFactor,
20};
21use spin_factors::{
22 anyhow, ConfigureAppContext, Factor, FactorData, PrepareContext, RuntimeFactors,
23 SelfInstanceBuilder,
24};
25use wasmtime_wasi_http::WasiHttpCtx;
26
27pub use wasmtime_wasi_http::{
28 bindings::http::types::ErrorCode,
29 body::HyperOutgoingBody,
30 types::{HostFutureIncomingResponse, OutgoingRequestConfig},
31 HttpResult,
32};
33
34#[derive(Default)]
35pub struct OutboundHttpFactor {
36 _priv: (),
37}
38
39impl Factor for OutboundHttpFactor {
40 type RuntimeConfig = RuntimeConfig;
41 type AppState = AppState;
42 type InstanceBuilder = InstanceState;
43
44 fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
45 ctx.link_bindings(spin_world::v1::http::add_to_linker::<_, FactorData<Self>>)?;
46 wasi::add_to_linker(ctx)?;
47 Ok(())
48 }
49
50 fn configure_app<T: RuntimeFactors>(
51 &self,
52 mut ctx: ConfigureAppContext<T, Self>,
53 ) -> anyhow::Result<Self::AppState> {
54 let connection_pooling = ctx
55 .take_runtime_config()
56 .unwrap_or_default()
57 .connection_pooling;
58 Ok(AppState {
59 wasi_http_clients: wasi::HttpClients::new(connection_pooling),
60 connection_pooling,
61 })
62 }
63
64 fn prepare<T: RuntimeFactors>(
65 &self,
66 mut ctx: PrepareContext<T, Self>,
67 ) -> anyhow::Result<Self::InstanceBuilder> {
68 let outbound_networking = ctx.instance_builder::<OutboundNetworkingFactor>()?;
69 let allowed_hosts = outbound_networking.allowed_hosts();
70 let blocked_networks = outbound_networking.blocked_networks();
71 let component_tls_configs = outbound_networking.component_tls_configs();
72 Ok(InstanceState {
73 wasi_http_ctx: WasiHttpCtx::new(),
74 allowed_hosts,
75 blocked_networks,
76 component_tls_configs,
77 self_request_origin: None,
78 request_interceptor: None,
79 spin_http_client: None,
80 wasi_http_clients: ctx.app_state().wasi_http_clients.clone(),
81 connection_pooling: ctx.app_state().connection_pooling,
82 })
83 }
84}
85
86pub struct InstanceState {
87 wasi_http_ctx: WasiHttpCtx,
88 allowed_hosts: OutboundAllowedHosts,
89 blocked_networks: BlockedNetworks,
90 component_tls_configs: ComponentTlsClientConfigs,
91 self_request_origin: Option<SelfRequestOrigin>,
92 request_interceptor: Option<Arc<dyn OutboundHttpInterceptor>>,
93 spin_http_client: Option<reqwest::Client>,
99 wasi_http_clients: wasi::HttpClients,
104 connection_pooling: bool,
105}
106
107impl InstanceState {
108 pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
113 self.self_request_origin = Some(origin);
114 }
115
116 pub fn set_request_interceptor(
120 &mut self,
121 interceptor: impl OutboundHttpInterceptor + 'static,
122 ) -> anyhow::Result<()> {
123 if self.request_interceptor.is_some() {
124 anyhow::bail!("set_request_interceptor can only be called once");
125 }
126 self.request_interceptor = Some(Arc::new(interceptor));
127 Ok(())
128 }
129}
130
131impl SelfInstanceBuilder for InstanceState {}
132
133pub type Request = http::Request<wasmtime_wasi_http::body::HyperOutgoingBody>;
134pub type Response = http::Response<wasmtime_wasi_http::body::HyperIncomingBody>;
135
136#[derive(Clone, Debug)]
138pub struct SelfRequestOrigin {
139 pub scheme: Scheme,
140 pub authority: Authority,
141}
142
143impl SelfRequestOrigin {
144 pub fn create(scheme: Scheme, auth: &str) -> anyhow::Result<Self> {
145 Ok(SelfRequestOrigin {
146 scheme,
147 authority: auth
148 .parse()
149 .with_context(|| format!("address '{auth}' is not a valid authority"))?,
150 })
151 }
152
153 pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
154 Ok(Self {
155 scheme: uri.scheme().context("URI missing scheme")?.clone(),
156 authority: uri.authority().context("URI missing authority")?.clone(),
157 })
158 }
159
160 fn into_uri(self, path_and_query: Option<PathAndQuery>) -> Uri {
161 let mut parts = Parts::default();
162 parts.scheme = Some(self.scheme);
163 parts.authority = Some(self.authority);
164 parts.path_and_query = path_and_query;
165 Uri::from_parts(parts).unwrap()
166 }
167
168 fn use_tls(&self) -> bool {
169 self.scheme == Scheme::HTTPS
170 }
171
172 fn host_header(&self) -> HeaderValue {
173 HeaderValue::from_str(self.authority.as_str()).unwrap()
174 }
175}
176
177impl std::fmt::Display for SelfRequestOrigin {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 write!(f, "{}://{}", self.scheme, self.authority)
180 }
181}
182
183pub struct AppState {
184 wasi_http_clients: wasi::HttpClients,
186 connection_pooling: bool,
187}