use std::io::{self, Read, Write};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, TcpStream};
use std::num::NonZeroUsize;
use std::os::fd::OwnedFd;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use rustls::{ServerConfig, ServerConnection, StreamOwned};
use serde::Deserialize;
use super::super::service::Service;
use super::clients::Clients;
use super::listener::{Accepted, admit, spawn_client};
use super::{Limits, Wire};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NetworkConfig {
pub listen: SocketAddr,
#[serde(default)]
pub tls: Option<TlsFiles>,
#[serde(default)]
pub max_connections: Option<NonZeroUsize>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TlsFiles {
pub certificate_file: PathBuf,
pub key_file: PathBuf,
}
pub struct Network {
listener: TcpListener,
address: SocketAddr,
tls: Option<Arc<ServerConfig>>,
max_connections: NonZeroUsize,
stopping: Arc<AtomicBool>,
active: Arc<Clients>,
}
impl Network {
pub fn bind(
config: &NetworkConfig,
service: &Service,
max_clients: NonZeroUsize,
) -> Result<Self, String> {
if !service.decides() {
return Err("network_requires_lys: a network connection is admitted only by the Lys pass it presents; add a lys section to the service configuration".to_owned());
}
let tls = match &config.tls {
Some(files) => Some(Arc::new(server_config(files)?)),
None if config.listen.ip().is_loopback() => None,
None => {
return Err(format!(
"network_config_invalid: listen {} is not a loopback address, so it needs tls with a certificate_file and key_file; plain TCP is served only on loopback, behind a TLS proxy",
config.listen
));
}
};
let listener = TcpListener::bind(config.listen)
.map_err(|error| format!("network_bind_failed: {}: {error}", config.listen))?;
let address = listener
.local_addr()
.map_err(|error| format!("network_bind_failed: {}: {error}", config.listen))?;
Ok(Self {
listener,
address,
tls,
max_connections: config.max_connections.unwrap_or(max_clients),
stopping: Arc::new(AtomicBool::new(false)),
active: Arc::new(Clients::new(0)),
})
}
#[must_use]
pub const fn address(&self) -> SocketAddr {
self.address
}
#[must_use]
pub const fn secured(&self) -> bool {
self.tls.is_some()
}
#[must_use]
pub fn stopper(&self) -> Stop {
Stop {
stopping: Arc::clone(&self.stopping),
wake: wake_address(self.address),
}
}
#[cfg(test)]
pub(super) fn occupancy(&self) -> Occupancy {
Occupancy(Arc::clone(&self.active))
}
pub fn run(&self, service: &Arc<Service>, limits: Limits) -> io::Result<()> {
if limits.frame_timeout.is_zero() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"frame timeout must be positive",
));
}
let limits = Limits {
max_clients: self.max_connections,
..limits
};
let incoming = self
.listener
.incoming()
.map_while(|accepted| (!self.stopping.load(Ordering::Acquire)).then_some(accepted))
.map(|accepted| {
accepted.map(|tcp| Peer {
tcp,
tls: self.tls.clone(),
})
});
admit(incoming, &self.active, service, limits, spawn_client);
self.active.drain()
}
}
#[derive(Clone)]
pub struct Stop {
stopping: Arc<AtomicBool>,
wake: SocketAddr,
}
impl Stop {
pub fn stop(&self) {
self.stopping.store(true, Ordering::Release);
if let Err(error) = TcpStream::connect_timeout(&self.wake, Duration::from_secs(1)) {
eprintln!("haematite network_stop_wake_failed: {}: {error}", self.wake);
}
}
}
#[cfg(test)]
pub(super) struct Occupancy(Arc<Clients>);
#[cfg(test)]
impl Occupancy {
pub(super) fn wait_at_most(&self, count: usize, bound: Duration) -> bool {
self.0.wait_at_most(count, bound)
}
}
const fn wake_address(address: SocketAddr) -> SocketAddr {
let ip = match address.ip() {
IpAddr::V4(ip) if ip.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V6(ip) if ip.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST),
ip => ip,
};
SocketAddr::new(ip, address.port())
}
pub(crate) fn server_config(files: &TlsFiles) -> Result<ServerConfig, String> {
let invalid =
|what: &str, error: &dyn std::fmt::Display| format!("network_tls_invalid: {what}: {error}");
if !files.certificate_file.is_absolute() || !files.key_file.is_absolute() {
return Err(
"network_tls_invalid: certificate_file and key_file must be absolute paths".to_owned(),
);
}
let certificate = files.certificate_file.display().to_string();
let chain = CertificateDer::pem_file_iter(&files.certificate_file)
.map_err(|error| invalid(&certificate, &error))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| invalid(&certificate, &error))?;
if chain.is_empty() {
return Err(invalid(&certificate, &"holds no certificate"));
}
let key_file = files.key_file.display().to_string();
let key = PrivateKeyDer::from_pem_file(&files.key_file)
.map_err(|error| invalid(&key_file, &error))?;
ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.map_err(|error| invalid("protocol versions", &error))?
.with_no_client_auth()
.with_single_cert(chain, key)
.map_err(|error| invalid("certificate and key", &error))
}
struct Peer {
tcp: TcpStream,
tls: Option<Arc<ServerConfig>>,
}
impl Accepted for Peer {
type Wire = NetworkWire;
fn duplicate(&self) -> io::Result<Self> {
Ok(Self {
tcp: self.tcp.try_clone()?,
tls: self.tls.clone(),
})
}
fn into_wire(self) -> io::Result<NetworkWire> {
Ok(match self.tls {
None => NetworkWire::Plain(self.tcp),
Some(config) => NetworkWire::Tls(Box::new(StreamOwned::new(
ServerConnection::new(config).map_err(io::Error::other)?,
self.tcp,
))),
})
}
}
pub(super) enum NetworkWire {
Plain(TcpStream),
Tls(Box<StreamOwned<ServerConnection, TcpStream>>),
}
impl NetworkWire {
fn tcp(&self) -> &TcpStream {
match self {
Self::Plain(tcp) => tcp,
Self::Tls(tls) => &tls.sock,
}
}
}
impl Read for NetworkWire {
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
match self {
Self::Plain(tcp) => tcp.read(bytes),
Self::Tls(tls) => match tls.read(bytes) {
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(0),
read => read,
},
}
}
}
impl Write for NetworkWire {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
match self {
Self::Plain(tcp) => tcp.write(bytes),
Self::Tls(tls) => tls.write(bytes),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Plain(tcp) => tcp.flush(),
Self::Tls(tls) => tls.flush(),
}
}
}
impl Wire for NetworkWire {
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
self.tcp().set_read_timeout(timeout)
}
fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
self.tcp().set_write_timeout(timeout)
}
fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
self.tcp().set_nonblocking(nonblocking)
}
fn peer(&self) -> io::Result<OwnedFd> {
self.tcp().try_clone().map(OwnedFd::from)
}
}