1use crate::host_identity::{HostConnectError, HostExpectation, HostProcess};
6use crate::suite::Suite;
7use std::io::{self, Read, Write};
8use std::sync::Arc;
9
10pub enum Accepted {
13 Conn(Conn),
15 Rejected(String),
18 Vanished,
21}
22
23pub const DEFAULT_MAX_PIPE_INSTANCES: u32 = 16;
25
26pub struct Conn {
28 pub reader: Box<dyn Read + Send>,
29 pub writer: Box<dyn Write + Send>,
30 pub peer_closed: Arc<dyn Fn() -> bool + Send + Sync>,
32 pub shutdown: Arc<dyn Fn() + Send + Sync>,
33 pub peer: crate::peer::PeerIdentity,
36}
37
38fn fnv(s: &str) -> u32 {
39 let mut h: u32 = 0x811c_9dc5;
40 for b in s
41 .to_lowercase()
42 .replace('\\', "/")
43 .trim_end_matches('/')
44 .bytes()
45 {
46 h ^= u32::from(b);
47 h = h.wrapping_mul(0x0100_0193);
48 }
49 h
50}
51
52pub const MAX_UNIX_SOCKET_PATH: usize = 103;
55
56const IN_SUITE_PATH_LIMIT: usize = 100;
60
61#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct EndpointError {
65 pub path: String,
66 pub len: usize,
67 pub limit: usize,
68}
69
70impl std::fmt::Display for EndpointError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(
73 f,
74 "service socket path {:?} is {} bytes, over the {}-byte limit; set a shorter TMPDIR",
75 self.path, self.len, self.limit
76 )
77 }
78}
79impl std::error::Error for EndpointError {}
80
81fn fnv1a64(s: &str) -> u64 {
84 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
85 for b in s.bytes() {
86 h ^= u64::from(b);
87 h = h.wrapping_mul(0x0000_0100_0000_01b3);
88 }
89 h
90}
91
92fn fallback_file_name(tag: &str, service: &str) -> String {
95 format!("rk-{tag}-{:016x}.sock", fnv1a64(service))
96}
97
98fn legacy_fallback_file_name(tag: &str, service: &str) -> String {
100 format!("rk-{tag}-{service}.sock")
101}
102
103fn suite_tag(suite: &Suite) -> String {
104 format!("{:08x}", fnv(&suite.root().to_string_lossy()))
105}
106
107fn unix_endpoint_in(
110 suite: &Suite,
111 service: &str,
112 temp: &std::path::Path,
113) -> Result<String, EndpointError> {
114 let p = suite.service_dir(service).join(format!("{service}.sock"));
115 if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
116 return Ok(p.to_string_lossy().into_owned());
117 }
118 let f = temp
119 .join(fallback_file_name(&suite_tag(suite), service))
120 .to_string_lossy()
121 .into_owned();
122 if f.len() <= MAX_UNIX_SOCKET_PATH {
123 Ok(f)
124 } else {
125 Err(EndpointError {
126 len: f.len(),
127 path: f,
128 limit: MAX_UNIX_SOCKET_PATH,
129 })
130 }
131}
132
133fn legacy_unix_endpoint_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Option<String> {
136 let p = suite.service_dir(service).join(format!("{service}.sock"));
137 if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
138 return None;
139 }
140 let f = temp
141 .join(legacy_fallback_file_name(&suite_tag(suite), service))
142 .to_string_lossy()
143 .into_owned();
144 (f.len() <= MAX_UNIX_SOCKET_PATH).then_some(f)
145}
146
147pub fn try_endpoint_for(suite: &Suite, service: &str) -> Result<String, EndpointError> {
157 if cfg!(windows) {
158 let tag = suite_tag(suite);
159 let user: String = std::env::var("USERNAME")
165 .unwrap_or_default()
166 .chars()
167 .filter(|c| c.is_ascii_alphanumeric())
168 .take(20)
169 .collect();
170 Ok(format!(r"\\.\pipe\rightkit-{service}-{user}-{tag}"))
171 } else {
172 unix_endpoint_in(suite, service, &std::env::temp_dir())
173 }
174}
175
176pub fn endpoint_for(suite: &Suite, service: &str) -> String {
179 try_endpoint_for(suite, service).unwrap_or_else(|e| e.path)
180}
181
182pub fn endpoint_candidates(suite: &Suite, service: &str) -> Vec<String> {
188 candidates_in(suite, service, &std::env::temp_dir())
189}
190
191fn candidates_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Vec<String> {
192 let mut v = Vec::new();
193 if cfg!(windows) {
194 if let Ok(e) = try_endpoint_for(suite, service) {
195 v.push(e);
196 }
197 return v;
198 }
199 if let Ok(e) = unix_endpoint_in(suite, service, temp) {
200 v.push(e);
201 }
202 if let Some(old) = legacy_unix_endpoint_in(suite, service, temp) {
203 if !v.contains(&old) {
204 v.push(old);
205 }
206 }
207 v
208}
209
210pub fn find_live_endpoint(suite: &Suite, service: &str) -> Option<String> {
212 endpoint_candidates(suite, service)
213 .into_iter()
214 .find(|e| probe_endpoint(e))
215}
216
217pub fn probe_endpoint(endpoint: &str) -> bool {
219 connect(endpoint).is_ok()
220}
221
222#[cfg(unix)]
223mod imp {
224 use super::*;
225 use std::os::fd::AsRawFd;
226 use std::os::unix::fs::{MetadataExt, PermissionsExt};
227 use std::os::unix::net::{UnixListener, UnixStream};
228 use std::path::PathBuf;
229
230 use crate::peer::{verify_peer_is_current_user, PeerError, PeerIdentity};
231
232 extern "C" {
233 fn recv(fd: i32, buf: *mut u8, len: usize, flags: i32) -> isize;
234 }
235 #[cfg(target_os = "macos")]
237 const PEEK_NOWAIT: i32 = 0x2 | 0x80;
238 #[cfg(not(target_os = "macos"))]
239 const PEEK_NOWAIT: i32 = 0x2 | 0x40;
240
241 pub fn current_uid() -> u32 {
242 crate::peer::current_euid()
243 }
244
245 fn admit(
248 s: UnixStream,
249 verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
250 ) -> io::Result<Accepted> {
251 match verify(&s) {
252 Ok(peer) => wrap(s).map(|conn| Accepted::Conn(Conn { peer, ..conn })),
253 Err(e) if e.peer_gone() => Ok(Accepted::Vanished),
254 Err(e) => Ok(Accepted::Rejected(e.to_string())),
255 }
256 }
257
258 fn wrap(s: UnixStream) -> io::Result<Conn> {
259 let r = s.try_clone()?;
260 let w = s.try_clone()?;
261 let watch = s.try_clone()?;
262 let sd = s;
263 Ok(Conn {
264 peer: crate::peer::PeerIdentity::default(),
265 reader: Box::new(r),
266 writer: Box::new(w),
267 peer_closed: Arc::new(move || {
268 let mut b = [0u8; 1];
269 let n = unsafe { recv(watch.as_raw_fd(), b.as_mut_ptr(), 1, PEEK_NOWAIT) };
270 n == 0
271 || (n < 0
272 && !matches!(
273 io::Error::last_os_error().kind(),
274 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
275 ))
276 }),
277 shutdown: Arc::new(move || {
278 let _ = sd.shutdown(std::net::Shutdown::Both);
279 }),
280 })
281 }
282
283 pub struct Listener {
284 l: UnixListener,
285 path: PathBuf,
286 }
287
288 impl Listener {
289 pub fn bind(endpoint: &str, _max_instances: u32) -> io::Result<Listener> {
291 let path = PathBuf::from(endpoint);
292 if let Ok(md) = std::fs::symlink_metadata(&path) {
293 if md.uid() != current_uid() || !md.file_type().is_socket_like() {
295 return Err(io::Error::new(
296 io::ErrorKind::AddrInUse,
297 "endpoint path is occupied by something that is not our socket",
298 ));
299 }
300 if UnixStream::connect(&path).is_ok() {
301 return Err(io::Error::new(
302 io::ErrorKind::AddrInUse,
303 "a live host already serves this endpoint",
304 ));
305 }
306 let _ = std::fs::remove_file(&path);
307 }
308 let l = UnixListener::bind(&path)?;
309 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
310 Ok(Listener { l, path })
311 }
312 pub fn accept(&self) -> io::Result<Accepted> {
314 let (s, _) = self.l.accept()?;
315 admit(s, verify_peer_is_current_user)
316 }
317 #[cfg(test)]
318 pub fn accept_with(
319 &self,
320 verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
321 ) -> io::Result<Accepted> {
322 let (s, _) = self.l.accept()?;
323 admit(s, verify)
324 }
325 pub fn wake(&self) {
326 let _ = UnixStream::connect(&self.path);
327 }
328 pub fn cleanup(&self) {
329 let _ = std::fs::remove_file(&self.path);
330 }
331 }
332
333 trait SocketLike {
334 fn is_socket_like(&self) -> bool;
335 }
336 impl SocketLike for std::fs::FileType {
337 fn is_socket_like(&self) -> bool {
338 use std::os::unix::fs::FileTypeExt;
339 self.is_socket()
340 }
341 }
342
343 pub fn connect(endpoint: &str) -> io::Result<Conn> {
346 let s = UnixStream::connect(endpoint)?;
347 verify_peer_is_current_user(&s).map_err(io::Error::from)?;
348 wrap(s)
349 }
350
351 pub fn connect_expecting(
357 endpoint: &str,
358 expect: &HostExpectation,
359 ) -> Result<(Conn, HostProcess), HostConnectError> {
360 let s = UnixStream::connect(endpoint).map_err(HostConnectError::Io)?;
361 let host = crate::host_identity::verify_unix_host(&s, expect)?;
362 let conn = wrap(s).map_err(HostConnectError::Io)?;
363 Ok((conn, host))
364 }
365}
366
367#[cfg(windows)]
368mod imp {
369 use super::*;
370 use crate::peer::{
371 owner_only_security_attributes, verify_pipe_peer_is_current_user,
372 verify_pipe_server_is_current_user, OwnerOnlySecurityAttributes,
373 };
374 use std::os::windows::io::RawHandle;
375 use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
376 use std::sync::{Mutex, RwLock};
377 use windows::core::PCWSTR;
378 use windows::Win32::Foundation::{CloseHandle, HANDLE};
379 use windows::Win32::Storage::FileSystem::{
380 ReadFile, WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED,
381 PIPE_ACCESS_DUPLEX,
382 };
383 use windows::Win32::System::Pipes::{
384 ConnectNamedPipe, CreateNamedPipeW, PeekNamedPipe, PIPE_READMODE_BYTE,
385 PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT,
386 };
387 use windows::Win32::System::Threading::CreateEventW;
388 use windows::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED};
389
390 pub fn current_uid() -> u32 {
391 0
392 }
393
394 struct SendHandle(HANDLE);
395 unsafe impl Send for SendHandle {}
396
397 pub struct Listener {
398 name: Vec<u16>,
399 attrs: OwnerOnlySecurityAttributes,
400 max_instances: u32,
402 first: AtomicBool,
403 endpoint: String,
404 pending: Mutex<Option<SendHandle>>,
407 }
408
409 fn wide(s: &str) -> Vec<u16> {
410 s.encode_utf16().chain(std::iter::once(0)).collect()
411 }
412
413 const ERROR_BROKEN_PIPE: u32 = 109;
414 const ERROR_IO_PENDING: u32 = 997;
415 const ERROR_OPERATION_ABORTED: u32 = 995;
416 const ERROR_NO_DATA: u32 = 232;
417 const ERROR_PIPE_CONNECTED: u32 = 535;
418 const FILE_FLAG_OVERLAPPED_BITS: u32 = 0x4000_0000;
419 const SECURITY_SQOS_PRESENT_BITS: u32 = 0x0010_0000;
421 const SECURITY_IDENTIFICATION_BITS: u32 = 0x0001_0000;
422
423 fn code(e: &windows::core::Error) -> u32 {
424 (e.code().0 as u32) & 0xFFFF
425 }
426
427 struct PipeInner {
433 h: RwLock<Option<HANDLE>>,
437 raw: AtomicIsize,
438 cancelled: AtomicBool,
439 closing: Mutex<()>,
440 }
441 unsafe impl Send for PipeInner {}
442 unsafe impl Sync for PipeInner {}
443 impl Drop for PipeInner {
444 fn drop(&mut self) {
445 if let Some(h) = self.h.get_mut().unwrap_or_else(|e| e.into_inner()).take() {
446 unsafe {
447 let _ = CloseHandle(h);
448 }
449 }
450 }
451 }
452
453 #[derive(Clone)]
454 struct Pipe(Arc<PipeInner>);
455
456 impl Pipe {
457 fn new(h: HANDLE) -> Pipe {
458 Pipe(Arc::new(PipeInner {
459 h: RwLock::new(Some(h)),
460 raw: AtomicIsize::new(h.0 as isize),
461 cancelled: AtomicBool::new(false),
462 closing: Mutex::new(()),
463 }))
464 }
465
466 fn cancel(&self) {
469 let _one = self.0.closing.lock().unwrap_or_else(|e| e.into_inner());
470 self.0.cancelled.store(true, Ordering::SeqCst);
471 let raw = HANDLE(self.0.raw.load(Ordering::SeqCst) as *mut _);
472 if self.0.h.read().map(|g| g.is_some()).unwrap_or(false) {
473 unsafe {
475 let _ = CancelIoEx(raw, None);
476 }
477 }
478 let mut g = self.0.h.write().unwrap_or_else(|e| e.into_inner()); if let Some(h) = g.take() {
480 unsafe {
481 let _ = CloseHandle(h);
482 }
483 }
484 }
485
486 fn run(
489 &self,
490 start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> windows::core::Result<()>,
491 ) -> io::Result<usize> {
492 if self.0.cancelled.load(Ordering::SeqCst) {
493 return Err(io::Error::new(
494 io::ErrorKind::ConnectionAborted,
495 "connection closed",
496 ));
497 }
498 let guard = self.0.h.read().unwrap_or_else(|e| e.into_inner());
499 let Some(handle) = *guard else {
500 return Err(io::Error::new(
501 io::ErrorKind::ConnectionAborted,
502 "connection closed",
503 ));
504 };
505 unsafe {
506 let ev =
507 CreateEventW(None, true, false, PCWSTR::null()).map_err(io::Error::other)?;
508 let mut ov = OVERLAPPED {
509 hEvent: ev,
510 ..Default::default()
511 };
512 let started = start(handle, &mut ov);
513 if self.0.cancelled.load(Ordering::SeqCst) {
516 let _ = CancelIoEx(handle, Some(&ov));
517 }
518 let outcome = match started {
519 Ok(()) => Ok(true),
520 Err(e) if code(&e) == ERROR_IO_PENDING => Ok(true),
521 Err(e) if code(&e) == ERROR_PIPE_CONNECTED => Ok(false),
523 Err(e) => Err(e),
524 };
525 let res = match outcome {
526 Ok(false) => Ok(0),
527 Ok(true) => {
528 let mut n = 0u32;
529 match GetOverlappedResult(handle, &ov, &mut n, true) {
531 Ok(()) => Ok(n as usize),
532 Err(e) => Err(e),
533 }
534 }
535 Err(e) => Err(e),
536 };
537 let _ = CloseHandle(ev);
538 match res {
539 Ok(n) => Ok(n),
540 Err(e) => match code(&e) {
541 ERROR_BROKEN_PIPE | ERROR_NO_DATA => {
542 Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed"))
543 }
544 ERROR_OPERATION_ABORTED => Err(io::Error::new(
545 io::ErrorKind::ConnectionAborted,
546 "operation cancelled",
547 )),
548 _ => Err(io::Error::other(e)),
549 },
550 }
551 }
552 }
553 }
554
555 struct Half(Pipe);
556 impl Read for Half {
557 fn read(&mut self, b: &mut [u8]) -> io::Result<usize> {
558 match self
559 .0
560 .run(|h, ov| unsafe { ReadFile(h, Some(b), None, Some(ov)) })
561 {
562 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(0), r => r,
564 }
565 }
566 }
567 impl Write for Half {
568 fn write(&mut self, b: &[u8]) -> io::Result<usize> {
569 self.0
570 .run(|h, ov| unsafe { WriteFile(h, Some(b), None, Some(ov)) })
571 }
572 fn flush(&mut self) -> io::Result<()> {
573 Ok(()) }
575 }
576
577 fn wrap(pipe: Pipe) -> Conn {
578 let (w, watch, sd) = (pipe.clone(), pipe.clone(), pipe.clone());
579 Conn {
580 peer: crate::peer::PeerIdentity::default(),
581 reader: Box::new(Half(pipe)),
582 writer: Box::new(Half(w)),
583 peer_closed: Arc::new(move || {
584 if watch.0.cancelled.load(Ordering::SeqCst) {
585 return true;
586 }
587 let g = watch.0.h.read().unwrap_or_else(|e| e.into_inner());
589 match *g {
590 Some(h) => unsafe { PeekNamedPipe(h, None, 0, None, None, None).is_err() },
591 None => true,
592 }
593 }),
594 shutdown: Arc::new(move || sd.cancel()),
597 }
598 }
599
600 impl Listener {
601 pub fn bind(endpoint: &str, max_instances: u32) -> io::Result<Listener> {
604 let attrs = owner_only_security_attributes()?;
605 let l = Listener {
606 name: wide(endpoint),
607 attrs,
608 max_instances: max_instances.clamp(1, 254),
609 first: AtomicBool::new(true),
610 endpoint: endpoint.to_string(),
611 pending: Mutex::new(None),
612 };
613 let h = l.instance().map_err(|e| {
617 if e.raw_os_error() == Some(5) {
618 io::Error::new(
619 io::ErrorKind::AddrInUse,
620 format!("pipe {endpoint} still held: {e}"),
621 )
622 } else {
623 e
624 }
625 })?;
626 *l.pending.lock().unwrap_or_else(|e| e.into_inner()) = Some(SendHandle(h));
627 Ok(l)
628 }
629
630 fn instance(&self) -> io::Result<HANDLE> {
631 let mut open = PIPE_ACCESS_DUPLEX;
632 if self.first.swap(false, Ordering::SeqCst) {
633 open |= FILE_FLAG_FIRST_PIPE_INSTANCE;
634 }
635 let h = unsafe {
636 CreateNamedPipeW(
637 PCWSTR(self.name.as_ptr()),
638 open | FILE_FLAG_OVERLAPPED,
639 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
640 self.max_instances,
641 64 * 1024,
642 64 * 1024,
643 0,
644 Some(self.attrs.raw()),
645 )
646 };
647 if h.is_invalid() {
648 return Err(io::Error::last_os_error());
649 }
650 Ok(h)
651 }
652
653 pub fn accept(&self) -> io::Result<Accepted> {
654 let pending = self
655 .pending
656 .lock()
657 .unwrap_or_else(|e| e.into_inner())
658 .take();
659 let h = match pending {
660 Some(p) => p.0,
661 None => self.instance()?,
662 };
663 let pipe = Pipe::new(h); let connected = pipe.run(|h, ov| unsafe { ConnectNamedPipe(h, Some(ov)) });
665 connected.map_err(|e| io::Error::new(e.kind(), format!("accept: {e}")))?;
666 let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
669 match verify_pipe_peer_is_current_user(raw) {
670 Ok(peer) => Ok(Accepted::Conn(Conn { peer, ..wrap(pipe) })),
671 Err(e) => {
672 pipe.cancel();
673 if e.peer_gone() {
674 Ok(Accepted::Vanished)
675 } else {
676 Ok(Accepted::Rejected(e.to_string()))
677 }
678 }
679 }
680 }
681 pub fn wake(&self) {
682 let _ = open_overlapped(&self.endpoint);
683 }
684 pub fn cleanup(&self) {}
685 }
686
687 pub fn connect(endpoint: &str) -> io::Result<Conn> {
691 let pipe = open_overlapped(endpoint)?;
692 let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
693 if let Err(e) = verify_pipe_server_is_current_user(raw) {
694 pipe.cancel();
695 return Err(e.into());
696 }
697 Ok(wrap(pipe))
698 }
699
700 pub fn connect_expecting(
704 endpoint: &str,
705 expect: &HostExpectation,
706 ) -> Result<(Conn, HostProcess), HostConnectError> {
707 let pipe = open_overlapped(endpoint).map_err(HostConnectError::Io)?;
708 let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
709 if let Err(e) = verify_pipe_server_is_current_user(raw) {
710 pipe.cancel();
711 return Err(HostConnectError::Io(e.into()));
712 }
713 match crate::host_identity::verify_pipe_host(raw, expect) {
714 Ok(host) => Ok((wrap(pipe), host)),
715 Err(e) => {
716 pipe.cancel();
717 Err(e)
718 }
719 }
720 }
721
722 fn open_overlapped(endpoint: &str) -> io::Result<Pipe> {
723 use std::os::windows::fs::OpenOptionsExt;
724 use std::os::windows::io::IntoRawHandle;
725 const ERROR_FILE_NOT_FOUND: i32 = 2;
726 const ERROR_PIPE_BUSY: i32 = 231;
727 let start = std::time::Instant::now();
728 loop {
729 match std::fs::OpenOptions::new()
730 .read(true)
731 .write(true)
732 .custom_flags(FILE_FLAG_OVERLAPPED_BITS)
733 .security_qos_flags(SECURITY_SQOS_PRESENT_BITS | SECURITY_IDENTIFICATION_BITS)
734 .open(endpoint)
735 {
736 Ok(f) => return Ok(Pipe::new(HANDLE(f.into_raw_handle() as *mut _))),
737 Err(e) => {
738 let limit = match e.raw_os_error() {
739 Some(ERROR_PIPE_BUSY) => std::time::Duration::from_secs(3),
740 Some(ERROR_FILE_NOT_FOUND) => std::time::Duration::from_secs(1),
741 _ => return Err(e),
742 };
743 if start.elapsed() >= limit {
744 return Err(e);
745 }
746 std::thread::sleep(std::time::Duration::from_millis(25));
747 }
748 }
749 }
750 }
751}
752
753pub use imp::Listener;
754pub(crate) use imp::{connect, connect_expecting, current_uid};
755
756#[cfg(unix)]
761pub fn pid_alive(pid: u32) -> bool {
762 extern "C" {
763 fn kill(pid: i32, sig: i32) -> i32;
764 }
765 if pid == 0 || pid > i32::MAX as u32 {
766 return false;
767 }
768 unsafe {
769 kill(pid as i32, 0) == 0 || io::Error::last_os_error().raw_os_error() == Some(1)
770 }
772}
773#[cfg(windows)]
774pub fn pid_alive(pid: u32) -> bool {
775 use windows::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED};
776 use windows::Win32::System::Threading::{
777 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
778 };
779 const STILL_ACTIVE: u32 = 259;
780 if pid == 0 {
781 return false;
782 }
783 unsafe {
784 match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
785 Ok(h) => {
786 let mut code = 0u32;
787 let ok = GetExitCodeProcess(h, &mut code).is_ok();
788 let _ = CloseHandle(h);
789 ok && code == STILL_ACTIVE
790 }
791 Err(e) => e.code() == ERROR_ACCESS_DENIED.to_hresult(),
792 }
793 }
794}
795
796pub struct Stream {
801 pub reader: Box<dyn Read + Send>,
802 pub writer: Box<dyn Write + Send>,
803 shutdown: Arc<dyn Fn() + Send + Sync>,
804}
805
806impl Stream {
807 pub fn abort_handle(&self) -> Arc<dyn Fn() + Send + Sync> {
810 self.shutdown.clone()
811 }
812}
813
814pub fn open_stream(endpoint: &str) -> io::Result<Stream> {
821 let c = connect(endpoint)?;
822 Ok(Stream {
823 reader: c.reader,
824 writer: c.writer,
825 shutdown: c.shutdown,
826 })
827}
828
829pub fn open_stream_expecting(
836 endpoint: &str,
837 expect: &HostExpectation,
838) -> Result<Stream, crate::ConnectError> {
839 match connect_expecting(endpoint, expect) {
840 Ok((c, _host)) => Ok(Stream {
841 reader: c.reader,
842 writer: c.writer,
843 shutdown: c.shutdown,
844 }),
845 Err(HostConnectError::Io(e)) => Err(crate::ConnectError::Service(
846 crate::ServiceError::retryable(
847 crate::ERROR_UNAVAILABLE,
848 format!("host unreachable: {e}"),
849 ),
850 )),
851 Err(HostConnectError::Identity(e)) => Err(crate::ConnectError::HostIdentity(e)),
852 }
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use std::sync::atomic::{AtomicU32, Ordering};
859
860 static SEQ: AtomicU32 = AtomicU32::new(0);
861
862 fn unique(tag: &str) -> String {
863 let n = SEQ.fetch_add(1, Ordering::SeqCst);
864 let t = std::time::SystemTime::now()
865 .duration_since(std::time::UNIX_EPOCH)
866 .map(|d| d.subsec_nanos())
867 .unwrap_or(0);
868 format!("{tag}-{}-{n}-{t:x}", std::process::id())
869 }
870
871 #[cfg(unix)]
872 fn endpoint(dir: &tempfile::TempDir) -> String {
873 dir.path().join("t.sock").to_string_lossy().into_owned()
874 }
875 #[cfg(windows)]
876 fn endpoint(_: &NoDir) -> String {
877 format!(r"\\.\pipe\rightkit-transport-test-{}", unique("p"))
878 }
879 #[cfg(unix)]
880 fn tmp() -> tempfile::TempDir {
881 tempfile::Builder::new().prefix("rk").tempdir().unwrap()
882 }
883 #[cfg(windows)]
885 struct NoDir;
886 #[cfg(windows)]
887 fn tmp() -> NoDir {
888 NoDir
889 }
890
891 #[test]
894 fn same_user_round_trip_authenticates_both_ends() {
895 let _ = unique("x");
896 let dir = tmp();
897 let ep = endpoint(&dir);
898 let l = Listener::bind(&ep, 4).unwrap();
899 let client_ep = ep.clone();
900 let t = std::thread::spawn(move || {
901 let mut c = connect(&client_ep).expect("same-user server must authenticate");
902 c.writer.write_all(b"ping\n").unwrap();
903 c.writer.flush().unwrap();
904 let mut buf = [0u8; 5];
905 c.reader.read_exact(&mut buf).unwrap();
906 buf
907 });
908 let Accepted::Conn(mut conn) = l.accept().unwrap() else {
909 panic!("same-user peer must be accepted");
910 };
911 let mut buf = [0u8; 5];
912 conn.reader.read_exact(&mut buf).unwrap();
913 assert_eq!(&buf, b"ping\n");
914 conn.writer.write_all(b"pong\n").unwrap();
915 conn.writer.flush().unwrap();
916 assert_eq!(&t.join().unwrap(), b"pong\n");
917 l.cleanup();
918 }
919
920 #[cfg(unix)]
922 #[test]
923 fn rejected_peer_is_closed_unread() {
924 use crate::peer::{PeerError, PeerIdentity};
925 let dir = tmp();
926 let ep = endpoint(&dir);
927 let l = Listener::bind(&ep, 4).unwrap();
928 let client_ep = ep.clone();
929 let t = std::thread::spawn(move || {
930 let mut s = std::os::unix::net::UnixStream::connect(&client_ep).unwrap();
931 let _ = s.write_all(b"{\"type\":\"hello\"}\n");
932 let mut b = [0u8; 1];
933 matches!(s.read(&mut b), Ok(0) | Err(_))
935 });
936 let foreign = PeerIdentity {
937 uid: Some(crate::peer::current_euid().wrapping_add(1)),
938 ..Default::default()
939 };
940 match l
941 .accept_with(|_| Err(PeerError::Mismatch(foreign)))
942 .unwrap()
943 {
944 Accepted::Rejected(reason) => assert!(reason.contains("not the current user")),
945 _ => panic!("a foreign peer must be rejected"),
946 }
947 assert!(
948 t.join().unwrap(),
949 "rejected client must see a close, not a reply"
950 );
951 l.cleanup();
952 }
953
954 #[cfg(unix)]
956 #[test]
957 fn unsupported_verifier_never_admits() {
958 use crate::peer::PeerError;
959 let dir = tmp();
960 let ep = endpoint(&dir);
961 let l = Listener::bind(&ep, 4).unwrap();
962 let client_ep = ep.clone();
963 let t = std::thread::spawn(move || std::os::unix::net::UnixStream::connect(client_ep));
964 let a = l
965 .accept_with(|_| Err(PeerError::Unsupported("test")))
966 .unwrap();
967 assert!(matches!(a, Accepted::Rejected(_)));
968 let _ = t.join();
969 l.cleanup();
970 }
971
972 #[cfg(windows)]
973 #[test]
974 fn public_pipe_verifiers_accept_a_same_user_pipe() {
975 use crate::peer::{
976 owner_only_security_attributes, verify_pipe_peer_is_current_user,
977 verify_pipe_server_is_current_user,
978 };
979 use std::os::windows::fs::OpenOptionsExt;
980 use std::os::windows::io::{AsRawHandle, RawHandle};
981 use windows::core::PCWSTR;
982 use windows::Win32::Security::SECURITY_ATTRIBUTES;
983 use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX;
984 use windows::Win32::System::Pipes::{
985 ConnectNamedPipe, CreateNamedPipeW, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE,
986 PIPE_WAIT,
987 };
988 let name = format!(r"\\.\pipe\rightkit-verify-test-{}", unique("v"));
989 let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
990 let attrs = owner_only_security_attributes().unwrap();
991 let server = unsafe {
992 CreateNamedPipeW(
993 PCWSTR(wide.as_ptr()),
994 PIPE_ACCESS_DUPLEX,
995 PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
996 1,
997 4096,
998 4096,
999 0,
1000 Some(attrs.as_ptr() as *const SECURITY_ATTRIBUTES),
1001 )
1002 };
1003 assert!(!server.is_invalid());
1004 let client_name = name.clone();
1005 let t = std::thread::spawn(move || {
1006 std::fs::OpenOptions::new()
1007 .read(true)
1008 .write(true)
1009 .security_qos_flags(0x0010_0000 | 0x0001_0000)
1010 .open(client_name)
1011 .unwrap()
1012 });
1013 unsafe { ConnectNamedPipe(server, None) }.ok();
1015 let client = t.join().unwrap();
1016 let id = verify_pipe_peer_is_current_user(server.0 as RawHandle).unwrap();
1017 assert_eq!(id.pid, Some(std::process::id()));
1018 assert_eq!(
1019 id.sid,
1020 Some(crate::peer::current_user_sid_string().unwrap())
1021 );
1022 let id = verify_pipe_server_is_current_user(client.as_raw_handle()).unwrap();
1023 assert_eq!(id.pid, Some(std::process::id()));
1024 let _ = unsafe { windows::Win32::Foundation::CloseHandle(server) };
1025 }
1026
1027 fn long_service() -> String {
1030 format!("{}-instance", "a".repeat(39))
1031 }
1032
1033 #[test]
1034 fn fallback_file_name_is_pinned_and_bounded() {
1035 assert_eq!(
1037 fallback_file_name("0badc0de", &long_service()),
1038 "rk-0badc0de-3a43f919c4e992b6.sock"
1039 );
1040 assert_eq!(
1041 fallback_file_name("0badc0de", "speech"),
1042 "rk-0badc0de-c1d6288e168cb0d7.sock"
1043 );
1044 assert_eq!(fallback_file_name("0badc0de", &long_service()).len(), 33);
1045 assert_eq!(fnv1a64(""), 0xcbf2_9ce4_8422_2325);
1046 }
1047
1048 #[cfg(unix)]
1049 fn long_root() -> tempfile::TempDir {
1050 let d = tempfile::Builder::new()
1053 .prefix(&"r".repeat(49))
1054 .tempdir_in("/tmp")
1055 .unwrap();
1056 let n = d.path().as_os_str().len();
1057 assert!((50..=69).contains(&n), "root is {n} bytes");
1058 d
1059 }
1060
1061 #[cfg(unix)]
1062 #[test]
1063 fn a_48_byte_service_name_binds_and_connects_under_a_long_temp_root() {
1064 let suite = Suite::per_run_temp("ep-long").unwrap();
1065 let service = long_service();
1066 assert_eq!(service.len(), 48);
1067 let root = long_root();
1068 let ep = unix_endpoint_in(&suite, &service, root.path()).unwrap();
1069 assert!(ep.starts_with(root.path().to_str().unwrap()));
1070 assert!(
1071 ep.len() <= MAX_UNIX_SOCKET_PATH,
1072 "{ep} is {} bytes",
1073 ep.len()
1074 );
1075 assert_eq!(legacy_unix_endpoint_in(&suite, &service, root.path()), None);
1077 let l = Listener::bind(&ep, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
1078 assert!(connect(&ep).is_ok());
1079 l.cleanup();
1080 assert_eq!(
1082 candidates_in(&suite, &service, root.path()),
1083 vec![ep.clone()]
1084 );
1085 }
1086
1087 #[cfg(unix)]
1088 #[test]
1089 fn a_new_client_still_finds_an_old_host_on_the_old_fallback_name() {
1090 let suite = Suite::per_run_temp("ep-old").unwrap();
1091 let service = "legacy-fallback-name";
1093 let root = tempfile::Builder::new()
1094 .prefix("rk")
1095 .tempdir_in("/tmp")
1096 .unwrap();
1097 if suite
1099 .service_dir(service)
1100 .join(format!("{service}.sock"))
1101 .as_os_str()
1102 .len()
1103 <= IN_SUITE_PATH_LIMIT
1104 {
1105 return;
1106 }
1107 let new = unix_endpoint_in(&suite, service, root.path()).unwrap();
1108 let old = legacy_unix_endpoint_in(&suite, service, root.path()).expect("old name fitted");
1109 assert_ne!(new, old);
1110 assert_eq!(
1111 candidates_in(&suite, service, root.path()),
1112 vec![new.clone(), old.clone()]
1113 );
1114 let _old_host = std::os::unix::net::UnixListener::bind(&old).unwrap();
1116 let live = candidates_in(&suite, service, root.path())
1117 .into_iter()
1118 .find(|e| probe_endpoint(e));
1119 assert_eq!(live.as_deref(), Some(old.as_str()));
1120 let l = Listener::bind(&new, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
1122 let live = candidates_in(&suite, service, root.path())
1123 .into_iter()
1124 .find(|e| probe_endpoint(e));
1125 assert_eq!(live.as_deref(), Some(new.as_str()));
1126 l.cleanup();
1127 }
1128
1129 #[cfg(unix)]
1130 #[test]
1131 fn an_unfittable_temp_root_is_a_typed_error_naming_path_length_and_limit() {
1132 let suite = Suite::per_run_temp("ep-absurd").unwrap();
1133 let service = long_service();
1134 let temp = std::path::PathBuf::from(format!("/{}", "t".repeat(90)));
1135 let e = unix_endpoint_in(&suite, &service, &temp).unwrap_err();
1136 assert_eq!(e.limit, MAX_UNIX_SOCKET_PATH);
1137 assert_eq!(e.len, e.path.len());
1138 assert!(e.len > e.limit);
1139 assert!(e.path.starts_with(temp.to_str().unwrap()));
1140 let msg = e.to_string();
1141 assert!(msg.contains(&e.path) && msg.contains(&e.len.to_string()) && msg.contains("103"));
1142 assert!(legacy_unix_endpoint_in(&suite, &service, &temp).is_none());
1143 assert!(candidates_in(&suite, &service, &temp).is_empty());
1144 }
1145}