spin_factor_outbound_http/
lib.rs

1pub mod intercept;
2mod spin;
3mod wasi;
4pub mod wasi_2023_10_18;
5pub mod wasi_2023_11_10;
6
7use std::sync::Arc;
8
9use anyhow::Context;
10use http::{
11    uri::{Authority, Parts, PathAndQuery, Scheme},
12    HeaderValue, Uri,
13};
14use intercept::OutboundHttpInterceptor;
15use spin_factor_outbound_networking::{
16    BlockedNetworks, ComponentTlsClientConfigs, OutboundAllowedHosts, OutboundNetworkingFactor,
17};
18use spin_factors::{
19    anyhow, ConfigureAppContext, Factor, PrepareContext, RuntimeFactors, SelfInstanceBuilder,
20};
21use wasmtime_wasi_http::WasiHttpCtx;
22
23pub use wasmtime_wasi_http::{
24    body::HyperOutgoingBody,
25    types::{HostFutureIncomingResponse, OutgoingRequestConfig},
26    HttpResult,
27};
28
29#[derive(Default)]
30pub struct OutboundHttpFactor {
31    _priv: (),
32}
33
34impl Factor for OutboundHttpFactor {
35    type RuntimeConfig = ();
36    type AppState = ();
37    type InstanceBuilder = InstanceState;
38
39    fn init(&mut self, ctx: &mut impl spin_factors::InitContext<Self>) -> anyhow::Result<()> {
40        ctx.link_bindings(spin_world::v1::http::add_to_linker)?;
41        wasi::add_to_linker(ctx)?;
42        Ok(())
43    }
44
45    fn configure_app<T: RuntimeFactors>(
46        &self,
47        _ctx: ConfigureAppContext<T, Self>,
48    ) -> anyhow::Result<Self::AppState> {
49        Ok(())
50    }
51
52    fn prepare<T: RuntimeFactors>(
53        &self,
54        mut ctx: PrepareContext<T, Self>,
55    ) -> anyhow::Result<Self::InstanceBuilder> {
56        let outbound_networking = ctx.instance_builder::<OutboundNetworkingFactor>()?;
57        let allowed_hosts = outbound_networking.allowed_hosts();
58        let blocked_networks = outbound_networking.blocked_networks();
59        let component_tls_configs = outbound_networking.component_tls_configs();
60        Ok(InstanceState {
61            wasi_http_ctx: WasiHttpCtx::new(),
62            allowed_hosts,
63            blocked_networks,
64            component_tls_configs,
65            self_request_origin: None,
66            request_interceptor: None,
67            spin_http_client: None,
68        })
69    }
70}
71
72pub struct InstanceState {
73    wasi_http_ctx: WasiHttpCtx,
74    allowed_hosts: OutboundAllowedHosts,
75    blocked_networks: BlockedNetworks,
76    component_tls_configs: ComponentTlsClientConfigs,
77    self_request_origin: Option<SelfRequestOrigin>,
78    request_interceptor: Option<Arc<dyn OutboundHttpInterceptor>>,
79    // Connection-pooling client for 'fermyon:spin/http' interface
80    spin_http_client: Option<reqwest::Client>,
81}
82
83impl InstanceState {
84    /// Sets the [`SelfRequestOrigin`] for this instance.
85    ///
86    /// This is used to handle outbound requests to relative URLs. If unset,
87    /// those requests will fail.
88    pub fn set_self_request_origin(&mut self, origin: SelfRequestOrigin) {
89        self.self_request_origin = Some(origin);
90    }
91
92    /// Sets a [`OutboundHttpInterceptor`] for this instance.
93    ///
94    /// Returns an error if it has already been called for this instance.
95    pub fn set_request_interceptor(
96        &mut self,
97        interceptor: impl OutboundHttpInterceptor + 'static,
98    ) -> anyhow::Result<()> {
99        if self.request_interceptor.is_some() {
100            anyhow::bail!("set_request_interceptor can only be called once");
101        }
102        self.request_interceptor = Some(Arc::new(interceptor));
103        Ok(())
104    }
105}
106
107impl SelfInstanceBuilder for InstanceState {}
108
109pub type Request = http::Request<wasmtime_wasi_http::body::HyperOutgoingBody>;
110pub type Response = http::Response<wasmtime_wasi_http::body::HyperIncomingBody>;
111
112/// SelfRequestOrigin indicates the base URI to use for "self" requests.
113#[derive(Clone, Debug)]
114pub struct SelfRequestOrigin {
115    pub scheme: Scheme,
116    pub authority: Authority,
117}
118
119impl SelfRequestOrigin {
120    pub fn create(scheme: Scheme, auth: &str) -> anyhow::Result<Self> {
121        Ok(SelfRequestOrigin {
122            scheme,
123            authority: auth
124                .parse()
125                .with_context(|| format!("address '{auth}' is not a valid authority"))?,
126        })
127    }
128
129    pub fn from_uri(uri: &Uri) -> anyhow::Result<Self> {
130        Ok(Self {
131            scheme: uri.scheme().context("URI missing scheme")?.clone(),
132            authority: uri.authority().context("URI missing authority")?.clone(),
133        })
134    }
135
136    fn into_uri(self, path_and_query: Option<PathAndQuery>) -> Uri {
137        let mut parts = Parts::default();
138        parts.scheme = Some(self.scheme);
139        parts.authority = Some(self.authority);
140        parts.path_and_query = path_and_query;
141        Uri::from_parts(parts).unwrap()
142    }
143
144    fn use_tls(&self) -> bool {
145        self.scheme == Scheme::HTTPS
146    }
147
148    fn host_header(&self) -> HeaderValue {
149        HeaderValue::from_str(self.authority.as_str()).unwrap()
150    }
151}
152
153impl std::fmt::Display for SelfRequestOrigin {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(f, "{}://{}", self.scheme, self.authority)
156    }
157}