mod headers;
mod instrument;
mod outbound_http;
mod server;
mod spin;
mod tls;
mod wagi;
mod wasi;
use std::{
error::Error,
net::{Ipv4Addr, SocketAddr, ToSocketAddrs},
path::PathBuf,
sync::Arc,
};
use anyhow::{bail, Context};
use clap::Args;
use serde::Deserialize;
use spin_app::App;
use spin_factors::RuntimeFactors;
use spin_trigger::Trigger;
use wasmtime_wasi_http::bindings::http::types::ErrorCode;
pub use server::HttpServer;
pub use tls::TlsConfig;
pub(crate) use wasmtime_wasi_http::body::HyperIncomingBody as Body;
pub(crate) type TriggerApp<F> = spin_trigger::TriggerApp<HttpTrigger, F>;
pub(crate) type TriggerInstanceBuilder<'a, F> =
spin_trigger::TriggerInstanceBuilder<'a, HttpTrigger, F>;
#[derive(Args)]
pub struct CliArgs {
#[clap(long = "listen", env = "SPIN_HTTP_LISTEN_ADDR", default_value = "127.0.0.1:3000", value_parser = parse_listen_addr)]
pub address: SocketAddr,
#[clap(long, env = "SPIN_TLS_CERT", requires = "tls-key")]
pub tls_cert: Option<PathBuf>,
#[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")]
pub tls_key: Option<PathBuf>,
}
impl CliArgs {
fn into_tls_config(self) -> Option<TlsConfig> {
match (self.tls_cert, self.tls_key) {
(Some(cert_path), Some(key_path)) => Some(TlsConfig {
cert_path,
key_path,
}),
(None, None) => None,
_ => unreachable!(),
}
}
}
pub struct HttpTrigger {
listen_addr: SocketAddr,
tls_config: Option<TlsConfig>,
}
impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
const TYPE: &'static str = "http";
type CliArgs = CliArgs;
type InstanceState = ();
fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
Self::new(app, cli_args.address, cli_args.into_tls_config())
}
async fn run(self, trigger_app: TriggerApp<F>) -> anyhow::Result<()> {
let server = self.into_server(trigger_app)?;
server.serve().await?;
Ok(())
}
fn supported_host_requirements() -> Vec<&'static str> {
vec![spin_app::locked::SERVICE_CHAINING_KEY]
}
}
impl HttpTrigger {
pub fn new(
app: &spin_app::App,
listen_addr: SocketAddr,
tls_config: Option<TlsConfig>,
) -> anyhow::Result<Self> {
Self::validate_app(app)?;
Ok(Self {
listen_addr,
tls_config,
})
}
pub fn into_server<F: RuntimeFactors>(
self,
trigger_app: TriggerApp<F>,
) -> anyhow::Result<Arc<HttpServer<F>>> {
let Self {
listen_addr,
tls_config,
} = self;
let server = Arc::new(HttpServer::new(listen_addr, tls_config, trigger_app)?);
Ok(server)
}
fn validate_app(app: &App) -> anyhow::Result<()> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TriggerMetadata {
base: Option<String>,
}
if let Some(TriggerMetadata { base: Some(base) }) = app.get_trigger_metadata("http")? {
if base == "/" {
tracing::warn!("This application has the deprecated trigger 'base' set to the default value '/'. This may be an error in the future!");
} else {
bail!("This application is using the deprecated trigger 'base' field. The base must be prepended to each [[trigger.http]]'s 'route'.")
}
}
Ok(())
}
}
fn parse_listen_addr(addr: &str) -> anyhow::Result<SocketAddr> {
let addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
if let Some(addr) = addrs
.iter()
.find(|addr| addr.is_ipv4() && addr.ip() == Ipv4Addr::LOCALHOST)
{
return Ok(*addr);
}
addrs.into_iter().next().context("couldn't resolve address")
}
#[derive(Debug, PartialEq)]
enum NotFoundRouteKind {
Normal(String),
WellKnown,
}
pub fn hyper_request_error(err: hyper::Error) -> ErrorCode {
if let Some(cause) = err.source() {
if let Some(err) = cause.downcast_ref::<ErrorCode>() {
return err.clone();
}
}
tracing::warn!("hyper request error: {err:?}");
ErrorCode::HttpProtocolError
}
pub fn dns_error(rcode: String, info_code: u16) -> ErrorCode {
ErrorCode::DnsError(wasmtime_wasi_http::bindings::http::types::DnsErrorPayload {
rcode: Some(rcode),
info_code: Some(info_code),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_listen_addr_prefers_ipv4() {
let addr = parse_listen_addr("localhost:12345").unwrap();
assert_eq!(addr.ip(), Ipv4Addr::LOCALHOST);
assert_eq!(addr.port(), 12345);
}
}