use std::future::Future;
use std::net::SocketAddr;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use crate::ServeError;
#[derive(Debug)]
enum Unreachable {
Lookup(std::io::Error),
NotLocal,
}
impl Unreachable {
fn into_serve_error(self, url: &str) -> ServeError {
let url = url.to_owned();
match self {
Self::Lookup(_) => ServeError::BackendUnresolvable { url },
Self::NotLocal => ServeError::BackendNotLocal { url },
}
}
}
impl From<Unreachable> for std::io::Error {
fn from(e: Unreachable) -> Self {
match e {
Unreachable::Lookup(e) => e,
Unreachable::NotLocal => {
Self::other("no resolved address may be reached by this listener")
}
}
}
}
use crate::locality::{admits, classify};
#[derive(Debug)]
pub(crate) struct TcpBackend {
host: String,
port: u16,
authority: String,
allow_private: bool,
}
impl TcpBackend {
pub(crate) async fn new(url: &str, allow_private: bool) -> Result<Self, ServeError> {
let invalid = || ServeError::InvalidBackendUrl {
url: url.to_owned(),
};
let parsed = url::Url::parse(url).map_err(|_| invalid())?;
if parsed.scheme() != "http" {
return Err(invalid());
}
if !matches!(parsed.path(), "" | "/")
|| parsed.query().is_some()
|| parsed.fragment().is_some()
|| !parsed.username().is_empty()
|| parsed.password().is_some()
{
return Err(invalid());
}
let host = parsed.host_str().ok_or_else(invalid)?;
let host = host
.strip_prefix('[')
.and_then(|h| h.strip_suffix(']'))
.unwrap_or(host)
.to_owned();
let port = parsed.port().unwrap_or(80);
let authority = if host.parse::<std::net::Ipv6Addr>().is_ok() {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
};
let this = Self {
host,
port,
authority,
allow_private,
};
match this.resolve().await {
Ok(_) => Ok(this),
Err(e) => Err(e.into_serve_error(url)),
}
}
async fn resolve(&self) -> Result<Vec<SocketAddr>, Unreachable> {
let candidates = tokio::net::lookup_host((self.host.as_str(), self.port))
.await
.map_err(Unreachable::Lookup)?;
let admissible = screen(candidates, self.allow_private);
if admissible.is_empty() {
return Err(Unreachable::NotLocal);
}
Ok(admissible)
}
}
pub(crate) trait Backend {
type Stream: AsyncRead + AsyncWrite + Unpin + Send;
fn authority(&self) -> &str;
fn connect(&self) -> impl Future<Output = std::io::Result<Self::Stream>> + Send;
}
impl Backend for TcpBackend {
type Stream = TcpStream;
fn authority(&self) -> &str {
&self.authority
}
async fn connect(&self) -> std::io::Result<TcpStream> {
let mut last = None;
for addr in self.resolve().await? {
match TcpStream::connect(addr).await {
Ok(stream) => return Ok(stream),
Err(e) => last = Some(e),
}
}
Err(last.unwrap_or_else(|| {
std::io::Error::other(format!("{} resolved to no address", self.authority))
}))
}
}
pub(crate) fn screen(
candidates: impl IntoIterator<Item = SocketAddr>,
allow_private: bool,
) -> Vec<SocketAddr> {
candidates
.into_iter()
.filter(|addr| admits(classify(addr.ip()), allow_private))
.collect()
}
#[cfg(test)]
#[path = "backend_tests.rs"]
mod backend_tests;