|
|
|
@@ -0,0 +1,533 @@
|
|
|
|
|
//! Local IPC between `kestrove-nmh` (started by the browser) and the running app.
|
|
|
|
|
//!
|
|
|
|
|
//! * Unix: a socket in a 0700 per-user directory, socket mode 0600, and a peer-credential check
|
|
|
|
|
//! on both ends (`SO_PEERCRED` on Linux, `getpeereid` on macOS/BSD). Linux uses
|
|
|
|
|
//! `$XDG_RUNTIME_DIR/kestrove/nmh.sock`. On macOS the sandboxed Kestrove app serves the socket from
|
|
|
|
|
//! its App Group container (`~/Library/Group Containers/<group>/ipc/nmh.sock`); the `kestrove-nmh`
|
|
|
|
|
//! bundled in Kestrove.app finds the group in `Contents/Resources/kestrove-app-group` (sealed by the
|
|
|
|
|
//! app's signature). A host outside an app bundle (the desktop app, development) uses the per-user
|
|
|
|
|
//! temp dir from `confstr(_CS_DARWIN_USER_TEMP_DIR)`.
|
|
|
|
|
//! * Windows: the named pipe `\\.\pipe\kestrove-nmh-<user>`, created first-instance-only (no
|
|
|
|
|
//! squatting), local clients only, with a DACL for the current user, plus a client token check.
|
|
|
|
|
//!
|
|
|
|
|
//! The channel carries only already-encrypted pairing frames (see `channel`), so this layer keeps
|
|
|
|
|
//! other users and remote machines out; the pairing key keeps other same-user processes out.
|
|
|
|
|
//! Traffic is strictly request → response: no concurrent read and write on one connection
|
|
|
|
|
//! (synchronous Windows pipe handles serialize I/O, so that would deadlock).
|
|
|
|
|
|
|
|
|
|
use std::io::{self, Read, Write};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
/// Overrides the IPC directory (Unix) or pipe name suffix (Windows). Tests use it to run an
|
|
|
|
|
/// isolated app; the browser passes its environment through to the host, so a browser started
|
|
|
|
|
/// with it set reaches that app.
|
|
|
|
|
pub const ENV_IPC_DIR: &str = "KESTROVE_IPC_DIR";
|
|
|
|
|
|
|
|
|
|
pub use imp::{connect, connect_in, Conn, Listener};
|
|
|
|
|
|
|
|
|
|
/// Where the socket lives (Unix) or a display string for the pipe (Windows).
|
|
|
|
|
pub fn endpoint() -> io::Result<PathBuf> {
|
|
|
|
|
imp::endpoint()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Name of the file next to a bundled `kestrove-nmh` (`Contents/Resources/`) that names the Mac app's App Group.
|
|
|
|
|
pub const APP_GROUP_FILE: &str = "kestrove-app-group";
|
|
|
|
|
|
|
|
|
|
impl Read for Conn {
|
|
|
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
|
|
|
self.0.read(buf)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Write for Conn {
|
|
|
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
|
|
|
self.0.write(buf)
|
|
|
|
|
}
|
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
|
|
|
self.0.flush()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
mod imp {
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
|
|
|
|
|
use std::os::unix::io::AsRawFd;
|
|
|
|
|
use std::os::unix::net::{UnixListener, UnixStream};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use super::ENV_IPC_DIR;
|
|
|
|
|
|
|
|
|
|
pub struct Conn(pub(super) UnixStream);
|
|
|
|
|
|
|
|
|
|
/// macOS limits `sun_path` to 104 bytes.
|
|
|
|
|
const MAX_SOCKET_PATH: usize = 103;
|
|
|
|
|
|
|
|
|
|
pub struct Listener {
|
|
|
|
|
inner: UnixListener,
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn uid() -> u32 {
|
|
|
|
|
// SAFETY: geteuid has no preconditions and cannot fail.
|
|
|
|
|
unsafe { libc::geteuid() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn base_dir() -> io::Result<PathBuf> {
|
|
|
|
|
if let Some(d) = std::env::var_os(ENV_IPC_DIR).filter(|d| !d.is_empty()) {
|
|
|
|
|
return Ok(PathBuf::from(d));
|
|
|
|
|
}
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
if let Some(d) = bundled_app_group_dir() {
|
|
|
|
|
return Ok(d);
|
|
|
|
|
}
|
|
|
|
|
platform_dir()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The Mac app's socket directory, when this executable is the `kestrove-nmh` inside Kestrove.app.
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
fn bundled_app_group_dir() -> Option<PathBuf> {
|
|
|
|
|
let exe = std::env::current_exe().ok()?;
|
|
|
|
|
let marker = exe.parent()?.parent()?.join("Resources").join(super::APP_GROUP_FILE);
|
|
|
|
|
let group = fs::read_to_string(marker).ok()?;
|
|
|
|
|
let group = group.trim();
|
|
|
|
|
let valid = !group.is_empty()
|
|
|
|
|
&& group.len() <= 128
|
|
|
|
|
&& !group.starts_with('.')
|
|
|
|
|
&& group.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-');
|
|
|
|
|
valid.then(|| home_dir().map(|h| h.join("Library/Group Containers").join(group).join("ipc")))?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The real home directory (not a sandbox container's), from the password database.
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
fn home_dir() -> Option<PathBuf> {
|
|
|
|
|
use std::ffi::{CStr, OsStr};
|
|
|
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
|
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
|
|
|
|
|
let mut buf = vec![0 as libc::c_char; 4096];
|
|
|
|
|
let mut out: *mut libc::passwd = std::ptr::null_mut();
|
|
|
|
|
// SAFETY: all pointers are valid for the sizes passed; `out` is null or points at `pwd`.
|
|
|
|
|
let rc = unsafe { libc::getpwuid_r(uid(), &mut pwd, buf.as_mut_ptr(), buf.len(), &mut out) };
|
|
|
|
|
if rc != 0 || out.is_null() || pwd.pw_dir.is_null() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
// SAFETY: pw_dir points into `buf`, NUL-terminated by getpwuid_r.
|
|
|
|
|
let dir = unsafe { CStr::from_ptr(pwd.pw_dir) };
|
|
|
|
|
Some(PathBuf::from(OsStr::from_bytes(dir.to_bytes())))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
fn platform_dir() -> io::Result<PathBuf> {
|
|
|
|
|
use std::ffi::{CStr, OsStr};
|
|
|
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
|
let mut buf = vec![0u8; 1024];
|
|
|
|
|
// SAFETY: buf is valid for buf.len() bytes; confstr NUL-terminates within that length.
|
|
|
|
|
let n = unsafe { libc::confstr(libc::_CS_DARWIN_USER_TEMP_DIR, buf.as_mut_ptr().cast(), buf.len()) };
|
|
|
|
|
if n == 0 || n > buf.len() {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
let s = CStr::from_bytes_until_nul(&buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
|
|
|
|
Ok(PathBuf::from(OsStr::from_bytes(s.to_bytes())).join("kestrove"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
|
fn platform_dir() -> io::Result<PathBuf> {
|
|
|
|
|
match std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from) {
|
|
|
|
|
Some(d) if d.is_absolute() => Ok(d.join("kestrove")),
|
|
|
|
|
// No runtime dir (e.g. no systemd session): a per-uid dir in /tmp, ownership checked in secure_dir.
|
|
|
|
|
_ => Ok(PathBuf::from(format!("/tmp/kestrove-{}", uid()))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn endpoint() -> io::Result<PathBuf> {
|
|
|
|
|
Ok(base_dir()?.join("nmh.sock"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn socket_in(dir: &std::path::Path) -> io::Result<PathBuf> {
|
|
|
|
|
let path = dir.join("nmh.sock");
|
|
|
|
|
if path.as_os_str().len() > MAX_SOCKET_PATH {
|
|
|
|
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "IPC socket path is too long"));
|
|
|
|
|
}
|
|
|
|
|
Ok(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Creates the directory 0700, or checks that an existing one is ours and not group/world accessible.
|
|
|
|
|
fn secure_dir(dir: &std::path::Path) -> io::Result<()> {
|
|
|
|
|
match fs::DirBuilder::new().recursive(true).mode(0o700).create(dir) {
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
|
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
|
}
|
|
|
|
|
let meta = fs::symlink_metadata(dir)?;
|
|
|
|
|
if !meta.is_dir() || meta.uid() != uid() {
|
|
|
|
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC directory is not owned by this user"));
|
|
|
|
|
}
|
|
|
|
|
if meta.mode() & 0o077 != 0 {
|
|
|
|
|
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
|
|
|
|
pub(super) fn peer_uid(s: &UnixStream) -> io::Result<u32> {
|
|
|
|
|
let mut cred = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
|
|
|
|
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
|
|
|
|
|
// SAFETY: cred and len are valid out-pointers of the sizes passed.
|
|
|
|
|
let rc = unsafe {
|
|
|
|
|
libc::getsockopt(
|
|
|
|
|
s.as_raw_fd(),
|
|
|
|
|
libc::SOL_SOCKET,
|
|
|
|
|
libc::SO_PEERCRED,
|
|
|
|
|
(&mut cred as *mut libc::ucred).cast(),
|
|
|
|
|
&mut len,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
if rc != 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
Ok(cred.uid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
|
|
|
|
pub(super) fn peer_uid(s: &UnixStream) -> io::Result<u32> {
|
|
|
|
|
let (mut uid, mut gid) = (0, 0);
|
|
|
|
|
// SAFETY: uid and gid are valid out-pointers.
|
|
|
|
|
if unsafe { libc::getpeereid(s.as_raw_fd(), &mut uid, &mut gid) } != 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
Ok(uid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_peer(s: &UnixStream) -> io::Result<()> {
|
|
|
|
|
if peer_uid(s)? != uid() {
|
|
|
|
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC peer is a different user"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Listener {
|
|
|
|
|
/// Binds the socket. Fails with `AddrInUse` if another app instance is already serving it.
|
|
|
|
|
pub fn bind() -> io::Result<Self> {
|
|
|
|
|
Self::bind_in(&base_dir()?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Binds `<dir>/nmh.sock`: the Mac app passes its App Group container's `ipc` directory.
|
|
|
|
|
pub fn bind_in(dir: &std::path::Path) -> io::Result<Self> {
|
|
|
|
|
let path = socket_in(dir)?;
|
|
|
|
|
secure_dir(dir)?;
|
|
|
|
|
if path.exists() {
|
|
|
|
|
if UnixStream::connect(&path).is_ok() {
|
|
|
|
|
return Err(io::Error::new(io::ErrorKind::AddrInUse, "Kestrove is already running"));
|
|
|
|
|
}
|
|
|
|
|
fs::remove_file(&path)?; // stale socket from a crashed instance
|
|
|
|
|
}
|
|
|
|
|
let inner = UnixListener::bind(&path)?;
|
|
|
|
|
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?;
|
|
|
|
|
Ok(Self { inner, path })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Blocks for the next same-user client. Other users' connections are dropped and skipped.
|
|
|
|
|
pub fn accept(&self) -> io::Result<Conn> {
|
|
|
|
|
loop {
|
|
|
|
|
let (s, _) = self.inner.accept()?;
|
|
|
|
|
if check_peer(&s).is_ok() {
|
|
|
|
|
// A stuck peer must not pin a thread forever.
|
|
|
|
|
s.set_read_timeout(Some(Duration::from_secs(600)))?;
|
|
|
|
|
return Ok(Conn(s));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for Listener {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _ = fs::remove_file(&self.path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn connect() -> io::Result<Conn> {
|
|
|
|
|
connect_in(&base_dir()?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn connect_in(dir: &std::path::Path) -> io::Result<Conn> {
|
|
|
|
|
let s = UnixStream::connect(socket_in(dir)?)?;
|
|
|
|
|
check_peer(&s)?; // don't talk to a socket another user planted
|
|
|
|
|
Ok(Conn(s))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
mod imp {
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::os::windows::ffi::OsStrExt;
|
|
|
|
|
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::ptr;
|
|
|
|
|
|
|
|
|
|
use windows_sys::Win32::Foundation::{
|
|
|
|
|
CloseHandle, GetLastError, LocalFree, ERROR_PIPE_CONNECTED, GENERIC_READ, GENERIC_WRITE, HANDLE,
|
|
|
|
|
INVALID_HANDLE_VALUE,
|
|
|
|
|
};
|
|
|
|
|
use windows_sys::Win32::Security::Authorization::{
|
|
|
|
|
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
|
|
|
|
};
|
|
|
|
|
use windows_sys::Win32::Security::{
|
|
|
|
|
EqualSid, GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
|
|
|
|
|
};
|
|
|
|
|
use windows_sys::Win32::Storage::FileSystem::{
|
|
|
|
|
CreateFileW, FILE_FLAG_FIRST_PIPE_INSTANCE, OPEN_EXISTING, PIPE_ACCESS_DUPLEX,
|
|
|
|
|
};
|
|
|
|
|
use windows_sys::Win32::System::Pipes::{
|
|
|
|
|
ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, GetNamedPipeServerProcessId,
|
|
|
|
|
PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT,
|
|
|
|
|
};
|
|
|
|
|
use windows_sys::Win32::System::Threading::{
|
|
|
|
|
GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use super::ENV_IPC_DIR;
|
|
|
|
|
|
|
|
|
|
pub struct Conn(pub(super) File);
|
|
|
|
|
|
|
|
|
|
pub struct Listener {
|
|
|
|
|
name: Vec<u16>,
|
|
|
|
|
sddl: Vec<u16>,
|
|
|
|
|
first: std::sync::atomic::AtomicBool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wide(s: &str) -> Vec<u16> {
|
|
|
|
|
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn pipe_name() -> String {
|
|
|
|
|
let suffix = std::env::var(ENV_IPC_DIR)
|
|
|
|
|
.ok()
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
.or_else(|| std::env::var("USERNAME").ok())
|
|
|
|
|
.unwrap_or_else(|| "user".into());
|
|
|
|
|
let clean: String = suffix.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }).collect();
|
|
|
|
|
format!(r"\\.\pipe\kestrove-nmh-{clean}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn endpoint() -> io::Result<PathBuf> {
|
|
|
|
|
Ok(PathBuf::from(pipe_name()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The TOKEN_USER buffer of a process (the SID pointer points into it).
|
|
|
|
|
fn token_user(process: HANDLE) -> io::Result<Vec<u8>> {
|
|
|
|
|
let mut token: HANDLE = ptr::null_mut();
|
|
|
|
|
// SAFETY: valid process handle and out-pointer.
|
|
|
|
|
if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
// SAFETY: we own the token handle from here on.
|
|
|
|
|
let token = unsafe { OwnedHandle::from_raw_handle(token) };
|
|
|
|
|
let mut len = 0u32;
|
|
|
|
|
// SAFETY: a size query with a null buffer is the documented pattern.
|
|
|
|
|
unsafe { GetTokenInformation(token.as_raw_handle(), TokenUser, ptr::null_mut(), 0, &mut len) };
|
|
|
|
|
let mut buf = vec![0u8; len as usize];
|
|
|
|
|
// SAFETY: buf has len bytes.
|
|
|
|
|
if unsafe { GetTokenInformation(token.as_raw_handle(), TokenUser, buf.as_mut_ptr().cast(), len, &mut len) } == 0
|
|
|
|
|
{
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
Ok(buf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sid_of(buf: &[u8]) -> *mut core::ffi::c_void {
|
|
|
|
|
// SAFETY: buf came from GetTokenInformation(TokenUser) and starts with a TOKEN_USER.
|
|
|
|
|
unsafe { (*(buf.as_ptr() as *const TOKEN_USER)).User.Sid }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn same_user_as_process(pid: u32) -> io::Result<()> {
|
|
|
|
|
// SAFETY: plain Win32 calls; every handle is closed before returning.
|
|
|
|
|
unsafe {
|
|
|
|
|
let ours = token_user(GetCurrentProcess())?;
|
|
|
|
|
let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
|
|
|
|
if h.is_null() {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
let theirs = token_user(h);
|
|
|
|
|
CloseHandle(h);
|
|
|
|
|
let theirs = theirs?;
|
|
|
|
|
if EqualSid(sid_of(&ours), sid_of(&theirs)) == 0 {
|
|
|
|
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC peer is a different user"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// SDDL granting full access to the current user only (no Everyone read like the default DACL).
|
|
|
|
|
fn user_only_sddl() -> io::Result<Vec<u16>> {
|
|
|
|
|
// SAFETY: ConvertSidToStringSidW allocates with LocalAlloc; we free it after copying.
|
|
|
|
|
unsafe {
|
|
|
|
|
let ours = token_user(GetCurrentProcess())?;
|
|
|
|
|
let mut s: *mut u16 = ptr::null_mut();
|
|
|
|
|
if ConvertSidToStringSidW(sid_of(&ours), &mut s) == 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
let mut n = 0;
|
|
|
|
|
while *s.add(n) != 0 {
|
|
|
|
|
n += 1;
|
|
|
|
|
}
|
|
|
|
|
let sid = String::from_utf16_lossy(std::slice::from_raw_parts(s, n));
|
|
|
|
|
LocalFree(s.cast());
|
|
|
|
|
Ok(wide(&format!("D:P(A;;GA;;;{sid})")))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Listener {
|
|
|
|
|
pub fn bind() -> io::Result<Self> {
|
|
|
|
|
let me = Self {
|
|
|
|
|
name: wide(&pipe_name()),
|
|
|
|
|
sddl: user_only_sddl()?,
|
|
|
|
|
first: std::sync::atomic::AtomicBool::new(true),
|
|
|
|
|
};
|
|
|
|
|
Ok(me)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_instance(&self) -> io::Result<HANDLE> {
|
|
|
|
|
let first = self.first.swap(false, std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut();
|
|
|
|
|
// SAFETY: sddl is NUL-terminated; sd is freed with LocalFree below.
|
|
|
|
|
unsafe {
|
|
|
|
|
if ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
|
|
|
self.sddl.as_ptr(),
|
|
|
|
|
SDDL_REVISION_1,
|
|
|
|
|
&mut sd,
|
|
|
|
|
ptr::null_mut(),
|
|
|
|
|
) == 0
|
|
|
|
|
{
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
let sa = SECURITY_ATTRIBUTES {
|
|
|
|
|
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
|
|
|
|
lpSecurityDescriptor: sd,
|
|
|
|
|
bInheritHandle: 0,
|
|
|
|
|
};
|
|
|
|
|
let h = CreateNamedPipeW(
|
|
|
|
|
self.name.as_ptr(),
|
|
|
|
|
PIPE_ACCESS_DUPLEX | if first { FILE_FLAG_FIRST_PIPE_INSTANCE } else { 0 },
|
|
|
|
|
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
|
|
|
|
|
PIPE_UNLIMITED_INSTANCES,
|
|
|
|
|
64 * 1024,
|
|
|
|
|
64 * 1024,
|
|
|
|
|
0,
|
|
|
|
|
&sa,
|
|
|
|
|
);
|
|
|
|
|
LocalFree(sd);
|
|
|
|
|
if h == INVALID_HANDLE_VALUE {
|
|
|
|
|
let e = io::Error::last_os_error();
|
|
|
|
|
return Err(if first { io::Error::new(io::ErrorKind::AddrInUse, e) } else { e });
|
|
|
|
|
}
|
|
|
|
|
Ok(h)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Named pipes have no directory: the same as [`Listener::bind`].
|
|
|
|
|
pub fn bind_in(_dir: &std::path::Path) -> io::Result<Self> {
|
|
|
|
|
Self::bind()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn accept(&self) -> io::Result<Conn> {
|
|
|
|
|
loop {
|
|
|
|
|
let h = self.create_instance()?;
|
|
|
|
|
// SAFETY: h is a fresh pipe handle owned by `file` from here on.
|
|
|
|
|
let file = unsafe { File::from_raw_handle(h) };
|
|
|
|
|
// SAFETY: blocking connect on our own pipe handle.
|
|
|
|
|
let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) } != 0
|
|
|
|
|
|| unsafe { GetLastError() } == ERROR_PIPE_CONNECTED;
|
|
|
|
|
if !ok {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let mut pid = 0u32;
|
|
|
|
|
// SAFETY: valid handle and out-pointer.
|
|
|
|
|
if unsafe { GetNamedPipeClientProcessId(h, &mut pid) } != 0 && same_user_as_process(pid).is_ok() {
|
|
|
|
|
return Ok(Conn(file));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Named pipes have no directory: the same as [`connect`].
|
|
|
|
|
pub fn connect_in(_dir: &std::path::Path) -> io::Result<Conn> {
|
|
|
|
|
connect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn connect() -> io::Result<Conn> {
|
|
|
|
|
let name = wide(&pipe_name());
|
|
|
|
|
// SAFETY: name is NUL-terminated; the handle is owned by File on success.
|
|
|
|
|
let h = unsafe {
|
|
|
|
|
CreateFileW(name.as_ptr(), GENERIC_READ | GENERIC_WRITE, 0, ptr::null(), OPEN_EXISTING, 0, ptr::null_mut())
|
|
|
|
|
};
|
|
|
|
|
if h == INVALID_HANDLE_VALUE {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
// SAFETY: h is a valid, owned handle.
|
|
|
|
|
let file = unsafe { File::from_raw_handle(h) };
|
|
|
|
|
let mut pid = 0u32;
|
|
|
|
|
// SAFETY: valid handle and out-pointer.
|
|
|
|
|
if unsafe { GetNamedPipeServerProcessId(h, &mut pid) } == 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
same_user_as_process(pid)?;
|
|
|
|
|
Ok(Conn(file))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(all(test, unix))]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::framing::{read_json, write_json};
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn socket_is_private_and_round_trips() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let ipc_dir = dir.path().join("ipc");
|
|
|
|
|
// SAFETY: tests in this module are the only ones touching this variable.
|
|
|
|
|
unsafe { std::env::set_var(ENV_IPC_DIR, &ipc_dir) };
|
|
|
|
|
let l = Listener::bind().unwrap();
|
|
|
|
|
let path = endpoint().unwrap();
|
|
|
|
|
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
|
|
|
|
|
assert_eq!(std::fs::metadata(&ipc_dir).unwrap().permissions().mode() & 0o777, 0o700);
|
|
|
|
|
|
|
|
|
|
let t = std::thread::spawn(move || {
|
|
|
|
|
let mut c = l.accept().unwrap();
|
|
|
|
|
let v = read_json(&mut c).unwrap().unwrap();
|
|
|
|
|
write_json(&mut c, &json!({"echo": v})).unwrap();
|
|
|
|
|
l
|
|
|
|
|
});
|
|
|
|
|
let mut c = connect().unwrap();
|
|
|
|
|
write_json(&mut c, &json!({"t": "ping"})).unwrap();
|
|
|
|
|
assert_eq!(read_json(&mut c).unwrap(), Some(json!({"echo": {"t": "ping"}})));
|
|
|
|
|
let l = t.join().unwrap();
|
|
|
|
|
// A second instance sees the live socket and refuses to steal it.
|
|
|
|
|
assert_eq!(Listener::bind().err().map(|e| e.kind()), Some(io::ErrorKind::AddrInUse));
|
|
|
|
|
drop(l);
|
|
|
|
|
assert!(!path.exists(), "socket removed on shutdown");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn explicit_directory() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let ipc_dir = dir.path().join("ipc");
|
|
|
|
|
let l = Listener::bind_in(&ipc_dir).unwrap();
|
|
|
|
|
assert!(ipc_dir.join("nmh.sock").exists());
|
|
|
|
|
let t = std::thread::spawn(move || {
|
|
|
|
|
let mut c = l.accept().unwrap();
|
|
|
|
|
let v = read_json(&mut c).unwrap().unwrap();
|
|
|
|
|
write_json(&mut c, &v).unwrap();
|
|
|
|
|
});
|
|
|
|
|
let mut c = connect_in(&ipc_dir).unwrap();
|
|
|
|
|
write_json(&mut c, &json!({"t": "ping"})).unwrap();
|
|
|
|
|
assert_eq!(read_json(&mut c).unwrap(), Some(json!({"t": "ping"})));
|
|
|
|
|
t.join().unwrap();
|
|
|
|
|
let long = dir.path().join("x".repeat(120));
|
|
|
|
|
assert_eq!(Listener::bind_in(&long).err().map(|e| e.kind()), Some(io::ErrorKind::InvalidInput));
|
|
|
|
|
}
|
|
|
|
|
}
|