use std::io;
use std::path::PathBuf;
pub fn path() -> io::Result<PathBuf> {
#[cfg(windows)]
{
let root = crate::session::default_root()?;
let mut sum = crc32fast::Hasher::new();
sum.update(root.as_os_str().as_encoded_bytes());
Ok(PathBuf::from(format!(
r"\\.\pipe\slipcase-open.{}.{:08x}",
pipe::own_sid()?,
sum.finalize()
)))
}
#[cfg(not(windows))]
{
if let Some(dir) = runtime_dir() {
return Ok(dir.join("slipcase-open").join("front-door"));
}
let sessions = crate::session::default_root()?;
let base = sessions.parent().unwrap_or(&sessions).to_path_buf();
Ok(base.join("front-door"))
}
}
#[cfg(unix)]
fn runtime_dir() -> Option<PathBuf> {
std::env::var_os("XDG_RUNTIME_DIR")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
}
#[cfg(all(not(unix), not(windows)))]
fn runtime_dir() -> Option<PathBuf> {
None
}
pub fn prepare(at: &std::path::Path) -> io::Result<()> {
let dir = at.parent().unwrap_or(at);
std::fs::create_dir_all(dir)?;
private(dir)
}
#[cfg(unix)]
fn private(dir: &std::path::Path) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
}
#[allow(clippy::unnecessary_wraps)]
#[cfg(not(unix))]
fn private(_dir: &std::path::Path) -> io::Result<()> {
Ok(())
}
#[cfg(unix)]
pub use unix::{bind, connect, Incoming, Listener, Stream};
#[cfg(unix)]
mod unix {
use std::io;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;
pub type Stream = UnixStream;
#[derive(Debug)]
pub struct Listener {
inner: UnixListener,
path: std::path::PathBuf,
}
pub type Incoming<'a> = std::os::unix::net::Incoming<'a>;
impl Listener {
pub fn incoming(&self) -> Incoming<'_> {
self.inner.incoming()
}
}
impl Drop for Listener {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub fn connect(at: &Path) -> io::Result<Stream> {
UnixStream::connect(at)
}
pub fn bind(at: &Path) -> io::Result<Listener> {
super::prepare(at)?;
match UnixListener::bind(at) {
Ok(inner) => Ok(Listener {
inner,
path: at.to_owned(),
}),
Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
if UnixStream::connect(at).is_ok() {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
"another instance is listening",
));
}
std::fs::remove_file(at)?;
Ok(Listener {
inner: UnixListener::bind(at)?,
path: at.to_owned(),
})
}
Err(e) => Err(e),
}
}
}
#[cfg(windows)]
pub use pipe::{bind, connect, Incoming, Listener, Stream};
#[cfg(windows)]
mod pipe {
use std::cell::Cell;
use std::io;
use std::os::windows::ffi::OsStrExt as _;
use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle};
use std::path::Path;
use std::time::Duration;
use windows_sys::Win32::Foundation::{
LocalFree, ERROR_ACCESS_DENIED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
SDDL_REVISION_1,
};
use windows_sys::Win32::Security::{
GetTokenInformation, TokenUser, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
};
use windows_sys::Win32::Storage::FileSystem::{
FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_ACCESS_DUPLEX,
};
use windows_sys::Win32::System::Pipes::{
ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE,
PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
const BUFFER: u32 = 4096;
const BUSY_PAUSE: Duration = Duration::from_millis(20);
const BUSY_TRIES: u32 = 50;
pub type Stream = std::fs::File;
pub struct Listener {
name: Vec<u16>,
security: Vec<u16>,
pending: Cell<Option<OwnedHandle>>,
}
impl std::fmt::Debug for Listener {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Listener").finish_non_exhaustive()
}
}
pub struct Incoming<'a> {
listener: &'a Listener,
}
impl Listener {
#[must_use]
pub fn incoming(&self) -> Incoming<'_> {
Incoming { listener: self }
}
}
impl Iterator for Incoming<'_> {
type Item = io::Result<Stream>;
fn next(&mut self) -> Option<Self::Item> {
let pending = self.listener.pending.take()?;
if let Err(why) = wait_for_client(&pending) {
return Some(Err(why));
}
match instance(&self.listener.name, &self.listener.security, false) {
Ok(next) => self.listener.pending.set(Some(next)),
Err(why) => return Some(Err(why)),
}
Some(Ok(Stream::from(pending)))
}
}
pub fn connect(at: &Path) -> io::Result<Stream> {
let open = || std::fs::OpenOptions::new().read(true).write(true).open(at);
for _ in 0..BUSY_TRIES {
match open() {
Err(why) if is_error(&why, ERROR_PIPE_BUSY) => {
std::thread::sleep(BUSY_PAUSE);
}
settled => return settled,
}
}
open()
}
pub fn bind(at: &Path) -> io::Result<Listener> {
let name = wide(at.as_os_str());
let security = wide(std::ffi::OsStr::new(&descriptor()?));
let first = instance(&name, &security, true).map_err(|why| {
if is_error(&why, ERROR_ACCESS_DENIED) {
io::Error::new(io::ErrorKind::AddrInUse, "another instance is listening")
} else {
why
}
})?;
Ok(Listener {
name,
security,
pending: Cell::new(Some(first)),
})
}
fn is_error(why: &io::Error, code: u32) -> bool {
why.raw_os_error()
.and_then(|got| u32::try_from(got).ok())
.is_some_and(|got| got == code)
}
fn wide(s: &std::ffi::OsStr) -> Vec<u16> {
s.encode_wide().chain(std::iter::once(0)).collect()
}
fn descriptor() -> io::Result<String> {
Ok(format!("D:P(A;;GA;;;{})", own_sid()?))
}
#[allow(unsafe_code)]
pub(super) fn own_sid() -> io::Result<String> {
let mut token = std::ptr::null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let token = unsafe { OwnedHandle::from_raw_handle(token) };
let mut wanted = 0u32;
unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenUser,
std::ptr::null_mut(),
0,
&raw mut wanted,
);
}
let mut buffer = vec![0u64; (wanted as usize).div_ceil(8)];
let read = unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenUser,
buffer.as_mut_ptr().cast(),
wanted,
&raw mut wanted,
)
};
if read == 0 {
return Err(io::Error::last_os_error());
}
let mut text: *mut u16 = std::ptr::null_mut();
let made = unsafe {
ConvertSidToStringSidW(
(*buffer.as_ptr().cast::<TOKEN_USER>()).User.Sid,
&raw mut text,
)
};
if made == 0 {
return Err(io::Error::last_os_error());
}
let sid = unsafe {
let mut len = 0;
while *text.add(len) != 0 {
len += 1;
}
let sid = String::from_utf16_lossy(std::slice::from_raw_parts(text, len));
LocalFree(text.cast());
sid
};
Ok(sid)
}
#[allow(unsafe_code)]
fn instance(name: &[u16], security: &[u16], first: bool) -> io::Result<OwnedHandle> {
let mut sd = std::ptr::null_mut();
let made = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
security.as_ptr(),
SDDL_REVISION_1,
&raw mut sd,
std::ptr::null_mut(),
)
};
if made == 0 {
return Err(io::Error::last_os_error());
}
let attributes = SECURITY_ATTRIBUTES {
nLength: u32::try_from(std::mem::size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(0),
lpSecurityDescriptor: sd,
bInheritHandle: 0,
};
let mut access = PIPE_ACCESS_DUPLEX;
if first {
access |= FILE_FLAG_FIRST_PIPE_INSTANCE;
}
let handle = unsafe {
CreateNamedPipeW(
name.as_ptr(),
access,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFFER,
BUFFER,
0,
&raw const attributes,
)
};
let why = io::Error::last_os_error();
unsafe {
LocalFree(sd.cast());
}
if handle == INVALID_HANDLE_VALUE {
return Err(why);
}
Ok(unsafe { OwnedHandle::from_raw_handle(handle) })
}
#[allow(unsafe_code)]
fn wait_for_client(pending: &OwnedHandle) -> io::Result<()> {
let connected = unsafe { ConnectNamedPipe(pending.as_raw_handle(), std::ptr::null_mut()) };
if connected != 0 {
return Ok(());
}
let why = io::Error::last_os_error();
if is_error(&why, ERROR_PIPE_CONNECTED) {
return Ok(());
}
Err(why)
}
}
#[cfg(not(any(unix, windows)))]
pub fn connect(_at: &std::path::Path) -> io::Result<std::net::TcpStream> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"the front door is not implemented on this platform yet",
))
}
#[cfg(all(test, unix))]
mod tests {
use super::{bind, connect, path, prepare};
use crate::ipc::{answer, ask, take, Request, Response};
#[test]
fn the_endpoint_is_under_a_per_user_directory() {
let at = path().unwrap();
assert!(at.is_absolute());
assert_eq!(at.file_name().unwrap(), "front-door");
}
#[cfg(unix)]
#[test]
fn the_directory_is_owner_only_whatever_the_umask_says() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = tempfile::tempdir().unwrap();
let at = tmp.path().join("run/slipcase-open/front-door");
prepare(&at).unwrap();
let mode = std::fs::metadata(at.parent().unwrap())
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o700);
}
#[cfg(unix)]
#[test]
fn a_request_reaches_the_instance_and_the_answer_comes_back() {
let tmp = tempfile::tempdir().unwrap();
let at = tmp.path().join("front-door");
let listener = bind(&at).unwrap();
let serving = std::thread::spawn(move || {
let mut stream = listener.incoming().next().unwrap().unwrap();
let request = take(&mut stream).unwrap();
answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
});
let mut client = connect(&at).unwrap();
let got = ask(&mut client, &Request::Ping).unwrap();
serving.join().unwrap();
assert_eq!(got, Response::Ok(vec!["Ping".to_string()]));
}
#[cfg(unix)]
#[test]
fn nothing_listening_is_a_connection_that_fails_rather_than_a_hang() {
let tmp = tempfile::tempdir().unwrap();
assert!(connect(&tmp.path().join("front-door")).is_err());
}
#[cfg(unix)]
#[test]
fn a_socket_a_crash_left_behind_is_cleared_rather_than_blocking_forever() {
let tmp = tempfile::tempdir().unwrap();
let at = tmp.path().join("front-door");
drop(std::os::unix::net::UnixListener::bind(&at).unwrap());
assert!(at.exists(), "the debris should still be there");
assert!(connect(&at).is_err(), "nothing should be listening on it");
let listener = bind(&at);
assert!(listener.is_ok(), "{:?}", listener.err());
assert!(connect(&at).is_ok(), "the new instance should answer");
}
#[cfg(unix)]
#[test]
fn an_endpoint_somebody_is_serving_is_not_taken_from_them() {
let tmp = tempfile::tempdir().unwrap();
let at = tmp.path().join("front-door");
let _live = bind(&at).unwrap();
let second = bind(&at);
assert!(
second.is_err(),
"the endpoint was taken from a live instance"
);
assert!(connect(&at).is_ok());
}
#[cfg(unix)]
#[test]
fn dropping_the_listener_takes_the_socket_with_it() {
let tmp = tempfile::tempdir().unwrap();
let at = tmp.path().join("front-door");
{
let _listener = bind(&at).unwrap();
assert!(at.exists());
}
assert!(!at.exists());
}
}
#[cfg(all(test, windows))]
mod windows_tests {
use super::{bind, connect, path};
use crate::ipc::{answer, ask, take, Request, Response};
fn a_door(what: &str) -> std::path::PathBuf {
std::path::PathBuf::from(format!(
r"\\.\pipe\slipcase-open-test.{}.{}",
std::process::id(),
what
))
}
#[test]
fn the_endpoint_is_this_users_pipe() {
let at = path().unwrap();
let name = at.to_str().unwrap();
assert!(name.starts_with(r"\\.\pipe\slipcase-open."), "{name}");
assert!(name.contains("S-1-"), "{name}");
}
#[test]
fn a_request_reaches_the_instance_and_the_answer_comes_back() {
let at = a_door("round-trip");
let listener = bind(&at).unwrap();
let serving = std::thread::spawn(move || {
let mut stream = listener.incoming().next().unwrap().unwrap();
let request = take(&mut stream).unwrap();
answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
});
let mut client = connect(&at).unwrap();
let got = ask(&mut client, &Request::Ping).unwrap();
serving.join().unwrap();
assert_eq!(got, Response::Ok(vec!["Ping".to_string()]));
}
#[test]
fn the_door_stays_open_for_the_next_caller() {
let at = a_door("second-caller");
let listener = bind(&at).unwrap();
let serving = std::thread::spawn(move || {
for stream in listener.incoming().take(2) {
let mut stream = stream.unwrap();
let request = take(&mut stream).unwrap();
answer(&mut stream, &Response::Ok(vec![format!("{request:?}")])).unwrap();
}
});
for _ in 0..2 {
let mut client = connect(&at).unwrap();
assert_eq!(
ask(&mut client, &Request::Ping).unwrap(),
Response::Ok(vec!["Ping".to_string()])
);
}
serving.join().unwrap();
}
#[test]
fn nothing_listening_is_a_connection_that_fails_rather_than_a_hang() {
assert!(connect(&a_door("empty")).is_err());
}
#[test]
fn an_endpoint_somebody_is_serving_is_not_taken_from_them() {
let at = a_door("rival");
let _live = bind(&at).unwrap();
match bind(&at) {
Ok(_) => panic!("the endpoint was taken from a live instance"),
Err(why) => assert_eq!(why.kind(), std::io::ErrorKind::AddrInUse),
}
}
#[test]
fn a_pipe_leaves_nothing_behind_to_clear() {
let at = a_door("debris");
{
let _listener = bind(&at).unwrap();
}
assert!(connect(&at).is_err(), "the name outlived its listener");
assert!(bind(&at).is_ok());
}
}