1use std::os::unix::fs::{FileTypeExt, PermissionsExt};
9use std::path::{Path, PathBuf};
10use std::{fs, io};
11
12use url::Url;
13
14const WIRE_VERSION: qmux::Version = qmux::Version::QMux01;
17
18#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum Error {
22 #[error(transparent)]
24 Io(#[from] io::Error),
25
26 #[error("missing socket path in unix:// URL")]
28 MissingPath,
29
30 #[error("qmux connect failed")]
32 Connect(#[source] qmux::Error),
33
34 #[error("qmux accept failed")]
36 Accept(#[source] qmux::Error),
37
38 #[error("refusing to replace existing non-socket file at {0}")]
40 NotASocket(PathBuf),
41}
42
43type Result<T> = std::result::Result<T, Error>;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct PeerCred {
51 pub uid: u32,
53 pub gid: u32,
55 pub pid: Option<i32>,
57}
58
59pub(crate) async fn connect(url: Url, protocols: &[&str]) -> Result<qmux::Session> {
65 let path = socket_path(&url).ok_or(Error::MissingPath)?;
66 tracing::debug!(%url, "connecting via Unix socket");
67 qmux::uds::Config::new(WIRE_VERSION)
68 .protocols(protocols.iter().copied())
69 .connect(path)
70 .await
71 .map_err(Error::Connect)
72}
73
74fn socket_path(url: &Url) -> Option<PathBuf> {
75 let path = url.path();
76 if path.is_empty() {
77 None
78 } else {
79 Some(PathBuf::from(path))
80 }
81}
82
83pub struct Listener {
89 listener: tokio::net::UnixListener,
90 path: PathBuf,
91 protocols: Vec<String>,
92}
93
94impl Listener {
95 pub async fn bind(path: impl AsRef<Path>) -> Result<Self> {
101 let path = path.as_ref().to_path_buf();
102
103 match fs::symlink_metadata(&path) {
107 Ok(meta) if meta.file_type().is_socket() => fs::remove_file(&path)?,
108 Ok(_) => return Err(Error::NotASocket(path)),
109 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
110 Err(err) => return Err(err.into()),
111 }
112
113 let listener = tokio::net::UnixListener::bind(&path)?;
114 Ok(Self {
115 listener,
116 path,
117 protocols: Vec::new(),
118 })
119 }
120
121 pub fn with_protocols<I, S>(mut self, protocols: I) -> Self
124 where
125 I: IntoIterator<Item = S>,
126 S: Into<String>,
127 {
128 self.protocols = protocols.into_iter().map(Into::into).collect();
129 self
130 }
131
132 pub fn set_mode(&self, mode: u32) -> Result<()> {
134 fs::set_permissions(&self.path, fs::Permissions::from_mode(mode))?;
135 Ok(())
136 }
137
138 pub fn path(&self) -> &Path {
140 &self.path
141 }
142
143 pub async fn accept(&self) -> Option<Result<(qmux::Session, PeerCred)>> {
148 match self.listener.accept().await {
149 Ok((stream, _addr)) => {
150 let cred = match stream.peer_cred() {
151 Ok(cred) => PeerCred {
152 uid: cred.uid(),
153 gid: cred.gid(),
154 pid: cred.pid(),
155 },
156 Err(err) => return Some(Err(err.into())),
157 };
158 let session = qmux::uds::Config::new(WIRE_VERSION)
159 .protocols(self.protocols.iter().map(String::as_str))
160 .accept(stream)
161 .await
162 .map_err(Error::Accept);
163 Some(session.map(|session| (session, cred)))
164 }
165 Err(err) => Some(Err(err.into())),
166 }
167 }
168}
169
170impl Drop for Listener {
171 fn drop(&mut self) {
172 let _ = fs::remove_file(&self.path);
174 }
175}