1mod headers;
4mod instrument;
5mod middleware;
6mod outbound_http;
7mod server;
8mod spin;
9mod tls;
10mod wagi;
11mod wasi;
12mod wasip3;
13
14use std::{
15 error::Error,
16 fmt::Display,
17 net::{Ipv4Addr, SocketAddr, ToSocketAddrs},
18 path::PathBuf,
19 str::FromStr,
20 sync::Arc,
21 time::Duration,
22};
23
24use anyhow::{Context, bail};
25use clap::Args;
26use rand::{
27 distr::uniform::{SampleRange, SampleUniform},
28 rand_core::Rng,
29};
30use serde::Deserialize;
31use spin_app::App;
32use spin_factors::RuntimeFactors;
33use spin_trigger::Trigger;
34use wasmtime_wasi_http::p2::bindings::http::types::ErrorCode;
35
36pub use server::HttpServer;
37
38pub use tls::TlsConfig;
39
40pub(crate) use wasmtime_wasi_http::p2::body::HyperIncomingBody as Body;
41
42const DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT: usize = 128;
43const DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT: usize = 16;
44const DEFAULT_REQUEST_TIMEOUT: Option<Range<Duration>> = None;
45const DEFAULT_IDLE_INSTANCE_TIMEOUT: Range<Duration> = Range::Value(Duration::from_secs(1));
46
47#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
49pub enum OutputFormat {
50 #[default]
52 Plain,
53 Json,
55}
56
57pub(crate) type TriggerApp<F> = spin_trigger::TriggerApp<HttpTrigger, F>;
59
60pub(crate) type TriggerInstanceBuilder<'a, F> =
62 spin_trigger::TriggerInstanceBuilder<'a, HttpTrigger, F>;
63
64#[derive(Args)]
65pub struct CliArgs {
66 #[clap(long = "listen", env = "SPIN_HTTP_LISTEN_ADDR", default_value = "127.0.0.1:3000", value_parser = parse_listen_addr)]
68 pub address: SocketAddr,
69
70 #[clap(long, env = "SPIN_TLS_CERT", requires = "tls_key")]
72 pub tls_cert: Option<PathBuf>,
73
74 #[clap(long, env = "SPIN_TLS_KEY", requires = "tls_cert")]
76 pub tls_key: Option<PathBuf>,
77
78 #[clap(long, env = "SPIN_HTTP1_MAX_BUF_SIZE")]
80 pub http1_max_buf_size: Option<usize>,
81
82 #[clap(long = "find-free-port")]
83 pub find_free_port: bool,
84
85 #[clap(value_enum, long = "format", default_value_t = OutputFormat::default())]
86 pub format: OutputFormat,
87
88 #[clap(long, value_parser = parse_usize_range)]
99 pub max_instance_reuse_count: Option<Range<usize>>,
100
101 #[clap(long, value_parser = parse_usize_range)]
112 pub max_instance_concurrent_reuse_count: Option<Range<usize>>,
113
114 #[clap(long, value_parser = parse_duration_range)]
126 pub request_timeout: Option<Range<Duration>>,
127
128 #[clap(long, default_value = "1s", value_parser = parse_duration_range)]
139 pub idle_instance_timeout: Range<Duration>,
140}
141
142impl CliArgs {
143 fn into_tls_config(self) -> Option<TlsConfig> {
144 match (self.tls_cert, self.tls_key) {
145 (Some(cert_path), Some(key_path)) => Some(TlsConfig {
146 cert_path,
147 key_path,
148 }),
149 (None, None) => None,
150 _ => unreachable!(),
151 }
152 }
153}
154
155#[derive(Copy, Clone)]
156pub enum Range<T> {
157 Value(T),
158 Bounds(T, T),
159}
160
161impl<T> Range<T> {
162 fn map<V>(self, fun: impl Fn(T) -> V) -> Range<V> {
163 match self {
164 Self::Value(v) => Range::Value(fun(v)),
165 Self::Bounds(a, b) => Range::Bounds(fun(a), fun(b)),
166 }
167 }
168}
169
170impl<T: SampleUniform + PartialOrd> SampleRange<T> for Range<T> {
171 fn sample_single<R: Rng + ?Sized>(self, rng: &mut R) -> Result<T, rand::distr::uniform::Error> {
172 match self {
173 Self::Value(v) => Ok(v),
174 Self::Bounds(a, b) => (a..b).sample_single(rng),
175 }
176 }
177
178 fn is_empty(&self) -> bool {
179 match self {
180 Self::Value(_) => false,
181 Self::Bounds(a, b) => (a..b).is_empty(),
182 }
183 }
184}
185
186fn parse_range<T: FromStr>(s: &str) -> Result<Range<T>, String>
187where
188 T::Err: Display,
189{
190 let error = |e| format!("expected integer or range; got {s:?}; {e}");
191 if let Some((start, end)) = s.split_once("..") {
192 Ok(Range::Bounds(
193 start.parse().map_err(error)?,
194 end.parse().map_err(error)?,
195 ))
196 } else {
197 Ok(Range::Value(s.parse().map_err(error)?))
198 }
199}
200
201fn parse_usize_range(s: &str) -> Result<Range<usize>, String> {
202 parse_range(s)
203}
204
205struct ParsedDuration(Duration);
206
207impl FromStr for ParsedDuration {
208 type Err = String;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
211 let error = |e| {
212 format!("expected integer suffixed by `s`, `ms`, `us`, `μs`, or `ns`; got {s:?}; {e}")
213 };
214 Ok(Self(match s.parse() {
215 Ok(val) => Duration::from_secs(val),
216 Err(err) => {
217 if let Some(num) = s.strip_suffix("s") {
218 Duration::from_secs(num.parse().map_err(error)?)
219 } else if let Some(num) = s.strip_suffix("ms") {
220 Duration::from_millis(num.parse().map_err(error)?)
221 } else if let Some(num) = s.strip_suffix("us").or(s.strip_suffix("μs")) {
222 Duration::from_micros(num.parse().map_err(error)?)
223 } else if let Some(num) = s.strip_suffix("ns") {
224 Duration::from_nanos(num.parse().map_err(error)?)
225 } else {
226 return Err(error(err));
227 }
228 }
229 }))
230 }
231}
232
233fn parse_duration_range(s: &str) -> Result<Range<Duration>, String> {
234 parse_range::<ParsedDuration>(s).map(|v| v.map(|v| v.0))
235}
236
237#[derive(Clone, Copy)]
238pub struct InstanceReuseConfig {
239 max_instance_reuse_count: Range<usize>,
240 max_instance_concurrent_reuse_count: Range<usize>,
241 request_timeout: Option<Range<Duration>>,
242 request_deadline: Option<Duration>,
243 idle_instance_timeout: Range<Duration>,
244}
245
246impl Default for InstanceReuseConfig {
247 fn default() -> Self {
248 Self {
249 max_instance_reuse_count: Range::Value(DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT),
250 max_instance_concurrent_reuse_count: Range::Value(
251 DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT,
252 ),
253 request_timeout: DEFAULT_REQUEST_TIMEOUT,
254 request_deadline: None,
255 idle_instance_timeout: DEFAULT_IDLE_INSTANCE_TIMEOUT,
256 }
257 }
258}
259
260impl InstanceReuseConfig {
261 pub fn single_use_with_request_deadline(timeout: Duration) -> Self {
269 Self {
270 max_instance_reuse_count: Range::Value(1),
271 max_instance_concurrent_reuse_count: Range::Value(1),
272 request_timeout: Some(Range::Value(timeout)),
273 request_deadline: Some(timeout),
274 idle_instance_timeout: DEFAULT_IDLE_INSTANCE_TIMEOUT,
275 }
276 }
277}
278
279pub struct HttpTrigger {
281 listen_addr: SocketAddr,
286 tls_config: Option<TlsConfig>,
287 find_free_port: bool,
288 http1_max_buf_size: Option<usize>,
289 reuse_config: InstanceReuseConfig,
290 output_format: OutputFormat,
291}
292
293impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
294 const TYPE: &'static str = "http";
295
296 type CliArgs = CliArgs;
297 type InstanceState = ();
298
299 fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
300 let find_free_port = cli_args.find_free_port;
301 let http1_max_buf_size = cli_args.http1_max_buf_size;
302 let output_format = cli_args.format;
303 let reuse_config = InstanceReuseConfig {
304 max_instance_reuse_count: cli_args
305 .max_instance_reuse_count
306 .unwrap_or(Range::Value(DEFAULT_WASIP3_MAX_INSTANCE_REUSE_COUNT)),
307 max_instance_concurrent_reuse_count: cli_args
308 .max_instance_concurrent_reuse_count
309 .unwrap_or(Range::Value(
310 DEFAULT_WASIP3_MAX_INSTANCE_CONCURRENT_REUSE_COUNT,
311 )),
312 request_timeout: cli_args.request_timeout,
313 request_deadline: None,
314 idle_instance_timeout: cli_args.idle_instance_timeout,
315 };
316
317 Self::new(
318 app,
319 cli_args.address,
320 cli_args.into_tls_config(),
321 find_free_port,
322 http1_max_buf_size,
323 reuse_config,
324 output_format,
325 )
326 }
327
328 async fn run(self, trigger_app: TriggerApp<F>) -> anyhow::Result<()> {
329 let server = self.into_server(trigger_app)?;
330
331 server.serve().await?;
332
333 Ok(())
334 }
335
336 fn trigger_dependencies_composer() -> impl spin_factors_executor::TriggerDependenciesComposer {
337 middleware::HttpMiddlewareComposer
338 }
339
340 fn supported_host_requirements() -> Vec<&'static str> {
341 vec![spin_app::locked::SERVICE_CHAINING_KEY]
342 }
343
344 fn display_name() -> String {
345 "HTTP".to_string()
346 }
347}
348
349impl HttpTrigger {
350 pub fn new(
352 app: &spin_app::App,
353 listen_addr: SocketAddr,
354 tls_config: Option<TlsConfig>,
355 find_free_port: bool,
356 http1_max_buf_size: Option<usize>,
357 reuse_config: InstanceReuseConfig,
358 output_format: OutputFormat,
359 ) -> anyhow::Result<Self> {
360 Self::validate_app(app)?;
361
362 Ok(Self {
363 listen_addr,
364 tls_config,
365 find_free_port,
366 http1_max_buf_size,
367 reuse_config,
368 output_format,
369 })
370 }
371
372 pub fn into_server<F: RuntimeFactors>(
374 self,
375 trigger_app: TriggerApp<F>,
376 ) -> anyhow::Result<Arc<HttpServer<F>>> {
377 let Self {
378 listen_addr,
379 tls_config,
380 find_free_port,
381 http1_max_buf_size,
382 reuse_config,
383 output_format,
384 } = self;
385 let server = Arc::new(HttpServer::new(
386 listen_addr,
387 tls_config,
388 find_free_port,
389 trigger_app,
390 http1_max_buf_size,
391 reuse_config,
392 output_format,
393 )?);
394 Ok(server)
395 }
396
397 fn validate_app(app: &App) -> anyhow::Result<()> {
398 #[derive(Deserialize)]
399 #[serde(deny_unknown_fields)]
400 struct TriggerMetadata {
401 base: Option<String>,
402 }
403 if let Some(TriggerMetadata { base: Some(base) }) = app.get_trigger_metadata("http")? {
404 if base == "/" {
405 tracing::warn!(
406 "This application has the deprecated trigger 'base' set to the default value '/'. This may be an error in the future!"
407 );
408 } else {
409 bail!(
410 "This application is using the deprecated trigger 'base' field. The base must be prepended to each [[trigger.http]]'s 'route'."
411 )
412 }
413 }
414 Ok(())
415 }
416}
417
418fn parse_listen_addr(addr: &str) -> anyhow::Result<SocketAddr> {
419 let addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
420 if let Some(addr) = addrs
422 .iter()
423 .find(|addr| addr.is_ipv4() && addr.ip() == Ipv4Addr::LOCALHOST)
424 {
425 return Ok(*addr);
426 }
427 addrs.into_iter().next().context("couldn't resolve address")
429}
430
431#[derive(Debug, PartialEq)]
432enum NotFoundRouteKind {
433 Normal(String),
434 WellKnown,
435}
436
437pub fn hyper_request_error(err: hyper::Error) -> ErrorCode {
439 if let Some(cause) = err.source()
441 && let Some(err) = cause.downcast_ref::<ErrorCode>()
442 {
443 return err.clone();
444 }
445
446 tracing::warn!("hyper request error: {err:?}");
447
448 ErrorCode::HttpProtocolError
449}
450
451pub fn dns_error(rcode: String, info_code: u16) -> ErrorCode {
452 ErrorCode::DnsError(
453 wasmtime_wasi_http::p2::bindings::http::types::DnsErrorPayload {
454 rcode: Some(rcode),
455 info_code: Some(info_code),
456 },
457 )
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn parse_listen_addr_prefers_ipv4() {
466 let addr = parse_listen_addr("localhost:12345").unwrap();
467 assert_eq!(addr.ip(), Ipv4Addr::LOCALHOST);
468 assert_eq!(addr.port(), 12345);
469 }
470
471 #[test]
472 fn request_deadline_config_is_single_use() {
473 let timeout = Duration::from_millis(500);
474 let config = InstanceReuseConfig::single_use_with_request_deadline(timeout);
475
476 assert!(matches!(config.max_instance_reuse_count, Range::Value(1)));
477 assert!(matches!(
478 config.max_instance_concurrent_reuse_count,
479 Range::Value(1)
480 ));
481 assert!(matches!(config.request_timeout, Some(Range::Value(value)) if value == timeout));
482 assert_eq!(config.request_deadline, Some(timeout));
483 }
484}