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