Skip to main content

mermaid_runtime/
daemon.rs

1use 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}