use super::{EvalFn, ListWindowsFn, PressHooksRef, handle_connection};
use crate::error::Error;
use crate::eval::EvalEngine;
use crate::recorder::Recorder;
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::ffi::c_void;
use std::mem;
use std::mem::MaybeUninit;
use std::os::windows::io::AsRawHandle;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
use windows::Win32::Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE};
use windows::Win32::Security::{
ACL, ACL_REVISION, AddAccessAllowedAce, EqualSid, GetLengthSid, GetTokenInformation, InitializeAcl,
InitializeSecurityDescriptor, PSECURITY_DESCRIPTOR, PSID, RevertToSelf, SECURITY_ATTRIBUTES,
SetSecurityDescriptorDacl, TOKEN_QUERY, TOKEN_USER, TokenUser,
};
use windows::Win32::System::Pipes::ImpersonateNamedPipeClient;
use windows::Win32::System::Threading::{GetCurrentProcess, GetCurrentThread, OpenProcessToken, OpenThreadToken};
pub fn socket_path(identifier: &str) -> PathBuf {
PathBuf::from(format!(r"\\.\pipe\tauri-hasgard-{identifier}"))
}
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct InstanceEntry {
pub pipe: String,
pub pid: u32,
pub created_at: u64,
}
fn instances_dir() -> std::io::Result<PathBuf> {
let local_app_data = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "LOCALAPPDATA environment variable is not set or empty")
})?;
Ok(PathBuf::from(local_app_data).join("tauri-hasgard").join("instances"))
}
fn instance_file_path(identifier: &str) -> std::io::Result<PathBuf> {
let dir = instances_dir()?;
std::fs::create_dir_all(&dir)?;
Ok(dir.join(format!("{identifier}.json")))
}
fn atomic_write_instance(path: &Path, entry: &InstanceEntry) -> std::io::Result<()> {
let json = serde_json::to_string(entry).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
fn register_instance(identifier: &str, pipe_path: &Path) -> std::io::Result<()> {
let path = instance_file_path(identifier)?;
let entry = InstanceEntry {
pipe: pipe_path.to_string_lossy().into_owned(),
pid: std::process::id(),
created_at: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(),
};
atomic_write_instance(&path, &entry)
}
fn unregister_instance(identifier: &str) -> std::io::Result<()> {
let path = instance_file_path(identifier)?;
if path.exists() {
std::fs::remove_file(&path)?;
}
Ok(())
}
pub struct RegistryGuard {
identifier: String,
}
impl Drop for RegistryGuard {
fn drop(&mut self) {
if let Err(e) = unregister_instance(&self.identifier) {
tracing::warn!(identifier = %self.identifier, error = %e, "failed to remove registry entry");
} else {
tracing::info!(identifier = %self.identifier, "registry entry removed");
}
}
}
struct AclBuffer {
ptr: *mut ACL,
layout: Layout,
}
impl AclBuffer {
fn as_ptr(&self) -> *mut ACL {
self.ptr
}
}
impl Drop for AclBuffer {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
dealloc(self.ptr.cast::<u8>(), self.layout);
}
}
}
}
struct OwnedHandle(HANDLE);
impl OwnedHandle {
fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.0.is_null() {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
}
struct SecurityAttributesGuard {
_sd: Box<MaybeUninit<windows::Win32::Security::SECURITY_DESCRIPTOR>>,
_acl: AclBuffer,
_sid_buf: Vec<u8>,
_token: OwnedHandle,
}
fn open_process_token() -> std::io::Result<OwnedHandle> {
let process = unsafe { GetCurrentProcess() };
let mut token = HANDLE(std::ptr::null_mut());
unsafe { OpenProcessToken(process, TOKEN_QUERY, &raw mut token) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(OwnedHandle(token))
}
fn open_thread_impersonation_token() -> std::io::Result<OwnedHandle> {
let thread = unsafe { GetCurrentThread() };
let mut token = HANDLE(std::ptr::null_mut());
unsafe { OpenThreadToken(thread, TOKEN_QUERY, true, &raw mut token) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(OwnedHandle(token))
}
fn get_user_sid(token: &OwnedHandle) -> std::io::Result<(Vec<u8>, PSID)> {
let mut return_length = 0u32;
unsafe {
let _ = GetTokenInformation(token.raw(), TokenUser, None, 0, &raw mut return_length);
}
if return_length == 0 {
return Err(std::io::Error::other("GetTokenInformation returned zero size"));
}
let mut buf = vec![0u8; return_length as usize];
unsafe {
GetTokenInformation(
token.raw(),
TokenUser,
Some(buf.as_mut_ptr().cast::<c_void>()),
return_length,
&raw mut return_length,
)
}
.map_err(|e| std::io::Error::other(e.to_string()))?;
#[allow(clippy::cast_ptr_alignment)]
let sid = unsafe { (*buf.as_ptr().cast::<TOKEN_USER>()).User.Sid };
Ok((buf, sid))
}
fn client_sid_matches_current_user(pipe: &NamedPipeServer) -> bool {
if unsafe { ImpersonateNamedPipeClient(HANDLE(pipe.as_raw_handle())) }.is_err() {
tracing::warn!("failed to impersonate named pipe client");
return true;
}
let client_token = match open_thread_impersonation_token() {
Ok(t) => t,
Err(e) => {
tracing::warn!(error = %e, "failed to open thread impersonation token");
unsafe {
let _ = RevertToSelf();
}
return true;
}
};
let client_sid_result = get_user_sid(&client_token);
unsafe {
let _ = RevertToSelf();
}
let (_client_buf, client_sid) = match client_sid_result {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "failed to read client SID");
return true;
}
};
let our_token = match open_process_token() {
Ok(t) => t,
Err(e) => {
tracing::warn!(error = %e, "failed to open process token");
return true;
}
};
let (_our_buf, our_sid) = match get_user_sid(&our_token) {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "failed to read own SID");
return true;
}
};
unsafe { EqualSid(client_sid, our_sid) }.is_ok()
}
fn build_acl(user_sid: PSID) -> std::io::Result<AclBuffer> {
let sid_length = unsafe { GetLengthSid(user_sid) } as usize;
let acl_size = (8 + 4 + 4 + sid_length + 3) & !3;
let layout = Layout::from_size_align(acl_size, mem::align_of::<ACL>())
.map_err(|e| std::io::Error::other(format!("invalid ACL layout: {e}")))?;
#[allow(clippy::cast_ptr_alignment)]
let ptr = unsafe { alloc_zeroed(layout) }.cast::<ACL>();
if ptr.is_null() {
return Err(std::io::Error::other("failed to allocate ACL"));
}
let buffer = AclBuffer { ptr, layout };
unsafe { InitializeAcl(buffer.as_ptr(), u32::try_from(acl_size).expect("ACL size fits in u32"), ACL_REVISION) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
unsafe { AddAccessAllowedAce(buffer.as_ptr(), ACL_REVISION, (GENERIC_READ | GENERIC_WRITE).0, user_sid) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(buffer)
}
fn build_security_descriptor(
acl: &AclBuffer,
) -> std::io::Result<Box<MaybeUninit<windows::Win32::Security::SECURITY_DESCRIPTOR>>> {
let sd_box = Box::new(MaybeUninit::<windows::Win32::Security::SECURITY_DESCRIPTOR>::uninit());
let sd_ptr = PSECURITY_DESCRIPTOR(sd_box.as_ptr() as *mut c_void);
unsafe { InitializeSecurityDescriptor(sd_ptr, 1) }.map_err(|e| std::io::Error::other(e.to_string()))?;
unsafe { SetSecurityDescriptorDacl(sd_ptr, true, Some(acl.as_ptr().cast_const()), false) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(sd_box)
}
fn create_user_only_security_attributes() -> std::io::Result<(SECURITY_ATTRIBUTES, SecurityAttributesGuard)> {
let token = open_process_token()?;
let (sid_buf, user_sid) = get_user_sid(&token)?;
let acl = build_acl(user_sid)?;
let sd = build_security_descriptor(&acl)?;
let sd_ptr = PSECURITY_DESCRIPTOR(sd.as_ptr() as *mut c_void);
let sa = SECURITY_ATTRIBUTES {
nLength: u32::try_from(mem::size_of::<SECURITY_ATTRIBUTES>())
.expect("SECURITY_ATTRIBUTES size must fit in u32"),
lpSecurityDescriptor: sd_ptr.0,
bInheritHandle: windows::core::BOOL(0),
};
let guard = SecurityAttributesGuard { _sd: sd, _acl: acl, _sid_buf: sid_buf, _token: token };
Ok((sa, guard))
}
pub fn bind(pipe_path: &Path) -> Result<(NamedPipeServer, RegistryGuard), Error> {
let (mut sa, _sec_guard) = create_user_only_security_attributes().map_err(Error::from)?;
let server = unsafe {
ServerOptions::new()
.first_pipe_instance(true)
.pipe_mode(tokio::net::windows::named_pipe::PipeMode::Byte)
.create_with_security_attributes_raw(pipe_path, (&raw mut sa).cast::<c_void>())
}
.map_err(Error::from)?;
tracing::info!(version = env!("CARGO_PKG_VERSION"), path = %pipe_path.display(), "tauri-hasgard named pipe listening");
let identifier = pipe_path
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.strip_prefix("tauri-hasgard-"))
.unwrap_or("unknown")
.to_string();
register_instance(&identifier, pipe_path)?;
let guard = RegistryGuard { identifier };
Ok((server, guard))
}
pub async fn run(
pipe_path: PathBuf, engine: EvalEngine, eval_fn: Option<EvalFn>, list_fn: Option<ListWindowsFn>,
press_hooks: Option<PressHooksRef>, recorder: Recorder,
) {
let (first_server, guard) = match bind(&pipe_path) {
Ok(bound) => bound,
Err(e) => {
tracing::error!(path = %pipe_path.display(), "failed to bind named pipe: {e}");
return;
}
};
let identifier = guard.identifier.clone();
if let Err(e) = accept_loop(first_server, &identifier, engine, eval_fn, list_fn, press_hooks, recorder).await {
tracing::error!("named pipe server error: {e}");
}
}
async fn accept_loop(
first_server: NamedPipeServer, identifier: &str, engine: EvalEngine, eval_fn: Option<EvalFn>,
list_fn: Option<ListWindowsFn>, press_hooks: Option<PressHooksRef>, recorder: Recorder,
) -> Result<(), Error> {
let ctx = Arc::new((engine, eval_fn, list_fn, press_hooks, recorder));
let mut server = first_server;
let pipe_path = socket_path(identifier);
loop {
server.connect().await?;
let next_server = loop {
let create_result: std::io::Result<NamedPipeServer> = (|| {
let (mut sa, _sec_guard) = create_user_only_security_attributes()?;
unsafe {
ServerOptions::new()
.pipe_mode(tokio::net::windows::named_pipe::PipeMode::Byte)
.create_with_security_attributes_raw(&pipe_path, (&raw mut sa).cast::<c_void>())
}
.map_err(std::io::Error::other)
})();
match create_result {
Ok(s) => break s,
Err(e) => {
tracing::warn!(
path = %pipe_path.display(),
error = %e,
"transient failure creating next pipe instance, retrying"
);
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
};
let current = server;
server = next_server;
if !client_sid_matches_current_user(¤t) {
tracing::warn!("client SID does not match current user, closing connection");
continue;
}
let ctx = Arc::clone(&ctx);
tokio::spawn(async move {
if let Err(e) =
handle_connection(current, &ctx.0, ctx.1.as_ref(), ctx.2.as_ref(), ctx.3.as_ref(), &ctx.4).await
{
tracing::warn!("connection error: {e}");
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::Response;
use serial_test::serial;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::windows::named_pipe::ClientOptions;
static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_pipe_path() -> PathBuf {
let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let name = format!("tauri-hasgard-test-{}-{n}", std::process::id());
PathBuf::from(format!(r"\\.\pipe\{name}"))
}
async fn start_test_server(path: &Path) -> tokio::task::JoinHandle<()> {
let engine = EvalEngine::new();
let path = path.to_path_buf();
let handle = tokio::spawn(async move {
run(path, engine, None, None, None, Recorder::new()).await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
handle
}
#[tokio::test]
#[serial]
async fn test_server_responds_ping_ok() {
let pipe = unique_pipe_path();
let handle = start_test_server(&pipe).await;
let client = ClientOptions::new().open(&pipe).expect("open test pipe");
let (reader, mut writer) = tokio::io::split(client);
let mut reader = BufReader::new(reader);
writer.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n").await.expect("write ping request");
writer.flush().await.expect("flush");
let mut line = String::new();
reader.read_line(&mut line).await.expect("read response");
let resp: Response = serde_json::from_str(&line).expect("parse response");
assert_eq!(resp.id, serde_json::json!(1));
assert!(resp.error.is_none());
let result = resp.result.expect("ping returns a result");
assert_eq!(result["status"], serde_json::json!("ok"));
assert_eq!(result["plugin_version"], serde_json::json!(env!("CARGO_PKG_VERSION")));
handle.abort();
let _ = handle.await;
}
#[tokio::test]
#[serial]
async fn test_server_handles_invalid_json() {
let pipe = unique_pipe_path();
let handle = start_test_server(&pipe).await;
let client = ClientOptions::new().open(&pipe).expect("open test pipe");
let (reader, mut writer) = tokio::io::split(client);
let mut reader = BufReader::new(reader);
writer.write_all(b"not json\n").await.expect("write invalid request");
writer.flush().await.expect("flush");
let mut line = String::new();
reader.read_line(&mut line).await.expect("read response");
let resp: Response = serde_json::from_str(&line).expect("parse response");
assert_eq!(resp.id, serde_json::Value::Null);
let err = resp.error.expect("error payload present");
assert_eq!(err.code, -32700);
handle.abort();
let _ = handle.await;
}
#[tokio::test]
#[serial]
async fn test_server_handles_multiple_requests() {
let pipe = unique_pipe_path();
let handle = start_test_server(&pipe).await;
let client = ClientOptions::new().open(&pipe).expect("open test pipe");
let (reader, mut writer) = tokio::io::split(client);
let mut reader = BufReader::new(reader);
for i in 1..=3 {
let req = format!("{{\"jsonrpc\":\"2.0\",\"id\":{i},\"method\":\"test\"}}\n");
writer.write_all(req.as_bytes()).await.expect("write request");
writer.flush().await.expect("flush");
let mut line = String::new();
reader.read_line(&mut line).await.expect("read response");
let resp: Response = serde_json::from_str(&line).expect("parse response");
assert_eq!(resp.id, serde_json::json!(i));
}
handle.abort();
let _ = handle.await;
}
#[tokio::test]
#[serial]
#[cfg(windows)]
async fn test_run_returns_when_bind_fails_instead_of_panicking() {
let pipe = unique_pipe_path();
let holder = start_test_server(&pipe).await;
let dup_path = pipe.clone();
let dup = tokio::spawn(async move {
run(dup_path, EvalEngine::new(), None, None, None, Recorder::new()).await;
});
let joined = tokio::time::timeout(Duration::from_secs(5), dup)
.await
.expect("run must return after a failed bind (#115), not hang");
assert!(joined.is_ok(), "run must not panic when binding fails (#115)");
holder.abort();
let _ = holder.await;
}
#[tokio::test]
#[serial]
#[cfg(windows)]
async fn test_bound_pipe_carries_user_only_dacl() {
use windows::Win32::Foundation::LocalFree;
use windows::Win32::Security::Authorization::{GetSecurityInfo, SE_KERNEL_OBJECT};
use windows::Win32::Security::{
ACL_SIZE_INFORMATION, AclSizeInformation, DACL_SECURITY_INFORMATION, GetAclInformation,
};
let pipe = unique_pipe_path();
let (server, guard) = bind(&pipe).expect("bind test pipe");
let raw_handle = server.as_raw_handle();
let handle = HANDLE(raw_handle);
let mut dacl_ptr: *mut ACL = std::ptr::null_mut();
let mut sd_ptr = PSECURITY_DESCRIPTOR::default();
unsafe {
GetSecurityInfo(
handle,
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
None,
None,
Some(&raw mut dacl_ptr),
None,
Some(&raw mut sd_ptr),
)
}
.ok()
.expect("GetSecurityInfo must succeed on a bound pipe");
assert!(!dacl_ptr.is_null(), "bound pipe must carry a non-NULL DACL");
let mut info = ACL_SIZE_INFORMATION::default();
let info_size =
u32::try_from(std::mem::size_of::<ACL_SIZE_INFORMATION>()).expect("ACL_SIZE_INFORMATION fits in u32");
unsafe { GetAclInformation(dacl_ptr, (&raw mut info).cast::<c_void>(), info_size, AclSizeInformation) }
.expect("GetAclInformation must succeed");
assert_eq!(info.AceCount, 1, "bound pipe DACL must contain exactly one ACE (owner-only)");
unsafe {
let _ = LocalFree(Some(windows::Win32::Foundation::HLOCAL(sd_ptr.0)));
}
drop(server);
drop(guard);
}
}