#[cfg(feature = "p2")]
use crate::p2::bindings::http::types as p2;
#[cfg(feature = "p3")]
use crate::p3::bindings::http::types as p3;
use crate::{DEFAULT_FORBIDDEN_HEADERS, Error, RequestOptions, Result};
use bytes::Bytes;
use http::{HeaderName, uri::Scheme};
use http_body_util::combinators::UnsyncBoxBody;
use std::marker;
use std::sync::{Arc, LazyLock};
use wasmtime::component::{HasData, ResourceTable};
use wasmtime_wasi::{NamedId, WasiCtxNamedView};
pub struct WasiHttp;
impl HasData for WasiHttp {
type Data<'a> = WasiHttpCtxView<'a>;
}
pub trait WasiHttpView: Send {
fn http(&mut self) -> WasiHttpCtxView<'_>;
}
pub struct WasiHttpCtxView<'a> {
pub hooks: &'a mut dyn WasiHttpHooks,
pub table: &'a mut ResourceTable,
pub ctx: &'a mut WasiHttpCtx,
}
const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;
#[derive(Debug, Clone)]
pub struct WasiHttpCtx {
pub(crate) field_size_limit: usize,
}
impl WasiHttpCtx {
pub fn new() -> Self {
Self {
field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
}
}
pub fn set_field_size_limit(&mut self, limit: usize) {
self.field_size_limit = limit;
}
}
impl Default for WasiHttpCtx {
fn default() -> Self {
Self::new()
}
}
pub type WasiBody = UnsyncBoxBody<Bytes, Error>;
pub type ForbiddenHeaderFilter = Arc<dyn Fn(&HeaderName) -> bool + Send + Sync>;
pub trait WasiHttpHooks: Send {
fn forbidden_header_filter(&mut self) -> ForbiddenHeaderFilter {
static DEFAULT: LazyLock<ForbiddenHeaderFilter> =
LazyLock::new(|| Arc::new(|name| DEFAULT_FORBIDDEN_HEADERS.contains(name)));
DEFAULT.clone()
}
fn is_supported_scheme(&mut self, scheme: &Scheme) -> bool {
*scheme == Scheme::HTTP || *scheme == Scheme::HTTPS
}
fn set_host_header(&mut self) -> bool {
true
}
fn default_scheme(&mut self) -> Option<Scheme> {
Some(Scheme::HTTPS)
}
#[cfg(feature = "default-send-request")]
fn send_request(
&mut self,
request: http::Request<WasiBody>,
options: Option<RequestOptions>,
fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
) -> Box<
dyn Future<
Output = Result<(
http::Response<WasiBody>,
Box<dyn Future<Output = Result<(), Error>> + Send>,
)>,
> + Send,
> {
_ = fut;
Box::new(async move {
use http_body_util::BodyExt;
let (res, io) = crate::default_send_request(request, options).await?;
Ok((
res.map(BodyExt::boxed_unsync),
Box::new(io) as Box<dyn Future<Output = _> + Send>,
))
})
}
#[cfg(not(feature = "default-send-request"))]
fn send_request(
&mut self,
request: http::Request<WasiBody>,
options: Option<RequestOptions>,
fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
) -> Box<
dyn Future<
Output = Result<(
http::Response<WasiBody>,
Box<dyn Future<Output = Result<(), Error>> + Send>,
)>,
> + Send,
>;
#[cfg(feature = "p2")]
fn p2_outgoing_body_buffer_chunks(&mut self) -> usize {
crate::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS
}
#[cfg(feature = "p2")]
fn p2_outgoing_body_chunk_size(&mut self) -> usize {
crate::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
}
#[cfg(feature = "p2")]
fn p2_error_from_hyper(&mut self, err: &hyper::Error) -> p2::ErrorCode {
tracing::warn!("hyper error: {err:?}");
p2::ErrorCode::HttpProtocolError
}
#[cfg(feature = "p2")]
fn p2_error_from_connect(&mut self, err: &std::io::Error) -> p2::ErrorCode {
tracing::warn!("connect error: {err:?}");
use std::io::ErrorKind;
match err.kind() {
ErrorKind::ConnectionRefused => p2::ErrorCode::ConnectionRefused,
ErrorKind::HostUnreachable | ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => {
p2::ErrorCode::DestinationUnavailable
}
ErrorKind::TimedOut => p2::ErrorCode::ConnectionTimeout,
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted => {
p2::ErrorCode::ConnectionTerminated
}
_ if is_address_family_unsupported(err) => p2::ErrorCode::DestinationUnavailable,
_ => p2::ErrorCode::ConnectionRefused,
}
}
#[cfg(feature = "p2")]
fn p2_error_from_tls(&mut self, err: &std::io::Error) -> p2::ErrorCode {
tracing::warn!("tls error: {err:?}");
#[cfg(feature = "default-send-request")]
if let Some(err) = err
.get_ref()
.and_then(|err| err.downcast_ref::<rustls::Error>())
{
return match err {
rustls::Error::InvalidCertificate(_) => p2::ErrorCode::TlsCertificateError,
rustls::Error::AlertReceived(alert) => {
p2::ErrorCode::TlsAlertReceived(p2::TlsAlertReceivedPayload {
alert_id: Some(u8::from(*alert)),
alert_message: None,
})
}
_ => p2::ErrorCode::TlsProtocolError,
};
}
match err.kind() {
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::UnexpectedEof => p2::ErrorCode::ConnectionTerminated,
_ => p2::ErrorCode::TlsProtocolError,
}
}
#[cfg(all(feature = "p2", feature = "default-send-request"))]
fn p2_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p2::ErrorCode {
tracing::warn!("dns lookup error: {err:?}");
p2::ErrorCode::DnsError(p2::DnsErrorPayload {
rcode: Some("invalid dns name".to_string()),
info_code: None,
})
}
#[cfg(feature = "p3")]
fn p3_outgoing_body_chunk_size(&mut self) -> usize {
crate::p3::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
}
#[cfg(feature = "p3")]
fn p3_error_from_hyper(&mut self, err: &hyper::Error) -> p3::ErrorCode {
tracing::warn!("hyper error: {err:?}");
p3::ErrorCode::HttpProtocolError
}
#[cfg(feature = "p3")]
fn p3_error_from_connect(&mut self, err: &std::io::Error) -> p3::ErrorCode {
tracing::warn!("connect error: {err:?}");
use std::io::ErrorKind;
match err.kind() {
ErrorKind::ConnectionRefused => p3::ErrorCode::ConnectionRefused,
ErrorKind::HostUnreachable | ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => {
p3::ErrorCode::DestinationUnavailable
}
ErrorKind::TimedOut => p3::ErrorCode::ConnectionTimeout,
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted => {
p3::ErrorCode::ConnectionTerminated
}
_ if is_address_family_unsupported(err) => p3::ErrorCode::DestinationUnavailable,
_ => p3::ErrorCode::ConnectionRefused,
}
}
#[cfg(feature = "p3")]
fn p3_error_from_tls(&mut self, err: &std::io::Error) -> p3::ErrorCode {
tracing::warn!("tls error: {err:?}");
#[cfg(feature = "default-send-request")]
if let Some(err) = err
.get_ref()
.and_then(|err| err.downcast_ref::<rustls::Error>())
{
return match err {
rustls::Error::InvalidCertificate(_) => p3::ErrorCode::TlsCertificateError,
rustls::Error::AlertReceived(alert) => {
p3::ErrorCode::TlsAlertReceived(p3::TlsAlertReceivedPayload {
alert_id: Some(u8::from(*alert)),
alert_message: None,
})
}
_ => p3::ErrorCode::TlsProtocolError,
};
}
match err.kind() {
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::UnexpectedEof => p3::ErrorCode::ConnectionTerminated,
_ => p3::ErrorCode::TlsProtocolError,
}
}
#[cfg(all(feature = "p3", feature = "default-send-request"))]
fn p3_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p3::ErrorCode {
tracing::warn!("dns lookup error: {err:?}");
p3::ErrorCode::DnsError(p3::DnsErrorPayload {
rcode: Some("invalid dns name".to_string()),
info_code: None,
})
}
}
#[cfg(any(feature = "p2", feature = "p3"))]
fn is_address_family_unsupported(err: &std::io::Error) -> bool {
use rustix::io::Errno;
Errno::from_io_error(err) == Some(Errno::AFNOSUPPORT)
}
#[cfg(feature = "default-send-request")]
pub fn default_hooks() -> &'static mut dyn WasiHttpHooks {
Default::default()
}
#[cfg(feature = "default-send-request")]
impl<'a> Default for &'a mut dyn WasiHttpHooks {
fn default() -> Self {
let x: &mut [(); 0] = &mut [];
x
}
}
#[doc(hidden)]
#[cfg(feature = "default-send-request")]
impl WasiHttpHooks for [(); 0] {}
pub struct WasiHttpNamed<T>(marker::PhantomData<fn() -> T>);
impl<T> HasData for WasiHttpNamed<T>
where
T: WasiHttpNamedView,
{
type Data<'a> = WasiCtxNamedView<'a, T>;
}
pub trait WasiHttpNamedView: Send + 'static {
fn http(&mut self, id: NamedId) -> WasiHttpCtxView<'_>;
}