use std::os::unix::fs::{FileTypeExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::{fs, io};
use url::Url;
const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error("missing socket path in unix:// URL")]
MissingPath,
#[error("qmux connect failed")]
Connect(#[source] qmux::Error),
#[error("qmux accept failed")]
Accept(#[source] qmux::Error),
#[error("refusing to replace existing non-socket file at {0}")]
NotASocket(PathBuf),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PeerCred {
pub uid: u32,
pub gid: u32,
pub pid: Option<i32>,
}
pub(crate) async fn connect(url: Url, protocols: &[&str]) -> Result<qmux::Session> {
let path = socket_path(&url).ok_or(Error::MissingPath)?;
tracing::debug!(%url, "connecting via Unix socket");
qmux::uds::Config::new(WIRE_VERSION)
.protocols(protocols.iter().copied())
.connect(path)
.await
.map_err(Error::Connect)
}
fn socket_path(url: &Url) -> Option<PathBuf> {
let path = url.path();
if path.is_empty() {
None
} else {
Some(PathBuf::from(path))
}
}
pub struct Listener {
listener: tokio::net::UnixListener,
path: PathBuf,
protocols: Vec<String>,
}
impl Listener {
pub async fn bind(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_path_buf();
match fs::symlink_metadata(&path) {
Ok(meta) if meta.file_type().is_socket() => fs::remove_file(&path)?,
Ok(_) => return Err(Error::NotASocket(path)),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
let listener = tokio::net::UnixListener::bind(&path)?;
Ok(Self {
listener,
path,
protocols: Vec::new(),
})
}
pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.protocols = protocols.into_iter().map(Into::into).collect();
self
}
pub fn set_mode(&self, mode: u32) -> Result<()> {
fs::set_permissions(&self.path, fs::Permissions::from_mode(mode))?;
Ok(())
}
pub fn path(&self) -> &Path {
&self.path
}
pub async fn accept(&self) -> Option<Result<(qmux::Session, PeerCred)>> {
match self.listener.accept().await {
Ok((stream, _addr)) => {
let cred = match stream.peer_cred() {
Ok(cred) => PeerCred {
uid: cred.uid(),
gid: cred.gid(),
pid: cred.pid(),
},
Err(err) => return Some(Err(err.into())),
};
let session = qmux::uds::Config::new(WIRE_VERSION)
.protocols(self.protocols.iter().map(String::as_str))
.accept(stream)
.await
.map_err(Error::Accept);
Some(session.map(|session| (session, cred)))
}
Err(err) => Some(Err(err.into())),
}
}
}
impl Drop for Listener {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}