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