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
34pub use wasi::{p2_to_p3_error_code, p3_to_p2_error_code};
35
36#[derive(Default)]
37pub struct OutboundHttpFactor {
38    _priv: (),
39}
40
41impl Factor for OutboundHttpFactor {
42    type RuntimeConfig = RuntimeConfig;
43    type AppState = AppState;
44    type InstanceBuilder = InstanceState;
45
46    fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
47        ctx.link_bindings(spin_world::v1::http::add_to_linker::<_, FactorData<Self>>)?;
48        wasi::add_to_linker(ctx)?;
49        Ok(())
50    }
51
52    fn configure_app<T: RuntimeFactors>(
53        &self,
54        mut ctx: ConfigureAppContext<T, Self>,
55    ) -> anyhow::Result<Self::AppState> {
56        let connection_pooling = ctx
57            .take_runtime_config()
58            .unwrap_or_default()
59            .connection_pooling;
60        Ok(AppState {
61            wasi_http_clients: wasi::HttpClients::new(connection_pooling),
62            connection_pooling,
63        })
64    }
65
66    fn prepare<T: RuntimeFactors>(
67        &self,
68        mut ctx: PrepareContext<T, Self>,
69    ) -> anyhow::Result<Self::InstanceBuilder> {
70        let outbound_networking = ctx.instance_builder::<OutboundNetworkingFactor>()?;
71        let allowed_hosts = outbound_networking.allowed_hosts();
72        let blocked_networks = outbound_networking.blocked_networks();
73        let component_tls_configs = outbound_networking.component_tls_configs();
74        Ok(InstanceState {
75            wasi_http_ctx: WasiHttpCtx::new(),
76            allowed_hosts,
77            blocked_networks,
78            component_tls_configs,
79            self_request_origin: None,
80            request_interceptor: None,
81            spin_http_client: None,
82            wasi_http_clients: ctx.app_state().wasi_http_clients.clone(),
83            connection_pooling: ctx.app_state().connection_pooling,
84        })
85    }
86}
87
88pub struct InstanceState {
89    wasi_http_ctx: WasiHttpCtx,
90    allowed_hosts: OutboundAllowedHosts,
91    blocked_networks: BlockedNetworks,
92    component_tls_configs: ComponentTlsClientConfigs,
93    self_request_origin: Option<SelfRequestOrigin>,
94    request_interceptor: Option<Arc<dyn OutboundHttpInterceptor>>,
95    // Connection-pooling client for 'fermyon:spin/http' interface
96    //
97    // TODO: We could move this to `AppState` to like the
98    // `wasi:http/outgoing-handler` pool for consistency, although it's probably
99    // not a high priority given that `fermyon:spin/http` is deprecated anyway.
100    spin_http_client: Option<reqwest::Client>,
101    // Connection pooling clients for `wasi:http/outgoing-handler` interface
102    //
103    // This is a clone of `AppState::wasi_http_clients`, meaning it is shared
104    // among all instances of the app.
105    wasi_http_clients: wasi::HttpClients,
106    connection_pooling: bool,
107}
108
109impl InstanceState {
110    /// Sets the [`SelfRequestOrigin`] for this instance.
111    ///
112    /// This is used to handle outbound requests to relative URLs. If unset,
113    /// those requests will fail.
114    pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
115        self.self_request_origin = Some(origin);
116    }
117
118    /// Sets a [`OutboundHttpInterceptor`] for this instance.
119    ///
120    /// Returns an error if it has already been called for this instance.
121    pub fn set_request_interceptor(
122        &mut self,
123        interceptor: impl OutboundHttpInterceptor + 'static,
124    ) -> anyhow::Result<()> {
125        if self.request_interceptor.is_some() {
126            anyhow::bail!("set_request_interceptor can only be called once");
127        }
128        self.request_interceptor = Some(Arc::new(interceptor));
129        Ok(())
130    }
131}
132
133impl SelfInstanceBuilder for InstanceState {}
134
135pub type Request = http::Request<wasmtime_wasi_http::body::HyperOutgoingBody>;
136pub type Response = http::Response<wasmtime_wasi_http::body::HyperIncomingBody>;
137
138/// SelfRequestOrigin indicates the base URI to use for "self" requests.
139#[derive(Clone, Debug)]
140pub struct SelfRequestOrigin {
141    pub scheme: Scheme,
142    pub authority: Authority,
143}
144
145impl SelfRequestOrigin {
146    pub fn create(scheme: Scheme, auth: &str) -> anyhow::Result<Self> {
147        Ok(SelfRequestOrigin {
148            scheme,
149            authority: auth
150                .parse()
151                .with_context(|| format!("address '{auth}' is not a valid authority"))?,
152        })
153    }
154
155    pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
156        Ok(Self {
157            scheme: uri.scheme().context("URI missing scheme")?.clone(),
158            authority: uri.authority().context("URI missing authority")?.clone(),
159        })
160    }
161
162    fn into_uri(self, path_and_query: Option<PathAndQuery>) -> Uri {
163        let mut parts = Parts::default();
164        parts.scheme = Some(self.scheme);
165        parts.authority = Some(self.authority);
166        parts.path_and_query = path_and_query;
167        Uri::from_parts(parts).unwrap()
168    }
169
170    fn use_tls(&self) -> bool {
171        self.scheme == Scheme::HTTPS
172    }
173
174    fn host_header(&self) -> HeaderValue {
175        HeaderValue::from_str(self.authority.as_str()).unwrap()
176    }
177}
178
179impl std::fmt::Display for SelfRequestOrigin {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        write!(f, "{}://{}", self.scheme, self.authority)
182    }
183}
184
185pub struct AppState {
186    // Connection pooling clients for `wasi:http/outgoing-handler` interface
187    wasi_http_clients: wasi::HttpClients,
188    connection_pooling: bool,
189}