use std::net;
#[cfg(any(test, all(feature = "uds", unix)))]
use std::path::PathBuf;
#[cfg(feature = "iroh")]
use crate::iroh;
use crate::{Error, QuicBackend};
use moq_net::Session;
use url::Url;
use futures::FutureExt;
use futures::future::BoxFuture;
use futures::stream::FuturesUnordered;
use futures::stream::StreamExt;
#[derive(clap::Args, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct ServerConfig {
#[serde(alias = "listen")]
#[arg(id = "server-bind", long = "server-bind", alias = "listen", env = "MOQ_SERVER_BIND")]
pub bind: Option<String>,
#[cfg(feature = "tcp")]
#[command(flatten)]
#[serde(default)]
pub tcp: crate::tcp::Config,
#[cfg(all(feature = "uds", unix))]
#[command(flatten)]
#[serde(default)]
pub unix: crate::unix::Config,
#[arg(id = "server-backend", long = "server-backend", env = "MOQ_SERVER_BACKEND")]
pub backend: Option<QuicBackend>,
#[command(flatten)]
#[serde(default)]
pub quic: crate::quic::Server,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[arg(id = "server-version", long = "server-version", env = "MOQ_SERVER_VERSION")]
pub version: Vec<moq_net::Version>,
#[command(flatten)]
#[serde(default)]
pub tls: crate::tls::Server,
}
impl ServerConfig {
pub fn init(self) -> crate::Result<Server> {
Server::new(self)
}
pub fn versions(&self) -> moq_net::Versions {
if self.version.is_empty() {
moq_net::Versions::all()
} else {
moq_net::Versions::from(self.version.clone())
}
}
#[allow(unused_mut)]
fn has_stream_listener(&self) -> bool {
let mut has = false;
#[cfg(feature = "tcp")]
{
has |= self.tcp.bind.is_some();
}
#[cfg(all(feature = "uds", unix))]
{
has |= self.unix.bind.is_some();
}
has
}
}
pub(crate) const DEFAULT_BIND: &str = "[::]:443";
pub struct Server {
moq: moq_net::Server,
versions: moq_net::Versions,
accept: FuturesUnordered<BoxFuture<'static, crate::Result<Request>>>,
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
streams: StreamListeners,
#[cfg(feature = "iroh")]
iroh: Option<iroh::Endpoint>,
#[cfg(feature = "noq")]
noq: Option<crate::noq::NoqServer>,
#[cfg(feature = "quinn")]
quinn: Option<crate::quinn::QuinnServer>,
#[cfg(feature = "quiche")]
quiche: Option<crate::quiche::QuicheServer>,
#[cfg(feature = "websocket")]
websocket: Option<crate::websocket::Listener>,
}
impl Server {
pub fn new(config: ServerConfig) -> crate::Result<Self> {
let backend = config.backend.clone().unwrap_or_else(crate::default_quic_backend);
let versions = config.versions();
let build_quic = config.bind.is_some() || !config.has_stream_listener();
if build_quic && !config.tls.root.is_empty() {
let mtls_supported = match backend {
#[cfg(feature = "quinn")]
QuicBackend::Quinn => true,
#[cfg(feature = "noq")]
QuicBackend::Noq => true,
#[allow(unreachable_patterns)]
_ => false,
};
if !mtls_supported {
return Err(Error::MtlsUnsupported);
}
}
#[cfg(feature = "noq")]
#[allow(unreachable_patterns)]
let noq = match backend {
QuicBackend::Noq if build_quic => Some(crate::noq::NoqServer::new(config.clone())?),
_ => None,
};
#[cfg(feature = "quinn")]
#[allow(unreachable_patterns)]
let quinn = match backend {
QuicBackend::Quinn if build_quic => Some(crate::quinn::QuinnServer::new(config.clone())?),
_ => None,
};
#[cfg(feature = "quiche")]
let quiche = match backend {
QuicBackend::Quiche if build_quic => Some(crate::quiche::QuicheServer::new(config.clone())?),
_ => None,
};
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
let mut stream_binds = Vec::new();
#[cfg(feature = "tcp")]
if let Some(addr) = config.tcp.bind {
stream_binds.push(StreamBind::Tcp(addr));
}
#[cfg(all(feature = "uds", unix))]
if let Some(path) = config.unix.bind.clone() {
stream_binds.push(StreamBind::Unix(path));
}
#[cfg(all(feature = "uds", unix))]
let unix_allow = config.unix.allow.clone().filter(|allow| !allow.is_empty());
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
let streams = StreamListeners::new(
stream_binds,
stream_versions(&versions),
#[cfg(all(feature = "uds", unix))]
unix_allow,
);
Ok(Server {
accept: Default::default(),
moq: moq_net::Server::new().with_versions(versions.clone()),
versions,
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
streams,
#[cfg(feature = "iroh")]
iroh: None,
#[cfg(feature = "noq")]
noq,
#[cfg(feature = "quinn")]
quinn,
#[cfg(feature = "quiche")]
quiche,
#[cfg(feature = "websocket")]
websocket: None,
})
}
#[cfg(feature = "websocket")]
pub fn with_websocket(mut self, websocket: crate::websocket::Listener) -> Self {
self.websocket = Some(websocket);
self
}
#[cfg(feature = "iroh")]
pub fn with_iroh(mut self, iroh: iroh::Endpoint) -> Self {
self.iroh = Some(iroh);
self
}
pub fn with_publisher(mut self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
self.moq = self.moq.with_publisher(publish);
self
}
pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self {
self.moq = self.moq.with_subscriber(subscribe);
self
}
pub fn with_stats(mut self, stats: moq_net::stats::Session) -> Self {
self.moq = self.moq.with_stats(stats);
self
}
pub async fn serve_publish(self, origin: moq_net::origin::Consumer) -> crate::Result<()> {
self.with_publisher(origin).serve().await
}
pub async fn serve_consume(self, origin: moq_net::origin::Producer) -> crate::Result<()> {
self.with_subscriber(origin).serve().await
}
async fn serve(mut self) -> crate::Result<()> {
if let Ok(addr) = self.local_addr() {
tracing::info!(%addr, "listening");
}
while let Some(request) = self.accept().await {
tokio::spawn(async move {
if let Err(err) = serve_session(request).await {
tracing::warn!(%err, "session ended with error");
}
});
}
Ok(())
}
pub fn certificates(&self) -> crate::tls::Certificates {
#[cfg(feature = "noq")]
if let Some(noq) = self.noq.as_ref() {
return noq.certificates();
}
#[cfg(feature = "quinn")]
if let Some(quinn) = self.quinn.as_ref() {
return quinn.certificates();
}
#[cfg(feature = "quiche")]
if let Some(quiche) = self.quiche.as_ref() {
return quiche.certificates();
}
crate::tls::Certificates::empty()
}
#[cfg(not(any(
feature = "noq",
feature = "quinn",
feature = "quiche",
feature = "iroh",
feature = "tcp",
all(feature = "uds", unix)
)))]
pub async fn accept(&mut self) -> Option<Request> {
unreachable!("no transport compiled; enable a QUIC backend, tcp, or uds feature");
}
#[cfg(any(
feature = "noq",
feature = "quinn",
feature = "quiche",
feature = "iroh",
feature = "tcp",
all(feature = "uds", unix)
))]
pub async fn accept(&mut self) -> Option<Request> {
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
if let Err(err) = self.streams.ensure_started().await {
tracing::error!(%err, "failed to bind stream listener");
return None;
}
loop {
#[cfg(feature = "noq")]
let noq_accept = async {
#[cfg(feature = "noq")]
if let Some(noq) = self.noq.as_mut() {
return noq.accept().await;
}
None
};
#[cfg(not(feature = "noq"))]
let noq_accept = async { None::<()> };
#[cfg(feature = "iroh")]
let iroh_accept = async {
#[cfg(feature = "iroh")]
if let Some(endpoint) = self.iroh.as_mut() {
return endpoint.accept().await;
}
None
};
#[cfg(not(feature = "iroh"))]
let iroh_accept = async { None::<()> };
#[cfg(feature = "quinn")]
let quinn_accept = async {
#[cfg(feature = "quinn")]
if let Some(quinn) = self.quinn.as_mut() {
return quinn.accept().await;
}
None
};
#[cfg(not(feature = "quinn"))]
let quinn_accept = async { None::<()> };
#[cfg(feature = "quiche")]
let quiche_accept = async {
#[cfg(feature = "quiche")]
if let Some(quiche) = self.quiche.as_mut() {
return quiche.accept().await;
}
None
};
#[cfg(not(feature = "quiche"))]
let quiche_accept = async { None::<()> };
#[cfg(feature = "websocket")]
let ws_ref = self.websocket.as_ref();
#[cfg(feature = "websocket")]
let ws_accept = async {
match ws_ref {
Some(ws) => ws.accept().await,
None => std::future::pending().await,
}
};
#[cfg(not(feature = "websocket"))]
let ws_accept = std::future::pending::<Option<crate::Result<()>>>();
#[allow(unused_variables)]
let server = self.moq.clone();
#[allow(unused_variables)]
let versions = self.versions.clone();
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
let stream_accept = self.streams.recv();
#[cfg(not(any(feature = "tcp", all(feature = "uds", unix))))]
let stream_accept = std::future::pending::<Option<Request>>();
tokio::select! {
Some(request) = stream_accept => {
return Some(request);
}
Some(_conn) = noq_accept => {
#[cfg(feature = "noq")]
{
let alpns = versions.alpns();
self.accept.push(async move {
let (session, url, identity) = super::noq::accept(_conn, alpns).await?;
let request = server.accept_request(session).await?;
Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Noq(Box::new(request)) })
}.boxed());
}
}
Some(_conn) = quinn_accept => {
#[cfg(feature = "quinn")]
{
let alpns = versions.alpns();
self.accept.push(async move {
let (session, url, identity) = super::quinn::accept(_conn, alpns).await?;
let request = server.accept_request(session).await?;
Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quinn(Box::new(request)) })
}.boxed());
}
}
Some(_conn) = quiche_accept => {
#[cfg(feature = "quiche")]
{
let alpns = versions.alpns();
self.accept.push(async move {
let (session, url, identity) = super::quiche::accept(_conn, alpns).await?;
let request = server.accept_request(session).await?;
Ok(Request { transport: Transport::Quic, url, identity, kind: RequestKind::Quiche(Box::new(request)) })
}.boxed());
}
}
Some(_conn) = iroh_accept => {
#[cfg(feature = "iroh")]
self.accept.push(async move {
let (session, url, identity) = super::iroh::accept(_conn).await?;
let request = server.accept_request(session).await?;
Ok(Request { transport: Transport::Iroh, url, identity, kind: RequestKind::Iroh(Box::new(request)) })
}.boxed());
}
Some(_res) = ws_accept => {
#[cfg(feature = "websocket")]
match _res {
Ok(session) => {
self.accept.push(async move {
let request = server.accept_request(session).await?;
Ok(Request { transport: Transport::WebSocket, url: None, identity: None, kind: RequestKind::Qmux(Box::new(request)) })
}.boxed());
}
Err(err) => tracing::debug!(%err, "failed to accept WebSocket session"),
}
}
Some(res) = self.accept.next() => {
match res {
Ok(session) => return Some(session),
Err(err) => tracing::debug!(%err, "failed to accept session"),
}
}
_ = tokio::signal::ctrl_c() => {
self.close().await;
return None;
}
}
}
}
#[cfg(feature = "iroh")]
pub fn iroh_endpoint(&self) -> Option<&iroh::Endpoint> {
self.iroh.as_ref()
}
pub fn local_addr(&self) -> crate::Result<net::SocketAddr> {
#[cfg(feature = "noq")]
if let Some(noq) = self.noq.as_ref() {
return Ok(noq.local_addr()?);
}
#[cfg(feature = "quinn")]
if let Some(quinn) = self.quinn.as_ref() {
return Ok(quinn.local_addr()?);
}
#[cfg(feature = "quiche")]
if let Some(quiche) = self.quiche.as_ref() {
return Ok(quiche.local_addr()?);
}
Err(Error::NoBackend("no QUIC listener configured"))
}
#[cfg(feature = "websocket")]
pub fn websocket_local_addr(&self) -> Option<net::SocketAddr> {
self.websocket.as_ref().and_then(|ws| ws.local_addr().ok())
}
pub async fn close(&mut self) {
#[cfg(feature = "noq")]
if let Some(noq) = self.noq.as_mut() {
noq.close();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
#[cfg(feature = "quinn")]
if let Some(quinn) = self.quinn.as_mut() {
quinn.close();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
#[cfg(feature = "quiche")]
if let Some(quiche) = self.quiche.as_mut() {
quiche.close();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
#[cfg(feature = "iroh")]
if let Some(iroh) = self.iroh.take() {
iroh.close().await;
}
#[cfg(feature = "websocket")]
{
let _ = self.websocket.take();
}
#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "iroh")))]
unreachable!("no QUIC backend compiled");
}
}
async fn serve_session(request: Request) -> crate::Result<()> {
let session = request.ok().await?;
Err(session.closed().await.into())
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
fn stream_versions(base: &moq_net::Versions) -> moq_net::Versions {
let mut versions: Vec<moq_net::Version> = base.iter().copied().collect();
if let Ok(lite05) = "moq-lite-05".parse::<moq_net::Version>() {
if !versions.contains(&lite05) {
versions.push(lite05);
}
}
moq_net::Versions::from(versions)
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
enum StreamBind {
#[cfg(feature = "tcp")]
Tcp(net::SocketAddr),
#[cfg(all(feature = "uds", unix))]
Unix(PathBuf),
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
struct StreamListeners {
binds: Vec<StreamBind>,
versions: moq_net::Versions,
#[cfg(all(feature = "uds", unix))]
unix_allow: Option<crate::unix::Allow>,
rx: Option<tokio::sync::mpsc::Receiver<Request>>,
tasks: Vec<tokio::task::JoinHandle<()>>,
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
impl StreamListeners {
fn new(
binds: Vec<StreamBind>,
versions: moq_net::Versions,
#[cfg(all(feature = "uds", unix))] unix_allow: Option<crate::unix::Allow>,
) -> Self {
Self {
binds,
versions,
#[cfg(all(feature = "uds", unix))]
unix_allow,
rx: None,
tasks: Vec::new(),
}
}
async fn ensure_started(&mut self) -> crate::Result<()> {
if self.rx.is_some() || self.binds.is_empty() {
return Ok(());
}
let (tx, rx) = tokio::sync::mpsc::channel(16);
for bind in self.binds.drain(..) {
let versions = self.versions.clone();
match bind {
#[cfg(feature = "tcp")]
StreamBind::Tcp(addr) => {
if !addr.ip().is_loopback() {
tracing::warn!(%addr, "tcp listener bound to a non-loopback address; qmux is UNENCRYPTED, ensure the network is trusted");
}
let listener = crate::tcp::Listener::bind(addr).await?.with_protocols(versions.alpns());
tracing::info!(%addr, "listening (tcp)");
self.tasks.push(spawn_tcp_loop(listener, versions, tx.clone()));
}
#[cfg(all(feature = "uds", unix))]
StreamBind::Unix(path) => {
let listener = crate::unix::Listener::bind(&path)
.await?
.with_protocols(versions.alpns());
listener.set_mode(0o666)?;
tracing::info!(path = %path.display(), allow = ?self.unix_allow, "listening (unix)");
self.tasks
.push(spawn_unix_loop(listener, versions, self.unix_allow.clone(), tx.clone()));
}
}
}
self.rx = Some(rx);
Ok(())
}
async fn recv(&mut self) -> Option<Request> {
match self.rx.as_mut() {
Some(rx) => rx.recv().await,
None => std::future::pending().await,
}
}
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
impl Drop for StreamListeners {
fn drop(&mut self) {
for task in &self.tasks {
task.abort();
}
}
}
#[cfg(feature = "tcp")]
fn spawn_tcp_loop(
listener: crate::tcp::Listener,
versions: moq_net::Versions,
tx: tokio::sync::mpsc::Sender<Request>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
match listener.accept().await {
Some(Ok(session)) => spawn_stream_request(session, Transport::Tcp, versions.clone(), tx.clone()),
Some(Err(err)) => tracing::warn!(%err, "tcp listener accept failed"),
None => break,
}
}
})
}
#[cfg(all(feature = "uds", unix))]
fn spawn_unix_loop(
listener: crate::unix::Listener,
versions: moq_net::Versions,
allow: Option<crate::unix::Allow>,
tx: tokio::sync::mpsc::Sender<Request>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
match listener.accept().await {
Some(Ok((session, cred))) => {
if let Some(allow) = &allow
&& !allow.permits(&cred)
{
tracing::warn!(uid = cred.uid, gid = cred.gid, pid = ?cred.pid, "unix connection rejected by allow list");
continue;
}
spawn_stream_request(session, Transport::Unix, versions.clone(), tx.clone());
}
Some(Err(err)) => tracing::warn!(%err, "unix listener accept failed"),
None => break,
}
}
})
}
#[cfg(any(feature = "tcp", all(feature = "uds", unix)))]
fn spawn_stream_request(
session: qmux::Session,
transport: Transport,
versions: moq_net::Versions,
tx: tokio::sync::mpsc::Sender<Request>,
) {
tokio::spawn(async move {
let server = moq_net::Server::new().with_versions(versions);
match server.accept_request(session).await {
Ok(request) => {
let request = Request {
transport,
url: None,
identity: None,
kind: RequestKind::Qmux(Box::new(request)),
};
let _ = tx.send(request).await;
}
Err(err) => tracing::debug!(%err, "stream SETUP handshake failed"),
}
});
}
pub(crate) enum RequestKind {
#[cfg(feature = "noq")]
Noq(Box<moq_net::Request<web_transport_noq::Session>>),
#[cfg(feature = "quinn")]
Quinn(Box<moq_net::Request<web_transport_quinn::Session>>),
#[cfg(feature = "quiche")]
Quiche(Box<moq_net::Request<web_transport_quiche::Connection>>),
#[cfg(feature = "iroh")]
Iroh(Box<moq_net::Request<web_transport_iroh::Session>>),
#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
Qmux(Box<moq_net::Request<qmux::Session>>),
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Transport {
Quic,
Iroh,
WebSocket,
Tcp,
Unix,
}
impl Transport {
pub const fn as_str(self) -> &'static str {
match self {
Self::Quic => "quic",
Self::Iroh => "iroh",
Self::WebSocket => "websocket",
Self::Tcp => "tcp",
Self::Unix => "unix",
}
}
}
impl std::fmt::Display for Transport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub struct Request {
transport: Transport,
url: Option<Url>,
identity: Option<crate::tls::PeerIdentity>,
kind: RequestKind,
}
macro_rules! request_ref {
($self:expr, $r:ident => $body:expr) => {
match &$self.kind {
#[cfg(feature = "noq")]
RequestKind::Noq($r) => $body,
#[cfg(feature = "quinn")]
RequestKind::Quinn($r) => $body,
#[cfg(feature = "quiche")]
RequestKind::Quiche($r) => $body,
#[cfg(feature = "iroh")]
RequestKind::Iroh($r) => $body,
#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
RequestKind::Qmux($r) => $body,
}
};
}
macro_rules! request_into {
($kind:expr, $r:ident => $body:expr) => {
match $kind {
#[cfg(feature = "noq")]
RequestKind::Noq($r) => $body,
#[cfg(feature = "quinn")]
RequestKind::Quinn($r) => $body,
#[cfg(feature = "quiche")]
RequestKind::Quiche($r) => $body,
#[cfg(feature = "iroh")]
RequestKind::Iroh($r) => $body,
#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
RequestKind::Qmux($r) => $body,
}
};
}
macro_rules! request_map {
($kind:expr, $r:ident => $body:expr) => {
match $kind {
#[cfg(feature = "noq")]
RequestKind::Noq($r) => RequestKind::Noq(Box::new($body)),
#[cfg(feature = "quinn")]
RequestKind::Quinn($r) => RequestKind::Quinn(Box::new($body)),
#[cfg(feature = "quiche")]
RequestKind::Quiche($r) => RequestKind::Quiche(Box::new($body)),
#[cfg(feature = "iroh")]
RequestKind::Iroh($r) => RequestKind::Iroh(Box::new($body)),
#[cfg(any(feature = "tcp", all(feature = "uds", unix), feature = "websocket"))]
RequestKind::Qmux($r) => RequestKind::Qmux(Box::new($body)),
}
};
}
impl Request {
pub async fn close(self, code: u16) -> crate::Result<()> {
let err = match code {
401 | 403 => moq_net::Error::Unauthorized,
other => moq_net::Error::App(other),
};
request_into!(self.kind, request => request.close(err));
Ok(())
}
pub fn with_publisher(self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
let Request {
transport,
url,
identity,
kind,
} = self;
let kind = request_map!(kind, request => request.with_publisher(publish));
Request {
transport,
url,
identity,
kind,
}
}
pub fn with_subscriber(self, subscribe: moq_net::origin::Producer) -> Self {
let Request {
transport,
url,
identity,
kind,
} = self;
let kind = request_map!(kind, request => request.with_subscriber(subscribe));
Request {
transport,
url,
identity,
kind,
}
}
pub fn with_stats(self, stats: moq_net::stats::Session) -> Self {
let Request {
transport,
url,
identity,
kind,
} = self;
let kind = request_map!(kind, request => request.with_stats(stats));
Request {
transport,
url,
identity,
kind,
}
}
pub async fn ok(self) -> crate::Result<Session> {
let pair = request_into!(self.kind, request => request.ok().await?);
Ok(crate::spawn_session(pair))
}
pub fn transport(&self) -> Transport {
self.transport
}
pub fn url(&self) -> Option<&Url> {
self.url.as_ref()
}
pub fn path(&self) -> &str {
let setup = request_ref!(self, r => r.path());
if setup.is_empty() {
self.url.as_ref().map(Url::path).unwrap_or("")
} else {
setup
}
}
pub fn role(&self) -> Option<moq_net::Role> {
request_ref!(self, r => r.role())
}
pub fn peer_identity(&self) -> Option<crate::tls::PeerIdentity> {
self.identity.clone()
}
#[doc(hidden)]
#[deprecated(note = "use `peer_identity` instead")]
pub fn has_peer_certificate(&self) -> bool {
self.peer_identity().is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transport_names_are_stable() {
assert_eq!(Transport::Quic.as_str(), "quic");
assert_eq!(Transport::Iroh.as_str(), "iroh");
assert_eq!(Transport::WebSocket.as_str(), "websocket");
assert_eq!(Transport::Tcp.as_str(), "tcp");
assert_eq!(Transport::Unix.as_str(), "unix");
}
#[cfg(feature = "quinn")]
#[tokio::test]
async fn certificates_expose_generated_fingerprints() {
let mut config = ServerConfig {
bind: Some("[::]:0".to_string()),
..Default::default()
};
config.tls.generate = vec!["localhost".into()];
let certs = config.init().expect("server init").certificates();
let fingerprints = certs.fingerprints();
assert_eq!(fingerprints.len(), 1, "one generated certificate");
assert_eq!(fingerprints[0].len(), 64);
assert!(fingerprints[0].chars().all(|c| c.is_ascii_hexdigit()));
}
#[cfg(all(feature = "uds", unix))]
#[tokio::test]
async fn certificates_are_empty_without_a_tls_backend() {
let mut config = ServerConfig::default();
config.unix.bind = Some(PathBuf::from("/tmp/moq-native-certificates-test.sock"));
let server = config.init().expect("server init");
assert!(server.certificates().fingerprints().is_empty());
}
#[test]
fn test_tls_string_or_array() {
let single = r#"
cert = "cert.pem"
key = "key.pem"
"#;
let config: crate::tls::Server = toml::from_str(single).unwrap();
assert_eq!(config.cert, vec![PathBuf::from("cert.pem")]);
assert_eq!(config.key, vec![PathBuf::from("key.pem")]);
let array = r#"
cert = ["a.pem", "b.pem"]
key = ["a.key", "b.key"]
generate = ["localhost"]
root = ["ca.pem"]
"#;
let config: crate::tls::Server = toml::from_str(array).unwrap();
assert_eq!(config.cert, vec![PathBuf::from("a.pem"), PathBuf::from("b.pem")]);
assert_eq!(config.key, vec![PathBuf::from("a.key"), PathBuf::from("b.key")]);
assert_eq!(config.generate, vec!["localhost".to_string()]);
assert_eq!(config.root, vec![PathBuf::from("ca.pem")]);
}
#[test]
fn bind_string_or_listen_alias() {
let bind: ServerConfig = toml::from_str(r#"bind = "[::]:443""#).unwrap();
assert_eq!(bind.bind.as_deref(), Some("[::]:443"));
let alias: ServerConfig = toml::from_str(r#"listen = "0.0.0.0:4443""#).unwrap();
assert_eq!(alias.bind.as_deref(), Some("0.0.0.0:4443"));
}
#[cfg(all(feature = "uds", unix))]
#[test]
fn stream_listener_config_parses() {
let config: ServerConfig = toml::from_str(
r#"
bind = "[::]:443"
[unix]
bind = "/run/moq.sock"
[unix.allow]
uid = [1001, 1002]
"#,
)
.unwrap();
assert_eq!(config.bind.as_deref(), Some("[::]:443"));
assert_eq!(config.unix.bind.as_deref(), Some(std::path::Path::new("/run/moq.sock")));
assert_eq!(config.unix.allow.as_ref().expect("allow").uid, vec![1001, 1002]);
assert!(config.has_stream_listener());
}
#[cfg(all(feature = "uds", unix))]
#[test]
fn stream_only_config_has_no_quic() {
let mut config = ServerConfig::default();
config.unix.bind = Some(PathBuf::from("/run/moq.sock"));
assert!(config.has_stream_listener());
assert!(config.bind.is_none());
assert!(!ServerConfig::default().has_stream_listener());
}
}