use crate::host_identity::{HostConnectError, HostExpectation, HostProcess};
use crate::suite::Suite;
use std::io::{self, Read, Write};
#[cfg(windows)]
use std::os::windows::io::IntoRawHandle;
use std::sync::Arc;
pub enum Accepted {
Conn(Conn),
Rejected(String),
Unidentified(crate::peer::PeerRefusal),
Vanished,
}
pub const DEFAULT_MAX_PIPE_INSTANCES: u32 = 16;
pub struct Conn {
pub reader: Box<dyn Read + Send>,
pub writer: Box<dyn Write + Send>,
pub peer_closed: Arc<dyn Fn() -> bool + Send + Sync>,
pub shutdown: Arc<dyn Fn() + Send + Sync>,
pub peer: crate::peer::PeerIdentity,
}
fn fnv(s: &str) -> u32 {
let mut h: u32 = 0x811c_9dc5;
for b in s
.to_lowercase()
.replace('\\', "/")
.trim_end_matches('/')
.bytes()
{
h ^= u32::from(b);
h = h.wrapping_mul(0x0100_0193);
}
h
}
pub const MAX_UNIX_SOCKET_PATH: usize = 103;
const IN_SUITE_PATH_LIMIT: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EndpointError {
pub path: String,
pub len: usize,
pub limit: usize,
}
impl std::fmt::Display for EndpointError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"service socket path {:?} is {} bytes, over the {}-byte limit; set a shorter TMPDIR",
self.path, self.len, self.limit
)
}
}
impl std::error::Error for EndpointError {}
fn fnv1a64(s: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn fallback_file_name(tag: &str, service: &str) -> String {
format!("rk-{tag}-{:016x}.sock", fnv1a64(service))
}
fn legacy_fallback_file_name(tag: &str, service: &str) -> String {
format!("rk-{tag}-{service}.sock")
}
fn suite_tag(suite: &Suite) -> String {
format!("{:08x}", fnv(&suite.root().to_string_lossy()))
}
fn unix_endpoint_in(
suite: &Suite,
service: &str,
temp: &std::path::Path,
) -> Result<String, EndpointError> {
let p = suite.service_dir(service).join(format!("{service}.sock"));
if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
return Ok(p.to_string_lossy().into_owned());
}
let f = temp
.join(fallback_file_name(&suite_tag(suite), service))
.to_string_lossy()
.into_owned();
if f.len() <= MAX_UNIX_SOCKET_PATH {
Ok(f)
} else {
Err(EndpointError {
len: f.len(),
path: f,
limit: MAX_UNIX_SOCKET_PATH,
})
}
}
fn legacy_unix_endpoint_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Option<String> {
let p = suite.service_dir(service).join(format!("{service}.sock"));
if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
return None;
}
let f = temp
.join(legacy_fallback_file_name(&suite_tag(suite), service))
.to_string_lossy()
.into_owned();
(f.len() <= MAX_UNIX_SOCKET_PATH).then_some(f)
}
pub fn try_endpoint_for(suite: &Suite, service: &str) -> Result<String, EndpointError> {
if cfg!(windows) {
let tag = suite_tag(suite);
let user: String = std::env::var("USERNAME")
.unwrap_or_default()
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.take(20)
.collect();
Ok(format!(r"\\.\pipe\rightkit-{service}-{user}-{tag}"))
} else {
unix_endpoint_in(suite, service, &std::env::temp_dir())
}
}
pub fn endpoint_for(suite: &Suite, service: &str) -> String {
try_endpoint_for(suite, service).unwrap_or_else(|e| e.path)
}
pub fn endpoint_candidates(suite: &Suite, service: &str) -> Vec<String> {
candidates_in(suite, service, &std::env::temp_dir())
}
fn candidates_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Vec<String> {
let mut v = Vec::new();
if cfg!(windows) {
if let Ok(e) = try_endpoint_for(suite, service) {
v.push(e);
}
return v;
}
if let Ok(e) = unix_endpoint_in(suite, service, temp) {
v.push(e);
}
if let Some(old) = legacy_unix_endpoint_in(suite, service, temp) {
if !v.contains(&old) {
v.push(old);
}
}
v
}
pub fn find_live_endpoint(suite: &Suite, service: &str) -> Option<String> {
endpoint_candidates(suite, service)
.into_iter()
.find(|e| probe_endpoint(e))
}
pub fn probe_endpoint(endpoint: &str) -> bool {
connect(endpoint).is_ok()
}
#[cfg(unix)]
mod imp {
use super::*;
use std::os::fd::AsRawFd;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use crate::peer::{verify_peer_is_current_user, PeerError, PeerIdentity};
extern "C" {
fn recv(fd: i32, buf: *mut u8, len: usize, flags: i32) -> isize;
}
#[cfg(target_os = "macos")]
const PEEK_NOWAIT: i32 = 0x2 | 0x80;
#[cfg(not(target_os = "macos"))]
const PEEK_NOWAIT: i32 = 0x2 | 0x40;
pub fn current_uid() -> u32 {
crate::peer::current_euid()
}
fn admit(
s: UnixStream,
verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
) -> io::Result<Accepted> {
match verify(&s) {
Ok(peer) => wrap(s).map(|conn| Accepted::Conn(Conn { peer, ..conn })),
Err(e) if e.peer_gone() => Ok(Accepted::Vanished),
Err(e) => Ok(match e.refusal() {
Some(refusal) => Accepted::Unidentified(refusal),
None => Accepted::Rejected(e.to_string()),
}),
}
}
fn wrap(s: UnixStream) -> io::Result<Conn> {
let r = s.try_clone()?;
let w = s.try_clone()?;
let watch = s.try_clone()?;
let sd = s;
Ok(Conn {
peer: crate::peer::PeerIdentity::default(),
reader: Box::new(r),
writer: Box::new(w),
peer_closed: Arc::new(move || {
let mut b = [0u8; 1];
let n = unsafe { recv(watch.as_raw_fd(), b.as_mut_ptr(), 1, PEEK_NOWAIT) };
n == 0
|| (n < 0
&& !matches!(
io::Error::last_os_error().kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
))
}),
shutdown: Arc::new(move || {
let _ = sd.shutdown(std::net::Shutdown::Both);
}),
})
}
pub struct Listener {
l: UnixListener,
path: PathBuf,
}
impl Listener {
pub fn bind(endpoint: &str, _max_instances: u32) -> io::Result<Listener> {
let path = PathBuf::from(endpoint);
if let Ok(md) = std::fs::symlink_metadata(&path) {
if md.uid() != current_uid() || !md.file_type().is_socket_like() {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
"endpoint path is occupied by something that is not our socket",
));
}
if UnixStream::connect(&path).is_ok() {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
"a live host already serves this endpoint",
));
}
let _ = std::fs::remove_file(&path);
}
let l = UnixListener::bind(&path)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
Ok(Listener { l, path })
}
pub fn accept(&self) -> io::Result<Accepted> {
let (s, _) = self.l.accept()?;
admit(s, verify_peer_is_current_user)
}
pub fn wake(&self) {
let _ = UnixStream::connect(&self.path);
}
pub fn cleanup(&self) {
let _ = std::fs::remove_file(&self.path);
}
}
trait SocketLike {
fn is_socket_like(&self) -> bool;
}
impl SocketLike for std::fs::FileType {
fn is_socket_like(&self) -> bool {
use std::os::unix::fs::FileTypeExt;
self.is_socket()
}
}
pub fn connect(endpoint: &str) -> io::Result<Conn> {
let s = UnixStream::connect(endpoint)?;
verify_peer_is_current_user(&s).map_err(io::Error::from)?;
wrap(s)
}
pub fn connect_expecting(
endpoint: &str,
expect: &HostExpectation,
) -> Result<(Conn, HostProcess), HostConnectError> {
let s = UnixStream::connect(endpoint).map_err(HostConnectError::Io)?;
let host = crate::host_identity::verify_unix_host(&s, expect)?;
let conn = wrap(s).map_err(HostConnectError::Io)?;
Ok((conn, host))
}
}
#[cfg(windows)]
mod imp {
use super::*;
use crate::peer::{
owner_only_security_attributes, verify_pipe_peer_is_current_user,
verify_pipe_server_is_current_user, OwnerOnlySecurityAttributes,
};
use std::os::windows::io::RawHandle;
use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
use std::sync::{Mutex, RwLock};
use windows::core::PCWSTR;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Storage::FileSystem::{
ReadFile, WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED,
PIPE_ACCESS_DUPLEX,
};
use windows::Win32::System::Pipes::{
ConnectNamedPipe, CreateNamedPipeW, PeekNamedPipe, PIPE_READMODE_BYTE,
PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT,
};
use windows::Win32::System::Threading::CreateEventW;
use windows::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED};
pub fn current_uid() -> u32 {
0
}
struct SendHandle(HANDLE);
unsafe impl Send for SendHandle {}
pub struct Listener {
name: Vec<u16>,
attrs: OwnerOnlySecurityAttributes,
max_instances: u32,
first: AtomicBool,
endpoint: String,
pending: Mutex<Option<SendHandle>>,
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
const ERROR_BROKEN_PIPE: u32 = 109;
const ERROR_IO_PENDING: u32 = 997;
const ERROR_OPERATION_ABORTED: u32 = 995;
const ERROR_NO_DATA: u32 = 232;
const ERROR_PIPE_CONNECTED: u32 = 535;
const FILE_FLAG_OVERLAPPED_BITS: u32 = 0x4000_0000;
const SECURITY_SQOS_PRESENT_BITS: u32 = 0x0010_0000;
const SECURITY_IDENTIFICATION_BITS: u32 = 0x0001_0000;
fn code(e: &windows::core::Error) -> u32 {
(e.code().0 as u32) & 0xFFFF
}
struct PipeInner {
h: RwLock<Option<HANDLE>>,
raw: AtomicIsize,
cancelled: AtomicBool,
closing: Mutex<()>,
}
unsafe impl Send for PipeInner {}
unsafe impl Sync for PipeInner {}
impl Drop for PipeInner {
fn drop(&mut self) {
if let Some(h) = self.h.get_mut().unwrap_or_else(|e| e.into_inner()).take() {
unsafe {
let _ = CloseHandle(h);
}
}
}
}
#[derive(Clone)]
struct Pipe(Arc<PipeInner>);
impl Pipe {
fn new(h: HANDLE) -> Pipe {
Pipe(Arc::new(PipeInner {
h: RwLock::new(Some(h)),
raw: AtomicIsize::new(h.0 as isize),
cancelled: AtomicBool::new(false),
closing: Mutex::new(()),
}))
}
fn cancel(&self) {
let _one = self.0.closing.lock().unwrap_or_else(|e| e.into_inner());
self.0.cancelled.store(true, Ordering::SeqCst);
let raw = HANDLE(self.0.raw.load(Ordering::SeqCst) as *mut _);
if self.0.h.read().map(|g| g.is_some()).unwrap_or(false) {
unsafe {
let _ = CancelIoEx(raw, None);
}
}
let mut g = self.0.h.write().unwrap_or_else(|e| e.into_inner()); if let Some(h) = g.take() {
unsafe {
let _ = CloseHandle(h);
}
}
}
fn run(
&self,
start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> windows::core::Result<()>,
) -> io::Result<usize> {
if self.0.cancelled.load(Ordering::SeqCst) {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"connection closed",
));
}
let guard = self.0.h.read().unwrap_or_else(|e| e.into_inner());
let Some(handle) = *guard else {
return Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"connection closed",
));
};
unsafe {
let ev =
CreateEventW(None, true, false, PCWSTR::null()).map_err(io::Error::other)?;
let mut ov = OVERLAPPED {
hEvent: ev,
..Default::default()
};
let started = start(handle, &mut ov);
if self.0.cancelled.load(Ordering::SeqCst) {
let _ = CancelIoEx(handle, Some(&ov));
}
let outcome = match started {
Ok(()) => Ok(true),
Err(e) if code(&e) == ERROR_IO_PENDING => Ok(true),
Err(e) if code(&e) == ERROR_PIPE_CONNECTED => Ok(false),
Err(e) => Err(e),
};
let res = match outcome {
Ok(false) => Ok(0),
Ok(true) => {
let mut n = 0u32;
match GetOverlappedResult(handle, &ov, &mut n, true) {
Ok(()) => Ok(n as usize),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
};
let _ = CloseHandle(ev);
match res {
Ok(n) => Ok(n),
Err(e) => match code(&e) {
ERROR_BROKEN_PIPE | ERROR_NO_DATA => {
Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed"))
}
ERROR_OPERATION_ABORTED => Err(io::Error::new(
io::ErrorKind::ConnectionAborted,
"operation cancelled",
)),
_ => Err(io::Error::other(e)),
},
}
}
}
}
struct Half(Pipe);
impl Read for Half {
fn read(&mut self, b: &mut [u8]) -> io::Result<usize> {
match self
.0
.run(|h, ov| unsafe { ReadFile(h, Some(b), None, Some(ov)) })
{
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(0), r => r,
}
}
}
impl Write for Half {
fn write(&mut self, b: &[u8]) -> io::Result<usize> {
self.0
.run(|h, ov| unsafe { WriteFile(h, Some(b), None, Some(ov)) })
}
fn flush(&mut self) -> io::Result<()> {
Ok(()) }
}
fn wrap(pipe: Pipe) -> Conn {
let (w, watch, sd) = (pipe.clone(), pipe.clone(), pipe.clone());
Conn {
peer: crate::peer::PeerIdentity::default(),
reader: Box::new(Half(pipe)),
writer: Box::new(Half(w)),
peer_closed: Arc::new(move || {
if watch.0.cancelled.load(Ordering::SeqCst) {
return true;
}
let g = watch.0.h.read().unwrap_or_else(|e| e.into_inner());
match *g {
Some(h) => unsafe { PeekNamedPipe(h, None, 0, None, None, None).is_err() },
None => true,
}
}),
shutdown: Arc::new(move || sd.cancel()),
}
}
impl Listener {
pub fn bind(endpoint: &str, max_instances: u32) -> io::Result<Listener> {
let attrs = owner_only_security_attributes()?;
let l = Listener {
name: wide(endpoint),
attrs,
max_instances: max_instances.clamp(1, 254),
first: AtomicBool::new(true),
endpoint: endpoint.to_string(),
pending: Mutex::new(None),
};
let h = l.instance().map_err(|e| {
if e.raw_os_error() == Some(5) {
io::Error::new(
io::ErrorKind::AddrInUse,
format!("pipe {endpoint} still held: {e}"),
)
} else {
e
}
})?;
*l.pending.lock().unwrap_or_else(|e| e.into_inner()) = Some(SendHandle(h));
Ok(l)
}
fn instance(&self) -> io::Result<HANDLE> {
let mut open = PIPE_ACCESS_DUPLEX;
if self.first.swap(false, Ordering::SeqCst) {
open |= FILE_FLAG_FIRST_PIPE_INSTANCE;
}
let h = unsafe {
CreateNamedPipeW(
PCWSTR(self.name.as_ptr()),
open | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
self.max_instances,
64 * 1024,
64 * 1024,
0,
Some(self.attrs.raw()),
)
};
if h.is_invalid() {
return Err(io::Error::last_os_error());
}
Ok(h)
}
pub fn accept(&self) -> io::Result<Accepted> {
let pending = self
.pending
.lock()
.unwrap_or_else(|e| e.into_inner())
.take();
let h = match pending {
Some(p) => p.0,
None => self.instance()?,
};
let pipe = Pipe::new(h); let connected = pipe.run(|h, ov| unsafe { ConnectNamedPipe(h, Some(ov)) });
connected.map_err(|e| io::Error::new(e.kind(), format!("accept: {e}")))?;
let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
match verify_pipe_peer_is_current_user(raw) {
Ok(peer) => Ok(Accepted::Conn(Conn { peer, ..wrap(pipe) })),
Err(e) => {
pipe.cancel();
if e.peer_gone() {
Ok(Accepted::Vanished)
} else if let Some(refusal) = e.refusal() {
Ok(Accepted::Unidentified(refusal))
} else {
Ok(Accepted::Rejected(e.to_string()))
}
}
}
}
pub fn wake(&self) {
let _ = open_overlapped(&self.endpoint);
}
pub fn cleanup(&self) {}
}
pub fn connect(endpoint: &str) -> io::Result<Conn> {
let pipe = open_overlapped(endpoint)?;
let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
if let Err(e) = verify_pipe_server_is_current_user(raw) {
pipe.cancel();
return Err(e.into());
}
Ok(wrap(pipe))
}
pub fn connect_expecting(
endpoint: &str,
expect: &HostExpectation,
) -> Result<(Conn, HostProcess), HostConnectError> {
let pipe = open_overlapped(endpoint).map_err(HostConnectError::Io)?;
let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
if let Err(e) = verify_pipe_server_is_current_user(raw) {
pipe.cancel();
return Err(HostConnectError::Io(e.into()));
}
match crate::host_identity::verify_pipe_host(raw, expect) {
Ok(host) => Ok((wrap(pipe), host)),
Err(e) => {
pipe.cancel();
Err(e)
}
}
}
fn open_overlapped(endpoint: &str) -> io::Result<Pipe> {
use std::os::windows::fs::OpenOptionsExt;
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_PIPE_BUSY: i32 = 231;
let start = std::time::Instant::now();
loop {
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.custom_flags(FILE_FLAG_OVERLAPPED_BITS)
.security_qos_flags(SECURITY_SQOS_PRESENT_BITS | SECURITY_IDENTIFICATION_BITS)
.open(endpoint)
{
Ok(f) => return Ok(Pipe::new(HANDLE(f.into_raw_handle() as *mut _))),
Err(e) => {
let limit = match e.raw_os_error() {
Some(ERROR_PIPE_BUSY) => std::time::Duration::from_secs(3),
Some(ERROR_FILE_NOT_FOUND) => std::time::Duration::from_secs(1),
_ => return Err(e),
};
if start.elapsed() >= limit {
return Err(e);
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
}
}
}
pub use imp::Listener;
pub(crate) use imp::{connect, connect_expecting, current_uid};
#[cfg(unix)]
pub fn pid_alive(pid: u32) -> bool {
extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
if pid == 0 || pid > i32::MAX as u32 {
return false;
}
unsafe {
kill(pid as i32, 0) == 0 || io::Error::last_os_error().raw_os_error() == Some(1)
}
}
#[cfg(windows)]
pub fn pid_alive(pid: u32) -> bool {
use windows::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED};
use windows::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
const STILL_ACTIVE: u32 = 259;
if pid == 0 {
return false;
}
unsafe {
match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
Ok(h) => {
let mut code = 0u32;
let ok = GetExitCodeProcess(h, &mut code).is_ok();
let _ = CloseHandle(h);
ok && code == STILL_ACTIVE
}
Err(e) => e.code() == ERROR_ACCESS_DENIED.to_hresult(),
}
}
}
pub struct Stream {
pub reader: Box<dyn Read + Send>,
pub writer: Box<dyn Write + Send>,
shutdown: Arc<dyn Fn() + Send + Sync>,
}
impl Stream {
pub fn abort_handle(&self) -> Arc<dyn Fn() + Send + Sync> {
self.shutdown.clone()
}
}
pub fn open_stream(endpoint: &str) -> io::Result<Stream> {
let c = connect(endpoint)?;
Ok(Stream {
reader: c.reader,
writer: c.writer,
shutdown: c.shutdown,
})
}
pub fn open_stream_expecting(
endpoint: &str,
expect: &HostExpectation,
) -> Result<Stream, crate::ConnectError> {
match connect_expecting(endpoint, expect) {
Ok((c, _host)) => Ok(Stream {
reader: c.reader,
writer: c.writer,
shutdown: c.shutdown,
}),
Err(HostConnectError::Io(e)) => Err(crate::ConnectError::Service(
crate::ServiceError::retryable(
crate::ERROR_UNAVAILABLE,
format!("host unreachable: {e}"),
),
)),
Err(HostConnectError::Identity(e)) => Err(crate::ConnectError::HostIdentity(e)),
}
}