spin_factor_outbound_http/
lib.rs

1pub 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    // Connection-pooling client for 'fermyon:spin/http' interface
94    //
95    // TODO: We could move this to `AppState` to like the
96    // `wasi:http/outgoing-handler` pool for consistency, although it's probably
97    // not a high priority given that `fermyon:spin/http` is deprecated anyway.
98    spin_http_client: Option<reqwest::Client>,
99    // Connection pooling clients for `wasi:http/outgoing-handler` interface
100    //
101    // This is a clone of `AppState::wasi_http_clients`, meaning it is shared
102    // among all instances of the app.
103    wasi_http_clients: wasi::HttpClients,
104    connection_pooling: bool,
105}
106
107impl InstanceState {
108    /// Sets the [`SelfRequestOrigin`] for this instance.
109    ///
110    /// This is used to handle outbound requests to relative URLs. If unset,
111    /// those requests will fail.
112    pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
113        self.self_request_origin = Some(origin);
114    }
115
116    /// Sets a [`OutboundHttpInterceptor`] for this instance.
117    ///
118    /// Returns an error if it has already been called for this instance.
119    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/// SelfRequestOrigin indicates the base URI to use for "self" requests.
137#[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    // Connection pooling clients for `wasi:http/outgoing-handler` interface
185    wasi_http_clients: wasi::HttpClients,
186    connection_pooling: bool,
187}