mermaid_runtime/
daemon.rs1#[cfg(unix)]
2use std::io::{BufRead, BufReader, Write};
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use base64::{Engine as _, engine::general_purpose};
7use serde::de::DeserializeOwned;
8use sha2::{Digest, Sha256};
9
10use crate::data_dir;
11
12pub const DAEMON_TOKEN_ENV: &str = "MERMAID_DAEMON_TOKEN";
13
14pub const DEFAULT_PAIRING_TTL_DAYS: i64 = 30;
18
19pub fn pairing_expiry_from_now(ttl_days: i64) -> Option<String> {
23 (ttl_days > 0).then(|| (chrono::Utc::now() + chrono::Duration::days(ttl_days)).to_rfc3339())
24}
25
26pub fn clamp_pairing_ttl_days(ttl_days: i64) -> i64 {
32 if ttl_days <= 0 {
33 DEFAULT_PAIRING_TTL_DAYS
34 } else {
35 ttl_days
36 }
37}
38
39pub fn daemon_socket_path() -> Result<PathBuf> {
40 Ok(data_dir()?.join("mermaidd.sock"))
41}
42
43pub fn generate_pairing_token() -> Result<(String, String)> {
44 let mut bytes = [0_u8; 32];
45 getrandom::fill(&mut bytes)
46 .map_err(|err| anyhow::anyhow!("failed to generate pairing token: {}", err))?;
47 let token = format!("mermaid_{}", general_purpose::URL_SAFE_NO_PAD.encode(bytes));
48 let hash = hash_pairing_token(&token);
49 Ok((token, hash))
50}
51
52pub fn hash_pairing_token(token: &str) -> String {
53 let digest = Sha256::digest(token.as_bytes());
54 hex_encode(&digest)
55}
56
57pub fn request_daemon_json(mut body: serde_json::Value) -> Result<serde_json::Value> {
58 if body.get("auth").is_none()
59 && let Ok(token) = std::env::var(DAEMON_TOKEN_ENV)
60 && !token.trim().is_empty()
61 {
62 body["auth"] = serde_json::json!({ "token": token });
63 }
64 request_daemon_text(&body.to_string())
65}
66
67pub fn request_daemon_text(line: &str) -> Result<serde_json::Value> {
68 #[cfg(unix)]
69 {
70 use std::os::unix::net::UnixStream;
71
72 let socket = daemon_socket_path()?;
73 let mut stream = UnixStream::connect(&socket)
74 .with_context(|| format!("failed to connect to {}", socket.display()))?;
75 stream.write_all(line.as_bytes())?;
76 stream.write_all(b"\n")?;
77 stream.flush()?;
78
79 let mut response = String::new();
80 let mut reader = BufReader::new(stream);
81 reader.read_line(&mut response)?;
82 let value: serde_json::Value =
83 serde_json::from_str(response.trim()).context("daemon returned invalid JSON")?;
84 if value.get("ok").and_then(|v| v.as_bool()) == Some(false) {
85 anyhow::bail!(
86 "{}",
87 value
88 .get("error")
89 .and_then(|v| v.as_str())
90 .unwrap_or("daemon request failed")
91 );
92 }
93 Ok(value)
94 }
95
96 #[cfg(not(unix))]
97 {
98 let _ = line;
99 anyhow::bail!("daemon IPC currently supports Unix sockets only")
100 }
101}
102
103pub fn snapshot_field_from_daemon<T: DeserializeOwned>(field: &str) -> Result<T> {
104 let value = request_daemon_json(serde_json::json!({ "command": "snapshot" }))?;
105 let field_value = value
106 .get(field)
107 .cloned()
108 .with_context(|| format!("daemon snapshot missing `{}`", field))?;
109 serde_json::from_value(field_value)
110 .with_context(|| format!("daemon snapshot field `{}` had unexpected shape", field))
111}
112
113fn hex_encode(bytes: &[u8]) -> String {
114 const HEX: &[u8; 16] = b"0123456789abcdef";
115 let mut out = String::with_capacity(bytes.len() * 2);
116 for byte in bytes {
117 out.push(HEX[(byte >> 4) as usize] as char);
118 out.push(HEX[(byte & 0x0f) as usize] as char);
119 }
120 out
121}
122
123#[cfg(test)]
124mod tests {
125 use crate::*;
126
127 #[test]
128 fn pairing_token_hash_is_stable_and_not_plaintext() {
129 let hash = hash_pairing_token("mermaid_test");
130 assert_eq!(hash, hash_pairing_token("mermaid_test"));
131 assert_ne!(hash, "mermaid_test");
132 assert_eq!(hash.len(), 64);
133 }
134
135 #[test]
136 fn generated_pairing_token_hash_matches_token() {
137 let (token, hash) = generate_pairing_token().expect("token");
138 assert!(token.starts_with("mermaid_"));
139 assert_eq!(hash, hash_pairing_token(&token));
140 }
141
142 #[test]
143 fn clamp_pairing_ttl_days_forces_expiry_for_non_positive() {
144 assert_eq!(clamp_pairing_ttl_days(0), DEFAULT_PAIRING_TTL_DAYS);
145 assert_eq!(clamp_pairing_ttl_days(-5), DEFAULT_PAIRING_TTL_DAYS);
146 assert_eq!(clamp_pairing_ttl_days(7), 7);
147 assert!(pairing_expiry_from_now(clamp_pairing_ttl_days(0)).is_some());
150 assert!(pairing_expiry_from_now(clamp_pairing_ttl_days(-1)).is_some());
151 }
152}