use std::io;
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient, NamedPipeServer};
const PIPE_BUSY: i32 = 231;
const ACCESS_DENIED: i32 = 5;
#[derive(Debug)]
pub enum LocalStream {
Server(NamedPipeServer),
Client(NamedPipeClient),
}
macro_rules! delegate {
($self:ident, $method:ident, $($arg:expr),*) => {
match $self.get_mut() {
LocalStream::Server(s) => Pin::new(s).$method($($arg),*),
LocalStream::Client(c) => Pin::new(c).$method($($arg),*),
}
};
}
impl AsyncRead for LocalStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
delegate!(self, poll_read, cx, buf)
}
}
impl AsyncWrite for LocalStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
delegate!(self, poll_write, cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
delegate!(self, poll_flush, cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
delegate!(self, poll_shutdown, cx)
}
}
pub type LocalReadHalf = tokio::io::ReadHalf<LocalStream>;
pub type LocalWriteHalf = tokio::io::WriteHalf<LocalStream>;
pub fn split_local(stream: LocalStream) -> (LocalReadHalf, LocalWriteHalf) {
tokio::io::split(stream)
}
pub async fn connect_local(path: &Path) -> io::Result<LocalStream> {
let mut tries = 0u32;
loop {
match ClientOptions::new().open(path) {
Ok(client) => return Ok(LocalStream::Client(client)),
Err(e) if e.raw_os_error() == Some(PIPE_BUSY) && tries < 50 => {
tries += 1;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
Err(e) => return Err(e),
}
}
}
#[cfg(feature = "service")]
pub fn authorize_local_peer(_stream: &LocalStream) -> bool {
true
}
#[cfg(feature = "service")]
#[derive(Debug)]
pub struct LocalListener {
path: std::path::PathBuf,
next: NamedPipeServer,
}
#[cfg(feature = "service")]
impl LocalListener {
pub async fn accept(&mut self) -> io::Result<LocalStream> {
self.next.connect().await?;
let connected = std::mem::replace(&mut self.next, create_instance(&self.path, false)?);
Ok(LocalStream::Server(connected))
}
}
#[cfg(feature = "service")]
pub fn bind_local(path: &Path) -> io::Result<LocalListener> {
let first = create_instance(path, true).map_err(|e| {
if e.raw_os_error() == Some(ACCESS_DENIED) {
io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"pipe {} already owned by another daemon: {e}",
path.display()
),
)
} else {
io::Error::new(e.kind(), format!("bind {}: {e}", path.display()))
}
})?;
Ok(LocalListener {
path: path.to_path_buf(),
next: first,
})
}
#[cfg(feature = "service")]
fn create_instance(path: &Path, first: bool) -> io::Result<NamedPipeServer> {
use tokio::net::windows::named_pipe::ServerOptions;
let sd = winsec::owner_only_attributes()?;
let mut opts = ServerOptions::new();
opts.first_pipe_instance(first);
sd.create(&mut opts, path)
}
#[cfg(feature = "service")]
#[allow(unsafe_code)]
mod winsec {
use super::*;
use core::ffi::c_void;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, HLOCAL, LocalFree};
use windows_sys::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
SDDL_REVISION_1,
};
use windows_sys::Win32::Security::{
GetTokenInformation, PSID, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER, TokenUser,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows_sys::core::PWSTR;
pub(super) struct OwnedAttributes {
descriptor: *mut c_void,
}
impl Drop for OwnedAttributes {
fn drop(&mut self) {
unsafe { LocalFree(self.descriptor as HLOCAL) };
}
}
impl OwnedAttributes {
pub(super) fn create(
&self,
opts: &mut tokio::net::windows::named_pipe::ServerOptions,
path: &Path,
) -> io::Result<NamedPipeServer> {
let mut sa = SECURITY_ATTRIBUTES {
nLength: core::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: self.descriptor,
bInheritHandle: 0,
};
unsafe {
opts.create_with_security_attributes_raw(path, (&raw mut sa).cast::<c_void>())
}
}
}
fn current_user_sid() -> io::Result<String> {
let mut token: HANDLE = core::ptr::null_mut();
if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
return Err(io::Error::last_os_error());
}
let mut len: u32 = 0;
unsafe { GetTokenInformation(token, TokenUser, core::ptr::null_mut(), 0, &mut len) };
let mut buf = vec![0u8; len as usize];
let ok = unsafe {
GetTokenInformation(
token,
TokenUser,
buf.as_mut_ptr().cast::<c_void>(),
len,
&mut len,
)
};
if ok == 0 {
let e = io::Error::last_os_error();
unsafe { CloseHandle(token) };
return Err(e);
}
let tu: TOKEN_USER = unsafe { std::ptr::read_unaligned(buf.as_ptr().cast::<TOKEN_USER>()) };
let sid: PSID = tu.User.Sid;
let mut pwstr: PWSTR = core::ptr::null_mut();
let ok = unsafe { ConvertSidToStringSidW(sid, &mut pwstr) };
if ok == 0 {
let e = io::Error::last_os_error();
unsafe { CloseHandle(token) };
return Err(e);
}
let s = unsafe {
let mut n = 0usize;
while *pwstr.add(n) != 0 {
n += 1;
}
String::from_utf16_lossy(core::slice::from_raw_parts(pwstr, n))
};
unsafe { LocalFree(pwstr as HLOCAL) };
unsafe { CloseHandle(token) };
Ok(s)
}
pub(super) fn sddl_for_current_user() -> io::Result<String> {
Ok(format!("D:P(A;;GA;;;{})", current_user_sid()?))
}
pub(super) fn owner_only_attributes() -> io::Result<OwnedAttributes> {
let sddl: Vec<u16> = sddl_for_current_user()?.encode_utf16().chain([0]).collect();
let mut descriptor: *mut c_void = core::ptr::null_mut();
let ok = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
core::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(OwnedAttributes { descriptor })
}
}
#[cfg(all(test, feature = "service"))]
mod tests {
use super::*;
#[test]
fn sddl_is_owner_only() {
let sddl = winsec::sddl_for_current_user().unwrap();
assert!(
sddl.starts_with("D:P(A;;GA;;;S-1-"),
"one protected allow-ACE: {sddl}"
);
assert_eq!(sddl.matches("(A;").count(), 1, "no second ACE: {sddl}");
}
#[tokio::test]
async fn pipe_binds_accepts_same_user_and_is_singleton() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let path = Path::new(r"\\.\pipe\mcpmesh-transport-test");
let mut listener = bind_local(path).unwrap();
let second = bind_local(path);
assert_eq!(second.unwrap_err().kind(), io::ErrorKind::AddrInUse);
let (client, server) = tokio::join!(connect_local(path), listener.accept());
let (mut client, mut server) = (client.unwrap(), server.unwrap());
assert!(authorize_local_peer(&server));
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
server.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"ping");
server.write_all(b"pong").await.unwrap();
client.read_exact(&mut buf).await.unwrap();
assert_eq!(&buf, b"pong");
}
}