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