#[cfg(any(unix, windows))]
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use base64::{Engine as _, engine::general_purpose};
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use crate::data_dir;
pub const DAEMON_TOKEN_ENV: &str = "MERMAID_DAEMON_TOKEN";
pub const DEFAULT_PAIRING_TTL_DAYS: i64 = 30;
pub fn pairing_expiry_from_now(ttl_days: i64) -> Option<String> {
(ttl_days > 0).then(|| (chrono::Utc::now() + chrono::Duration::days(ttl_days)).to_rfc3339())
}
pub fn clamp_pairing_ttl_days(ttl_days: i64) -> i64 {
if ttl_days <= 0 {
DEFAULT_PAIRING_TTL_DAYS
} else {
ttl_days
}
}
pub fn daemon_socket_path() -> Result<PathBuf> {
Ok(data_dir()?.join("mermaidd.sock"))
}
pub fn generate_pairing_token() -> Result<(String, String)> {
let mut bytes = [0_u8; 32];
getrandom::fill(&mut bytes)
.map_err(|err| anyhow::anyhow!("failed to generate pairing token: {}", err))?;
let token = format!("mermaid_{}", general_purpose::URL_SAFE_NO_PAD.encode(bytes));
let hash = hash_pairing_token(&token);
Ok((token, hash))
}
pub fn hash_pairing_token(token: &str) -> String {
let digest = Sha256::digest(token.as_bytes());
crate::hex_lower(&digest)
}
pub fn request_daemon_json(mut body: serde_json::Value) -> Result<serde_json::Value> {
if body.get("auth").is_none()
&& let Ok(token) = std::env::var(DAEMON_TOKEN_ENV)
&& !token.trim().is_empty()
{
body["auth"] = serde_json::json!({ "token": token });
}
request_daemon_text(&body.to_string())
}
pub fn request_daemon_text(line: &str) -> Result<serde_json::Value> {
#[cfg(unix)]
{
use std::os::unix::net::UnixStream;
let socket = daemon_socket_path()?;
let mut stream = UnixStream::connect(&socket)
.with_context(|| format!("failed to connect to {}", socket.display()))?;
stream.write_all(line.as_bytes())?;
stream.write_all(b"\n")?;
stream.flush()?;
let mut response = String::new();
let mut reader = BufReader::new(stream);
reader.read_line(&mut response)?;
let value: serde_json::Value =
serde_json::from_str(response.trim()).context("daemon returned invalid JSON")?;
if value.get("ok").and_then(|v| v.as_bool()) == Some(false) {
anyhow::bail!(
"{}",
value
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("daemon request failed")
);
}
Ok(value)
}
#[cfg(windows)]
{
let pipe_name = daemon_pipe_name()?;
let stream = open_daemon_pipe(&pipe_name)?;
let mut stream = stream;
stream.write_all(line.as_bytes())?;
stream.write_all(b"\n")?;
stream.flush()?;
let mut response = String::new();
let mut reader = BufReader::new(stream);
reader.read_line(&mut response)?;
let value: serde_json::Value =
serde_json::from_str(response.trim()).context("daemon returned invalid JSON")?;
if value.get("ok").and_then(|v| v.as_bool()) == Some(false) {
anyhow::bail!(
"{}",
value
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("daemon request failed")
);
}
Ok(value)
}
#[cfg(not(any(unix, windows)))]
{
let _ = line;
anyhow::bail!("daemon IPC supports Unix sockets and Windows named pipes only")
}
}
pub fn pipe_name_for_sid(sid: &str) -> String {
format!(r"\\.\pipe\mermaidd-{sid}")
}
pub fn pipe_sddl(sid: &str) -> String {
format!("D:P(A;;GA;;;SY)(A;;GA;;;{sid})")
}
#[cfg(windows)]
pub fn current_user_sid() -> Result<String> {
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, LocalFree};
use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
unsafe {
let mut token: HANDLE = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
anyhow::bail!("OpenProcessToken failed (error {})", GetLastError());
}
let result = (|| {
let mut needed: u32 = 0;
GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut needed);
anyhow::ensure!(
needed > 0,
"GetTokenInformation sizing call failed (error {})",
GetLastError()
);
let mut buf = vec![0_u8; needed as usize];
if GetTokenInformation(
token,
TokenUser,
buf.as_mut_ptr().cast(),
needed,
&mut needed,
) == 0
{
anyhow::bail!("GetTokenInformation failed (error {})", GetLastError());
}
let user = &*(buf.as_ptr() as *const TOKEN_USER);
let mut sid_w: *mut u16 = std::ptr::null_mut();
if ConvertSidToStringSidW(user.User.Sid, &mut sid_w) == 0 {
anyhow::bail!("ConvertSidToStringSidW failed (error {})", GetLastError());
}
let mut len = 0_usize;
while *sid_w.add(len) != 0 {
len += 1;
}
let sid = String::from_utf16_lossy(std::slice::from_raw_parts(sid_w, len));
LocalFree(sid_w.cast());
Ok(sid)
})();
CloseHandle(token);
result
}
}
#[cfg(windows)]
pub fn daemon_pipe_name() -> Result<String> {
Ok(pipe_name_for_sid(¤t_user_sid()?))
}
#[cfg(windows)]
pub struct PipeSecurity {
descriptor: windows_sys::Win32::Security::PSECURITY_DESCRIPTOR,
attributes: windows_sys::Win32::Security::SECURITY_ATTRIBUTES,
}
#[cfg(windows)]
impl PipeSecurity {
pub fn owner_only() -> Result<Self> {
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
};
use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
let sddl = pipe_sddl(¤t_user_sid()?);
let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
if unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
wide.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
std::ptr::null_mut(),
)
} == 0
{
anyhow::bail!(
"failed to build pipe security descriptor from `{}` (error {})",
sddl,
unsafe { GetLastError() }
);
}
let attributes = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: descriptor,
bInheritHandle: 0,
};
Ok(Self {
descriptor,
attributes,
})
}
pub fn attributes_ptr(&mut self) -> *mut core::ffi::c_void {
(&raw mut self.attributes).cast()
}
}
#[cfg(windows)]
impl Drop for PipeSecurity {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::Foundation::LocalFree(self.descriptor.cast());
}
}
}
#[cfg(windows)]
fn open_daemon_pipe(pipe_name: &str) -> Result<std::fs::File> {
const ATTEMPTS: u32 = 5;
for attempt in 1..=ATTEMPTS {
match std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(pipe_name)
{
Ok(file) => return Ok(file),
Err(err)
if err.raw_os_error()
== Some(windows_sys::Win32::Foundation::ERROR_PIPE_BUSY as i32)
&& attempt < ATTEMPTS =>
{
std::thread::sleep(std::time::Duration::from_millis(50));
},
Err(err) => {
return Err(err).with_context(|| {
format!("failed to connect to {pipe_name} (is mermaidd running?)")
});
},
}
}
anyhow::bail!("daemon pipe {pipe_name} stayed busy after {ATTEMPTS} attempts")
}
pub fn snapshot_field_from_daemon<T: DeserializeOwned>(field: &str) -> Result<T> {
let value = request_daemon_json(serde_json::json!({ "command": "snapshot" }))?;
let field_value = value
.get(field)
.cloned()
.with_context(|| format!("daemon snapshot missing `{}`", field))?;
serde_json::from_value(field_value)
.with_context(|| format!("daemon snapshot field `{}` had unexpected shape", field))
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn pairing_token_hash_is_stable_and_not_plaintext() {
let hash = hash_pairing_token("mermaid_test");
assert_eq!(hash, hash_pairing_token("mermaid_test"));
assert_ne!(hash, "mermaid_test");
assert_eq!(hash.len(), 64);
}
#[test]
fn generated_pairing_token_hash_matches_token() {
let (token, hash) = generate_pairing_token().expect("token");
assert!(token.starts_with("mermaid_"));
assert_eq!(hash, hash_pairing_token(&token));
}
#[test]
fn clamp_pairing_ttl_days_forces_expiry_for_non_positive() {
assert_eq!(clamp_pairing_ttl_days(0), DEFAULT_PAIRING_TTL_DAYS);
assert_eq!(clamp_pairing_ttl_days(-5), DEFAULT_PAIRING_TTL_DAYS);
assert_eq!(clamp_pairing_ttl_days(7), 7);
assert!(pairing_expiry_from_now(clamp_pairing_ttl_days(0)).is_some());
assert!(pairing_expiry_from_now(clamp_pairing_ttl_days(-1)).is_some());
}
#[test]
fn pipe_name_and_sddl_embed_the_sid() {
let sid = "S-1-5-21-1-2-3-1000";
assert_eq!(
super::pipe_name_for_sid(sid),
r"\\.\pipe\mermaidd-S-1-5-21-1-2-3-1000"
);
let sddl = super::pipe_sddl(sid);
assert_eq!(sddl, "D:P(A;;GA;;;SY)(A;;GA;;;S-1-5-21-1-2-3-1000)");
}
#[cfg(windows)]
#[test]
fn current_user_sid_and_pipe_security_resolve() {
let sid = super::current_user_sid().expect("current_user_sid");
assert!(sid.starts_with("S-1-"), "unexpected SID shape: {sid}");
let mut security = super::PipeSecurity::owner_only().expect("PipeSecurity");
assert!(!security.attributes_ptr().is_null());
assert!(super::daemon_pipe_name().expect("pipe name").contains(&sid));
}
}