use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use http::StatusCode;
use tokio::io::{AsyncRead, AsyncWrite};
use super::ErrorPage;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListenerKind {
Local,
Share,
}
#[derive(Debug, Clone, Copy)]
pub struct RouteRequest<'a> {
pub host: &'a str,
pub tls: bool,
pub kind: ListenerKind,
pub method: &'a http::Method,
pub path: &'a str,
pub authorization: Option<&'a str>,
pub referrer: Option<&'a str>,
pub opaque_origin: bool,
pub fetch_site: Option<&'a str>,
pub fetch_mode: Option<&'a str>,
pub fetch_dest: Option<&'a str>,
pub purpose: Option<&'a str>,
}
pub enum Route {
Upstream {
port: u16,
guard: Box<dyn ActivityGuard>,
checked_authorization: bool,
},
Dialed {
dialer: Dialer,
guard: Box<dyn ActivityGuard>,
},
File {
path: std::path::PathBuf,
guard: Box<dyn ActivityGuard>,
},
Error(ErrorPage),
}
pub trait ActivityGuard: Send + 'static {
fn long_lived(&mut self);
fn closed(&self) -> Option<BoxFuture<'static, ErrorPage>> {
None
}
}
impl ActivityGuard for () {
fn long_lived(&mut self) {}
}
pub trait UpstreamIo: AsyncRead + AsyncWrite + Send + Unpin {}
impl<T: AsyncRead + AsyncWrite + Send + Unpin> UpstreamIo for T {}
pub type UpstreamStream = Box<dyn UpstreamIo>;
pub trait Dial: Send + Sync + 'static {
fn dial(&self) -> BoxFuture<'_, io::Result<UpstreamStream>>;
fn failed(&self, error: &UpstreamError) -> ErrorPage;
}
#[derive(Clone)]
pub struct Dialer {
id: u64,
dial: Arc<dyn Dial>,
}
impl Dialer {
pub fn new(dial: Arc<dyn Dial>) -> Self {
static NEXT: AtomicU64 = AtomicU64::new(1);
Dialer {
id: NEXT.fetch_add(1, Ordering::Relaxed),
dial,
}
}
pub fn id(&self) -> u64 {
self.id
}
pub fn dial(&self) -> BoxFuture<'_, io::Result<UpstreamStream>> {
self.dial.dial()
}
pub fn failed(&self, error: &UpstreamError) -> ErrorPage {
self.dial.failed(error)
}
pub(crate) fn downgrade(&self) -> std::sync::Weak<dyn Dial> {
Arc::downgrade(&self.dial)
}
}
impl PartialEq for Dialer {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl std::fmt::Debug for Dialer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dialer").field("id", &self.id).finish()
}
}
#[derive(Debug)]
pub enum UpstreamError {
Connect(io::Error),
Request(String),
NotHttp(String),
}
pub trait Router: Send + Sync + 'static {
fn route<'a>(&'a self, request: RouteRequest<'a>) -> BoxFuture<'a, Route>;
fn closing(&self, kind: ListenerKind) -> Option<BoxFuture<'static, ()>> {
let _ = kind;
None
}
fn upstream_failed(
&self,
request: RouteRequest<'_>,
port: u16,
error: &UpstreamError,
) -> ErrorPage {
let _ = request;
match error {
UpstreamError::Connect(error) => ErrorPage::new(
StatusCode::BAD_GATEWAY,
"App not reachable",
format!("Nothing answered on port {port}: {error}"),
),
UpstreamError::Request(error) => ErrorPage::new(
StatusCode::BAD_GATEWAY,
"App failed to respond",
format!("The request to port {port} failed: {error}"),
),
UpstreamError::NotHttp(error) => ErrorPage::new(
StatusCode::BAD_GATEWAY,
"App sent an invalid response",
format!(
"The app on port {port} answered with something that isn't HTTP ({error}). \
Dev servers do this when they refuse a request, such as Next.js for a \
host that isn't in its allowedDevOrigins."
),
),
}
}
}