Skip to main content

sentinelpass_protocol/
client.rs

1//! IPC client — sends messages to the daemon.
2
3use crate::envelope::{IpcEnvelope, Origin};
4use crate::error::ProtocolError;
5use crate::message::IpcMessage;
6use crate::token::load_ipc_token;
7use crate::Result;
8use std::path::PathBuf;
9#[allow(unused_imports)]
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11#[cfg(windows)]
12use tracing::debug;
13
14/// IPC client for daemon communication
15pub struct IpcClient {
16    socket_path: PathBuf,
17    auth_token: String,
18    /// Per-client grant token (SENTINELPASS_CLIENT_TOKEN); sent on every
19    /// request so the daemon can enforce token-scoped grants.
20    client_token: Option<String>,
21    /// Provenance label for this process (native host / CLI).
22    origin: Option<Origin>,
23}
24
25impl IpcClient {
26    /// Create a new IPC client
27    pub fn new(socket_path: PathBuf) -> Result<Self> {
28        let auth_token = load_ipc_token()?;
29        Ok(Self::new_with_token(socket_path, auth_token))
30    }
31
32    /// Create a new IPC client with an explicit auth token.
33    pub fn new_with_token(socket_path: PathBuf, auth_token: String) -> Self {
34        Self {
35            socket_path,
36            auth_token,
37            client_token: None,
38            origin: None,
39        }
40    }
41
42    /// CLI client carrying a per-client grant token for external secret access.
43    pub fn new_for_cli(socket_path: PathBuf, client_token: Option<String>) -> Result<Self> {
44        let auth_token = load_ipc_token()?;
45        Ok(Self {
46            socket_path,
47            auth_token,
48            client_token,
49            origin: Some(Origin::Cli),
50        })
51    }
52
53    /// Override the per-client grant token and origin label. Intended for
54    /// embedders that construct the daemon token explicitly (tests, hosts).
55    pub fn with_context(mut self, client_token: Option<String>, origin: Option<Origin>) -> Self {
56        self.client_token = client_token;
57        self.origin = origin;
58        self
59    }
60
61    /// Browser native-messaging host client.
62    pub fn new_for_native_host(socket_path: PathBuf) -> Result<Self> {
63        let auth_token = load_ipc_token()?;
64        Ok(Self {
65            socket_path,
66            auth_token,
67            client_token: None,
68            origin: Some(Origin::NativeHost),
69        })
70    }
71
72    /// Send a message and wait for response
73    #[allow(unused_variables)]
74    pub async fn send(&self, msg: IpcMessage) -> Result<IpcMessage> {
75        #[cfg(unix)]
76        {
77            // Use Unix socket transport
78            let mut conn =
79                crate::transport::unix::UnixSocketConnection::connect(self.socket_path.clone())
80                    .await
81                    .map_err(|e| {
82                        ProtocolError::Ipc(format!("Failed to connect to daemon: {}", e))
83                    })?;
84
85            let envelope = IpcEnvelope {
86                token: self.auth_token.clone(),
87                client_token: self.client_token.clone(),
88                origin: self.origin,
89                message: msg,
90            };
91            let msg_bytes = serde_json::to_vec(&envelope)
92                .map_err(|e| ProtocolError::Ipc(format!("Failed to serialize message: {}", e)))?;
93
94            conn.write_message(&msg_bytes)
95                .await
96                .map_err(|e| ProtocolError::Ipc(format!("Failed to write message: {}", e)))?;
97
98            // Read response
99            let buffer = conn
100                .read_message()
101                .await
102                .map_err(|e| ProtocolError::Ipc(format!("Failed to read response: {}", e)))?;
103
104            serde_json::from_slice::<IpcMessage>(&buffer)
105                .map_err(|e| ProtocolError::Ipc(format!("Failed to parse response: {}", e)))
106        }
107
108        #[cfg(windows)]
109        {
110            // Determine if using named pipes or legacy TCP
111            let path_str = self.socket_path.to_string_lossy().to_string();
112            let use_tcp = path_str.starts_with("tcp://");
113
114            if use_tcp {
115                // Legacy TCP fallback for custom tcp://... paths
116                use tokio::net::TcpStream;
117
118                let addr_str = path_str.strip_prefix("tcp://").unwrap_or("127.0.0.1:35873");
119
120                // Connect to TCP socket with bounded retries
121                let connect_deadline =
122                    tokio::time::Instant::now() + tokio::time::Duration::from_secs(3);
123                let mut stream = loop {
124                    match TcpStream::connect(addr_str).await {
125                        Ok(s) => break s,
126                        Err(e) => {
127                            if e.kind() == std::io::ErrorKind::ConnectionRefused {
128                                if tokio::time::Instant::now() >= connect_deadline {
129                                    return Err(ProtocolError::Ipc(format!(
130                                        "Failed to connect to daemon at {}: timed out after 3s",
131                                        addr_str
132                                    )));
133                                }
134                                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
135                                continue;
136                            }
137                            return Err(ProtocolError::Ipc(format!(
138                                "Failed to connect to daemon: {}",
139                                e
140                            )));
141                        }
142                    }
143                };
144
145                let envelope = IpcEnvelope {
146                    token: self.auth_token.clone(),
147                    client_token: self.client_token.clone(),
148                    origin: self.origin,
149                    message: msg,
150                };
151                let msg_bytes = serde_json::to_vec(&envelope).map_err(|e| {
152                    ProtocolError::Ipc(format!("Failed to serialize message: {}", e))
153                })?;
154                let msg_bytes =
155                    crate::windows_frame::encrypt_windows_ipc_frame(&self.auth_token, &msg_bytes)?;
156
157                let length = msg_bytes.len() as u32;
158
159                stream
160                    .write_all(&length.to_be_bytes())
161                    .await
162                    .map_err(|e| ProtocolError::Ipc(format!("Failed to write length: {}", e)))?;
163
164                stream
165                    .write_all(&msg_bytes)
166                    .await
167                    .map_err(|e| ProtocolError::Ipc(format!("Failed to write message: {}", e)))?;
168
169                stream
170                    .flush()
171                    .await
172                    .map_err(|e| ProtocolError::Ipc(format!("Failed to flush: {}", e)))?;
173
174                // Read response
175                let mut length_buf = [0u8; 4];
176                stream
177                    .read_exact(&mut length_buf)
178                    .await
179                    .map_err(|e| ProtocolError::Ipc(format!("Failed to read length: {}", e)))?;
180
181                let response_length = u32::from_be_bytes(length_buf) as usize;
182
183                if response_length > 65536 {
184                    return Err(ProtocolError::Ipc("Response too large".to_string()));
185                }
186
187                let mut buffer = vec![0u8; response_length];
188                stream
189                    .read_exact(&mut buffer)
190                    .await
191                    .map_err(|e| ProtocolError::Ipc(format!("Failed to read response: {}", e)))?;
192
193                let buffer =
194                    crate::windows_frame::decrypt_windows_ipc_frame(&self.auth_token, &buffer)?;
195
196                serde_json::from_slice::<IpcMessage>(&buffer)
197                    .map_err(|e| ProtocolError::Ipc(format!("Failed to parse response: {}", e)))
198            } else {
199                // Default: Use named pipes
200                let pipe_name = crate::windows_frame::windows_named_pipe_path();
201                debug!("Connecting to named pipe: {}", pipe_name);
202
203                let mut conn = crate::transport::windows::connect_named_pipe(&pipe_name, 3000)
204                    .await
205                    .map_err(|e| {
206                        ProtocolError::Ipc(format!("Failed to connect to named pipe: {}", e))
207                    })?;
208
209                let envelope = IpcEnvelope {
210                    token: self.auth_token.clone(),
211                    client_token: self.client_token.clone(),
212                    origin: self.origin,
213                    message: msg,
214                };
215                let msg_bytes = serde_json::to_vec(&envelope).map_err(|e| {
216                    ProtocolError::Ipc(format!("Failed to serialize message: {}", e))
217                })?;
218                let msg_bytes =
219                    crate::windows_frame::encrypt_windows_ipc_frame(&self.auth_token, &msg_bytes)?;
220
221                conn.write_message(&msg_bytes)
222                    .await
223                    .map_err(|e| ProtocolError::Ipc(format!("Failed to write message: {}", e)))?;
224
225                // Read response
226                let buffer = conn
227                    .read_message()
228                    .await
229                    .map_err(|e| ProtocolError::Ipc(format!("Failed to read response: {}", e)))?;
230
231                let buffer =
232                    crate::windows_frame::decrypt_windows_ipc_frame(&self.auth_token, &buffer)?;
233
234                serde_json::from_slice::<IpcMessage>(&buffer)
235                    .map_err(|e| ProtocolError::Ipc(format!("Failed to parse response: {}", e)))
236            }
237        }
238    }
239}