use crate::host_identity::{HostConnectError, HostExpectation, HostProcess};
use crate::suite::Suite;
use std::io::{self, Read, Write};
use std::sync::Arc;
pub enum Accepted {
Conn(Conn),
Rejected(String),
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(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)
}
#[cfg(test)]
pub fn accept_with(
&self,
verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
) -> io::Result<Accepted> {
let (s, _) = self.l.accept()?;
admit(s, verify)
}
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 {
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;
use std::os::windows::io::IntoRawHandle;
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)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique(tag: &str) -> String {
let n = SEQ.fetch_add(1, Ordering::SeqCst);
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
format!("{tag}-{}-{n}-{t:x}", std::process::id())
}
#[cfg(unix)]
fn endpoint(dir: &tempfile::TempDir) -> String {
dir.path().join("t.sock").to_string_lossy().into_owned()
}
#[cfg(windows)]
fn endpoint(_: &NoDir) -> String {
format!(r"\\.\pipe\rightkit-transport-test-{}", unique("p"))
}
#[cfg(unix)]
fn tmp() -> tempfile::TempDir {
tempfile::Builder::new().prefix("rk").tempdir().unwrap()
}
#[cfg(windows)]
struct NoDir;
#[cfg(windows)]
fn tmp() -> NoDir {
NoDir
}
#[test]
fn same_user_round_trip_authenticates_both_ends() {
let _ = unique("x");
let dir = tmp();
let ep = endpoint(&dir);
let l = Listener::bind(&ep, 4).unwrap();
let client_ep = ep.clone();
let t = std::thread::spawn(move || {
let mut c = connect(&client_ep).expect("same-user server must authenticate");
c.writer.write_all(b"ping\n").unwrap();
c.writer.flush().unwrap();
let mut buf = [0u8; 5];
c.reader.read_exact(&mut buf).unwrap();
buf
});
let Accepted::Conn(mut conn) = l.accept().unwrap() else {
panic!("same-user peer must be accepted");
};
let mut buf = [0u8; 5];
conn.reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"ping\n");
conn.writer.write_all(b"pong\n").unwrap();
conn.writer.flush().unwrap();
assert_eq!(&t.join().unwrap(), b"pong\n");
l.cleanup();
}
#[cfg(unix)]
#[test]
fn rejected_peer_is_closed_unread() {
use crate::peer::{PeerError, PeerIdentity};
let dir = tmp();
let ep = endpoint(&dir);
let l = Listener::bind(&ep, 4).unwrap();
let client_ep = ep.clone();
let t = std::thread::spawn(move || {
let mut s = std::os::unix::net::UnixStream::connect(&client_ep).unwrap();
let _ = s.write_all(b"{\"type\":\"hello\"}\n");
let mut b = [0u8; 1];
matches!(s.read(&mut b), Ok(0) | Err(_))
});
let foreign = PeerIdentity {
uid: Some(crate::peer::current_euid().wrapping_add(1)),
..Default::default()
};
match l
.accept_with(|_| Err(PeerError::Mismatch(foreign)))
.unwrap()
{
Accepted::Rejected(reason) => assert!(reason.contains("not the current user")),
_ => panic!("a foreign peer must be rejected"),
}
assert!(
t.join().unwrap(),
"rejected client must see a close, not a reply"
);
l.cleanup();
}
#[cfg(unix)]
#[test]
fn unsupported_verifier_never_admits() {
use crate::peer::PeerError;
let dir = tmp();
let ep = endpoint(&dir);
let l = Listener::bind(&ep, 4).unwrap();
let client_ep = ep.clone();
let t = std::thread::spawn(move || std::os::unix::net::UnixStream::connect(client_ep));
let a = l
.accept_with(|_| Err(PeerError::Unsupported("test")))
.unwrap();
assert!(matches!(a, Accepted::Rejected(_)));
let _ = t.join();
l.cleanup();
}
#[cfg(windows)]
#[test]
fn public_pipe_verifiers_accept_a_same_user_pipe() {
use crate::peer::{
owner_only_security_attributes, verify_pipe_peer_is_current_user,
verify_pipe_server_is_current_user,
};
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::{AsRawHandle, RawHandle};
use windows::core::PCWSTR;
use windows::Win32::Security::SECURITY_ATTRIBUTES;
use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX;
use windows::Win32::System::Pipes::{
ConnectNamedPipe, CreateNamedPipeW, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE,
PIPE_WAIT,
};
let name = format!(r"\\.\pipe\rightkit-verify-test-{}", unique("v"));
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let attrs = owner_only_security_attributes().unwrap();
let server = unsafe {
CreateNamedPipeW(
PCWSTR(wide.as_ptr()),
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
1,
4096,
4096,
0,
Some(attrs.as_ptr() as *const SECURITY_ATTRIBUTES),
)
};
assert!(!server.is_invalid());
let client_name = name.clone();
let t = std::thread::spawn(move || {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.security_qos_flags(0x0010_0000 | 0x0001_0000)
.open(client_name)
.unwrap()
});
unsafe { ConnectNamedPipe(server, None) }.ok();
let client = t.join().unwrap();
let id = verify_pipe_peer_is_current_user(server.0 as RawHandle).unwrap();
assert_eq!(id.pid, Some(std::process::id()));
assert_eq!(
id.sid,
Some(crate::peer::current_user_sid_string().unwrap())
);
let id = verify_pipe_server_is_current_user(client.as_raw_handle()).unwrap();
assert_eq!(id.pid, Some(std::process::id()));
let _ = unsafe { windows::Win32::Foundation::CloseHandle(server) };
}
fn long_service() -> String {
format!("{}-instance", "a".repeat(39))
}
#[test]
fn fallback_file_name_is_pinned_and_bounded() {
assert_eq!(
fallback_file_name("0badc0de", &long_service()),
"rk-0badc0de-3a43f919c4e992b6.sock"
);
assert_eq!(
fallback_file_name("0badc0de", "speech"),
"rk-0badc0de-c1d6288e168cb0d7.sock"
);
assert_eq!(fallback_file_name("0badc0de", &long_service()).len(), 33);
assert_eq!(fnv1a64(""), 0xcbf2_9ce4_8422_2325);
}
#[cfg(unix)]
fn long_root() -> tempfile::TempDir {
let d = tempfile::Builder::new()
.prefix(&"r".repeat(49))
.tempdir_in("/tmp")
.unwrap();
let n = d.path().as_os_str().len();
assert!((50..=69).contains(&n), "root is {n} bytes");
d
}
#[cfg(unix)]
#[test]
fn a_48_byte_service_name_binds_and_connects_under_a_long_temp_root() {
let suite = Suite::per_run_temp("ep-long").unwrap();
let service = long_service();
assert_eq!(service.len(), 48);
let root = long_root();
let ep = unix_endpoint_in(&suite, &service, root.path()).unwrap();
assert!(ep.starts_with(root.path().to_str().unwrap()));
assert!(
ep.len() <= MAX_UNIX_SOCKET_PATH,
"{ep} is {} bytes",
ep.len()
);
assert_eq!(legacy_unix_endpoint_in(&suite, &service, root.path()), None);
let l = Listener::bind(&ep, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
assert!(connect(&ep).is_ok());
l.cleanup();
assert_eq!(
candidates_in(&suite, &service, root.path()),
vec![ep.clone()]
);
}
#[cfg(unix)]
#[test]
fn a_new_client_still_finds_an_old_host_on_the_old_fallback_name() {
let suite = Suite::per_run_temp("ep-old").unwrap();
let service = "legacy-fallback-name";
let root = tempfile::Builder::new()
.prefix("rk")
.tempdir_in("/tmp")
.unwrap();
if suite
.service_dir(service)
.join(format!("{service}.sock"))
.as_os_str()
.len()
<= IN_SUITE_PATH_LIMIT
{
return;
}
let new = unix_endpoint_in(&suite, service, root.path()).unwrap();
let old = legacy_unix_endpoint_in(&suite, service, root.path()).expect("old name fitted");
assert_ne!(new, old);
assert_eq!(
candidates_in(&suite, service, root.path()),
vec![new.clone(), old.clone()]
);
let _old_host = std::os::unix::net::UnixListener::bind(&old).unwrap();
let live = candidates_in(&suite, service, root.path())
.into_iter()
.find(|e| probe_endpoint(e));
assert_eq!(live.as_deref(), Some(old.as_str()));
let l = Listener::bind(&new, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
let live = candidates_in(&suite, service, root.path())
.into_iter()
.find(|e| probe_endpoint(e));
assert_eq!(live.as_deref(), Some(new.as_str()));
l.cleanup();
}
#[cfg(unix)]
#[test]
fn an_unfittable_temp_root_is_a_typed_error_naming_path_length_and_limit() {
let suite = Suite::per_run_temp("ep-absurd").unwrap();
let service = long_service();
let temp = std::path::PathBuf::from(format!("/{}", "t".repeat(90)));
let e = unix_endpoint_in(&suite, &service, &temp).unwrap_err();
assert_eq!(e.limit, MAX_UNIX_SOCKET_PATH);
assert_eq!(e.len, e.path.len());
assert!(e.len > e.limit);
assert!(e.path.starts_with(temp.to_str().unwrap()));
let msg = e.to_string();
assert!(msg.contains(&e.path) && msg.contains(&e.len.to_string()) && msg.contains("103"));
assert!(legacy_unix_endpoint_in(&suite, &service, &temp).is_none());
assert!(candidates_in(&suite, &service, &temp).is_empty());
}
}