sentinelpass_protocol/
client.rs1use crate::envelope::{IpcEnvelope, Origin};
4use crate::error::ProtocolError;
5use crate::message::IpcMessage;
6use crate::service::{ServiceOutcome, VaultOp, VaultOpResult};
7use crate::token::load_ipc_token;
8use crate::Result;
9use std::path::PathBuf;
10
11#[cfg(windows)]
12use tracing::debug;
13
14pub struct IpcClient {
16 socket_path: PathBuf,
17 auth_token: String,
18 client_token: Option<String>,
21 origin: Option<Origin>,
23 capability: Option<String>,
26}
27
28impl IpcClient {
29 pub fn new(socket_path: PathBuf) -> Result<Self> {
31 let auth_token = load_ipc_token()?;
32 Ok(Self::new_with_token(socket_path, auth_token))
33 }
34
35 pub fn new_with_token(socket_path: PathBuf, auth_token: String) -> Self {
37 Self {
38 socket_path,
39 auth_token,
40 client_token: None,
41 origin: None,
42 capability: None,
43 }
44 }
45
46 pub fn new_for_cli(socket_path: PathBuf, client_token: Option<String>) -> Result<Self> {
48 let auth_token = load_ipc_token()?;
49 Ok(Self {
50 socket_path,
51 auth_token,
52 client_token,
53 origin: Some(Origin::Cli),
54 capability: None,
55 })
56 }
57
58 pub fn with_context(mut self, client_token: Option<String>, origin: Option<Origin>) -> Self {
61 self.client_token = client_token;
62 self.origin = origin;
63 self
64 }
65
66 pub fn with_capability(mut self, capability: Option<String>) -> Self {
68 self.capability = capability;
69 self
70 }
71
72 pub fn new_for_native_host(socket_path: PathBuf) -> Result<Self> {
77 let auth_token = load_ipc_token()?;
78 Ok(Self {
79 socket_path,
80 auth_token,
81 client_token: None,
82 origin: Some(Origin::NativeHost),
83 capability: crate::token::load_native_host_capability(),
84 })
85 }
86
87 pub async fn send(&self, msg: IpcMessage) -> Result<IpcMessage> {
92 let envelope = IpcEnvelope {
93 token: self.auth_token.clone(),
94 client_token: self.client_token.clone(),
95 origin: self.origin,
96 capability: self.capability.clone(),
97 message: msg,
98 };
99 let msg_bytes = serde_json::to_vec(&envelope)
100 .map_err(|e| ProtocolError::Ipc(format!("Failed to serialize message: {}", e)))?;
101
102 #[cfg(unix)]
104 let transport_conn = {
105 crate::transport::unix::UnixSocketConnection::connect(self.socket_path.clone())
106 .await
107 .map_err(|e| ProtocolError::Ipc(format!("Failed to connect to daemon: {}", e)))?
108 };
109
110 #[cfg(windows)]
111 let transport_conn = {
112 let stored = self.socket_path.to_string_lossy().to_string();
117 let pipe_name = if stored.starts_with(r"\\.\pipe\") {
118 stored
119 } else {
120 crate::windows_frame::windows_named_pipe_path()
121 };
122 debug!("Connecting to named pipe: {}", pipe_name);
123 crate::transport::windows::connect_named_pipe(&pipe_name, 3000)
124 .await
125 .map_err(|e| {
126 ProtocolError::Ipc(format!("Failed to connect to named pipe: {}", e))
127 })?
128 };
129
130 let conn = crate::connection::TransportConnection::from(transport_conn);
132 let mut ipc = crate::connection::IpcConnection::connect_client(conn, &self.auth_token)
133 .await
134 .map_err(|e| ProtocolError::Ipc(format!("Session negotiation failed: {}", e)))?;
135
136 ipc.send_frame(&msg_bytes)
137 .await
138 .map_err(|e| ProtocolError::Ipc(format!("Failed to write message: {}", e)))?;
139
140 let buffer = ipc
141 .recv_frame()
142 .await
143 .map_err(|e| ProtocolError::Ipc(format!("Failed to read response: {}", e)))?;
144
145 serde_json::from_slice::<IpcMessage>(&buffer)
146 .map_err(|e| ProtocolError::Ipc(format!("Failed to parse response: {}", e)))
147 }
148
149 pub async fn call_service(&self, op: VaultOp) -> Result<VaultOpResult> {
153 let response = self.send(IpcMessage::ServiceCall { op }).await?;
154 match response {
155 IpcMessage::ServiceResult { outcome } => match outcome {
156 ServiceOutcome::Ok { result } => Ok(result),
157 ServiceOutcome::Err { error } => {
158 Err(ProtocolError::Service(error.code, error.message))
159 }
160 },
161 other => Err(ProtocolError::Ipc(format!(
162 "unexpected daemon response to service call: {}",
163 message_kind(&other)
164 ))),
165 }
166 }
167}
168
169fn message_kind(msg: &IpcMessage) -> &'static str {
172 match msg {
173 IpcMessage::GetCredentialResponse { .. }
174 | IpcMessage::GetExternalSecretResponse { .. }
175 | IpcMessage::ListDomainCredentialsResponse { .. }
176 | IpcMessage::GetTotpCodeResponse { .. }
177 | IpcMessage::SaveCredentialResponse { .. }
178 | IpcMessage::SaveSecretResponse { .. }
179 | IpcMessage::DeleteSecretResponse { .. } => "browser-surface response",
180 IpcMessage::UnlockVaultResponse { .. } => "unlock response",
181 IpcMessage::VaultStatusResponse { .. } => "vault status",
182 IpcMessage::SyncNowResponse { .. } => "sync-now response",
183 IpcMessage::SyncStatusResponse { .. } => "sync status",
184 _ => "other",
185 }
186}