use std::path::Path;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::net::{UnixListener, UnixStream};
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(unix)]
use std::sync::Mutex;
#[cfg(unix)]
use anyhow::Context;
use anyhow::Result;
#[cfg(unix)]
static CWD_SWITCH: Mutex<()> = Mutex::new(());
#[cfg(unix)]
pub fn unix_socket_path_limit() -> usize {
let addr: libc::sockaddr_un = unsafe { std::mem::zeroed() };
addr.sun_path.len() - 1
}
#[cfg(unix)]
pub fn bind_unix_listener(path: &Path) -> Result<UnixListener> {
with_short_socket_name(path, |short| UnixListener::bind(short))
.with_context(|| format!("bind unix socket {}", path.display()))
}
#[cfg(unix)]
pub fn connect_unix_stream(path: &Path) -> Result<UnixStream> {
with_short_socket_name(path, |short| UnixStream::connect(short))
.with_context(|| format!("connect unix socket {}", path.display()))
}
#[cfg(unix)]
fn with_short_socket_name<T>(
path: &Path,
act: impl FnOnce(&Path) -> std::io::Result<T>,
) -> Result<T> {
if path.as_os_str().as_bytes().len() <= unix_socket_path_limit() {
return Ok(act(path)?);
}
let parent = path
.parent()
.with_context(|| format!("socket path has no parent directory: {}", path.display()))?;
let name = path
.file_name()
.with_context(|| format!("socket path has no file name: {}", path.display()))?;
let _serialized = CWD_SWITCH
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let _restore = WorkingDirectoryGuard::switch_to(parent)?;
Ok(act(Path::new(name))?)
}
#[cfg(unix)]
struct WorkingDirectoryGuard {
previous: PathBuf,
}
#[cfg(unix)]
impl WorkingDirectoryGuard {
fn switch_to(directory: &Path) -> Result<Self> {
let previous = std::env::current_dir().context("read current working directory")?;
std::env::set_current_dir(directory)
.with_context(|| format!("enter socket directory {}", directory.display()))?;
Ok(Self { previous })
}
}
#[cfg(unix)]
impl Drop for WorkingDirectoryGuard {
fn drop(&mut self) {
if let Err(error) = std::env::set_current_dir(&self.previous) {
tracing::error!(
directory = %self.previous.display(),
%error,
"failed to restore working directory after binding a unix socket"
);
}
}
}
#[cfg(not(unix))]
pub fn unix_socket_path_limit() -> usize {
0
}
#[cfg(not(unix))]
pub fn bind_unix_listener(_path: &Path) -> Result<std::convert::Infallible> {
anyhow::bail!("Unix domain sockets require Unix")
}
#[cfg(not(unix))]
pub fn connect_unix_stream(_path: &Path) -> Result<std::convert::Infallible> {
anyhow::bail!("Unix domain sockets require Unix")
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::io::{Read, Write};
fn deep_directory(base: &Path) -> PathBuf {
let mut directory = base.to_path_buf();
while directory.as_os_str().len() < 200 {
directory.push("nested-directory-component");
}
std::fs::create_dir_all(&directory).expect("create nested directories");
directory
}
#[test]
fn bind_and_connect_work_through_a_path_longer_than_sun_path() {
let temporary = tempfile::tempdir().expect("tempdir");
let socket = deep_directory(temporary.path()).join("control.sock");
assert!(socket.as_os_str().len() > unix_socket_path_limit());
let before = std::env::current_dir().expect("cwd");
let listener = bind_unix_listener(&socket).expect("bind long path");
assert_eq!(std::env::current_dir().expect("cwd"), before);
let accepting = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept");
let mut byte = [0u8; 1];
stream.read_exact(&mut byte).expect("read");
byte[0]
});
let mut client = connect_unix_stream(&socket).expect("connect long path");
assert_eq!(std::env::current_dir().expect("cwd"), before);
client.write_all(&[7]).expect("write");
drop(client);
assert_eq!(accepting.join().expect("join"), 7);
}
#[test]
fn a_short_path_binds_without_switching_directory() {
let temporary = tempfile::tempdir().expect("tempdir");
let socket = temporary.path().join("short.sock");
let before = std::env::current_dir().expect("cwd");
let listener = bind_unix_listener(&socket).expect("bind short path");
assert_eq!(std::env::current_dir().expect("cwd"), before);
drop(listener);
assert!(socket.exists());
}
#[test]
fn binding_into_a_missing_directory_names_the_full_path() {
let temporary = tempfile::tempdir().expect("tempdir");
let socket = temporary.path().join("absent").join("control.sock");
let error = bind_unix_listener(&socket).expect_err("missing directory");
assert!(
format!("{error:#}").contains(&socket.display().to_string()),
"error should name the full path: {error:#}"
);
}
}