use anyhow::Result;
#[cfg(windows)]
use std::time::{Duration, Instant};
#[cfg(windows)]
mod security;
#[cfg(unix)]
pub type Stream = tokio::net::UnixStream;
#[cfg(windows)]
pub type Stream = tokio::net::windows::named_pipe::NamedPipeServer;
pub fn remove_stale_endpoint(endpoint: &str) -> Result<()> {
#[cfg(unix)]
{
if std::path::Path::new(endpoint).exists() {
std::fs::remove_file(endpoint)?;
}
}
#[cfg(windows)]
{
let _ = endpoint;
}
Ok(())
}
pub struct Listener {
#[cfg(unix)]
inner: tokio::net::UnixListener,
#[cfg(windows)]
name: std::ffi::OsString,
#[cfg(windows)]
idle: Option<tokio::net::windows::named_pipe::NamedPipeServer>,
#[cfg(windows)]
security: security::CurrentUserOnly,
}
#[cfg(unix)]
impl Listener {
pub fn bind(endpoint: &str) -> Result<Self> {
Ok(Self {
inner: tokio::net::UnixListener::bind(endpoint)?,
})
}
pub async fn accept(&mut self) -> Result<Stream> {
let (stream, _) = self.inner.accept().await?;
Ok(stream)
}
}
#[cfg(unix)]
impl From<tokio::net::UnixListener> for Listener {
fn from(inner: tokio::net::UnixListener) -> Self {
Self { inner }
}
}
#[cfg(windows)]
impl Listener {
pub fn bind(endpoint: &str) -> Result<Self> {
const REBIND_BUDGET: Duration = Duration::from_millis(1_000);
const REBIND_INTERVAL: Duration = Duration::from_millis(25);
const ERROR_ACCESS_DENIED: i32 = 5;
let mut security = security::CurrentUserOnly::new()?;
let deadline = Instant::now() + REBIND_BUDGET;
loop {
let created = unsafe {
tokio::net::windows::named_pipe::ServerOptions::new()
.first_pipe_instance(true)
.create_with_security_attributes_raw(endpoint, security.as_raw())
};
match created {
Ok(idle) => {
return Ok(Self {
name: endpoint.into(),
idle: Some(idle),
security,
});
}
Err(error)
if error.raw_os_error() == Some(ERROR_ACCESS_DENIED)
&& Instant::now() < deadline => {}
Err(error) => return Err(error.into()),
}
std::thread::sleep(REBIND_INTERVAL);
}
}
fn instance(&mut self) -> Result<tokio::net::windows::named_pipe::NamedPipeServer> {
Ok(unsafe {
tokio::net::windows::named_pipe::ServerOptions::new()
.create_with_security_attributes_raw(&self.name, self.security.as_raw())
}?)
}
pub async fn accept(&mut self) -> Result<Stream> {
let server = match self.idle.take() {
Some(server) => server,
None => self.instance()?,
};
if let Err(error) = server.connect().await {
self.idle = Some(server);
return Err(error.into());
}
match self.instance() {
Ok(idle) => self.idle = Some(idle),
Err(error) => {
eprintln!("[ghosttea] failed to republish {:?}: {error:#}", self.name);
}
}
Ok(server)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
struct Endpoint {
name: String,
#[cfg(unix)]
#[allow(dead_code)]
directory: tempfile::TempDir,
}
fn unique_endpoint(label: &str) -> Endpoint {
#[cfg(windows)]
{
let id = uuid::Uuid::new_v4();
Endpoint {
name: format!(r"\\.\pipe\ghosttea-test-{label}-{id}"),
}
}
#[cfg(unix)]
{
let directory = tempfile::tempdir().unwrap();
let name = directory
.path()
.join(format!("{label}.sock"))
.to_string_lossy()
.into_owned();
Endpoint { name, directory }
}
}
#[cfg(unix)]
async fn dial(endpoint: &str) -> tokio::net::UnixStream {
tokio::net::UnixStream::connect(endpoint).await.unwrap()
}
#[cfg(windows)]
async fn dial(endpoint: &str) -> tokio::net::windows::named_pipe::NamedPipeClient {
const ERROR_PIPE_BUSY: i32 = 231;
const ERROR_FILE_NOT_FOUND: i32 = 2;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
match tokio::net::windows::named_pipe::ClientOptions::new().open(endpoint) {
Ok(client) => return client,
Err(error)
if matches!(
error.raw_os_error(),
Some(ERROR_PIPE_BUSY) | Some(ERROR_FILE_NOT_FOUND)
) && std::time::Instant::now() < deadline => {}
Err(error) => panic!("failed to dial {endpoint}: {error}"),
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
async fn echo_once(listener: &mut Listener) -> u8 {
let mut stream = listener.accept().await.unwrap();
let byte = stream.read_u8().await.unwrap();
stream.write_u8(byte).await.unwrap();
stream.flush().await.unwrap();
byte
}
#[tokio::test]
async fn round_trips_one_client() {
let endpoint = unique_endpoint("round-trip");
let mut listener = Listener::bind(&endpoint.name).unwrap();
let client = tokio::spawn({
let name = endpoint.name.clone();
async move {
let mut stream = dial(&name).await;
stream.write_u8(7).await.unwrap();
stream.flush().await.unwrap();
stream.read_u8().await.unwrap()
}
});
assert_eq!(echo_once(&mut listener).await, 7);
assert_eq!(client.await.unwrap(), 7);
}
#[tokio::test]
async fn serves_clients_one_after_another() {
let endpoint = unique_endpoint("sequential");
let mut listener = Listener::bind(&endpoint.name).unwrap();
for expected in 1..=4_u8 {
let client = tokio::spawn({
let name = endpoint.name.clone();
async move {
let mut stream = dial(&name).await;
stream.write_u8(expected).await.unwrap();
stream.flush().await.unwrap();
stream.read_u8().await.unwrap()
}
});
assert_eq!(echo_once(&mut listener).await, expected);
assert_eq!(client.await.unwrap(), expected);
}
}
#[tokio::test]
async fn serves_clients_that_arrive_together() {
let endpoint = unique_endpoint("concurrent");
let mut listener = Listener::bind(&endpoint.name).unwrap();
let server = tokio::spawn(async move {
let mut seen = Vec::new();
for _ in 0..3 {
seen.push(echo_once(&mut listener).await);
}
seen.sort_unstable();
seen
});
let mut clients = Vec::new();
for value in [10_u8, 20, 30] {
let name = endpoint.name.clone();
clients.push(tokio::spawn(async move {
let mut stream = dial(&name).await;
stream.write_u8(value).await.unwrap();
stream.flush().await.unwrap();
stream.read_u8().await.unwrap()
}));
}
let mut echoed = Vec::new();
for client in clients {
echoed.push(client.await.unwrap());
}
echoed.sort_unstable();
assert_eq!(echoed, vec![10, 20, 30]);
assert_eq!(server.await.unwrap(), vec![10, 20, 30]);
}
#[tokio::test]
async fn refuses_to_bind_a_live_endpoint_twice() {
let endpoint = unique_endpoint("exclusive");
let _listener = Listener::bind(&endpoint.name).unwrap();
assert!(Listener::bind(&endpoint.name).is_err());
}
#[cfg(windows)]
#[tokio::test]
async fn grants_pipe_access_to_the_owning_account_only() {
use std::os::windows::io::AsRawHandle;
let endpoint = unique_endpoint("dacl");
let mut listener = Listener::bind(&endpoint.name).unwrap();
let expected = security::CurrentUserOnly::new().unwrap().dacl().unwrap();
assert!(
expected.starts_with("D:P("),
"not a protected DACL: {expected}"
);
assert_eq!(
expected.matches("(A;").count(),
1,
"not one entry: {expected}"
);
let bound =
security::dacl_of(listener.idle.as_ref().unwrap().as_raw_handle() as isize).unwrap();
assert_eq!(bound, expected, "on bind");
let client = tokio::spawn({
let name = endpoint.name.clone();
async move { dial(&name).await }
});
let _accepted = listener.accept().await.unwrap();
let _client = client.await.unwrap();
let rotated =
security::dacl_of(listener.idle.as_ref().unwrap().as_raw_handle() as isize).unwrap();
assert_eq!(rotated, expected, "after rotation");
}
#[tokio::test]
async fn rebinds_an_endpoint_its_previous_owner_just_released() {
let endpoint = unique_endpoint("rebind");
let first = Listener::bind(&endpoint.name).unwrap();
let client = dial(&endpoint.name).await;
drop(first);
remove_stale_endpoint(&endpoint.name).unwrap();
let second = Listener::bind(&endpoint.name).expect("rebind after release");
drop(client);
drop(second);
}
#[cfg(windows)]
#[tokio::test]
async fn accepts_again_after_losing_its_idle_instance() {
let endpoint = unique_endpoint("recreate");
let mut listener = Listener::bind(&endpoint.name).unwrap();
listener.idle = None;
let client = tokio::spawn({
let name = endpoint.name.clone();
async move {
let mut stream = dial(&name).await;
stream.write_u8(9).await.unwrap();
stream.flush().await.unwrap();
stream.read_u8().await.unwrap()
}
});
assert_eq!(echo_once(&mut listener).await, 9);
assert_eq!(client.await.unwrap(), 9);
}
}