1mod device;
4mod label;
5mod role;
6mod team;
7
8use std::{
9 fmt::Debug,
10 io,
11 path::Path,
12 time::{Duration, Instant},
13};
14
15use anyhow::Context as _;
16use aranya_crypto::{Csprng, Rng};
17#[doc(inline)]
18pub use aranya_daemon_api::ChanOp;
19use aranya_daemon_api::{
20 crypto::{
21 txp::{self, LengthDelimitedCodec},
22 PublicApiKey,
23 },
24 DaemonApiClient, Version, CS,
25};
26#[cfg(feature = "preview")]
27#[cfg_attr(docsrs, doc(cfg(feature = "preview")))]
28#[doc(inline)]
29pub use aranya_daemon_api::{
30 RoleManagementPerm as RoleManagementPermission, SimplePerm as Permission,
31};
32use aranya_util::{error::ReportExt, Addr};
33use tarpc::context;
34use tokio::{fs, net::UnixStream};
35use tracing::{debug, error, info};
36#[cfg(feature = "afc")]
37use {
38 crate::afc::{ChannelKeys as AfcChannelKeys, Channels as AfcChannels},
39 std::sync::Arc,
40};
41
42#[doc(inline)]
43#[expect(deprecated)]
44pub use self::device::KeyBundle;
45#[doc(inline)]
46pub use self::{
47 device::{Device, DeviceId, Devices, PublicKeyBundle},
48 label::{Label, LabelId, Labels},
49 role::{Role, RoleId, Roles},
50 team::{Team, TeamId},
51};
52use crate::{
53 config::{AddTeamConfig, CreateTeamConfig},
54 error::{self, aranya_error, InvalidArg, IpcError, Result},
55 util::ApiConv as _,
56};
57
58const IPC_TIMEOUT: Duration = Duration::from_secs(365 * 24 * 60 * 60);
62
63#[derive(Debug, Default)]
65pub struct ClientBuilder<'a> {
66 #[cfg(unix)]
68 daemon_uds_path: Option<&'a Path>,
69}
70
71impl ClientBuilder<'_> {
72 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub async fn connect(self) -> Result<Client> {
92 let Some(uds_path) = self.daemon_uds_path else {
93 return Err(IpcError::new(InvalidArg::new(
94 "daemon_uds_path",
95 "must specify the daemon's UDS path",
96 ))
97 .into());
98 };
99
100 async {
101 info!(path = ?uds_path, "connecting to daemon");
102
103 let daemon = {
104 let pk = {
105 let api_pk_path = uds_path.parent().unwrap_or(uds_path).join("api.pk");
107 let bytes = fs::read(&api_pk_path)
108 .await
109 .with_context(|| "unable to read daemon API public key")
110 .map_err(IpcError::new)?;
111 PublicApiKey::<CS>::decode(&bytes)
112 .context("unable to decode public API key")
113 .map_err(IpcError::new)?
114 };
115
116 let uds_path = uds_path
117 .canonicalize()
118 .context("could not canonicalize uds_path")
119 .map_err(error::other)?;
120 let sock = UnixStream::connect(&uds_path)
121 .await
122 .context("unable to connect to UDS path")
123 .map_err(IpcError::new)?;
124 let info = uds_path.as_os_str().as_encoded_bytes();
125 let codec = LengthDelimitedCodec::builder()
126 .max_frame_length(usize::MAX)
127 .new_codec();
128 let transport = txp::client(sock, codec, Rng, pk, info);
129
130 DaemonApiClient::new(tarpc::client::Config::default(), transport).spawn()
131 };
132 debug!("connected to daemon");
133
134 let got = daemon
135 .version(create_ctx())
136 .await
137 .map_err(IpcError::new)?
138 .context("unable to retrieve daemon version")
139 .map_err(error::other)?;
140 let want = Version::parse(env!("CARGO_PKG_VERSION"))
141 .context("unable to parse `CARGO_PKG_VERSION`")
142 .map_err(error::other)?;
143 if got.major != want.major || got.minor != want.minor {
144 return Err(IpcError::new(io::Error::new(
145 io::ErrorKind::Unsupported,
146 format!("version mismatch: `{got}` != `{want}`"),
147 ))
148 .into());
149 }
150 debug!(client = ?want, daemon = ?got, "versions");
151
152 #[cfg(feature = "afc")]
153 let afc_keys = {
154 let afc_shm_info = daemon
155 .afc_shm_info(create_ctx())
156 .await
157 .map_err(IpcError::new)?
158 .context("unable to retrieve afc shm info")
159 .map_err(error::other)?;
160 Arc::new(AfcChannelKeys::new(&afc_shm_info)?)
161 };
162
163 let client = Client {
164 daemon,
165 #[cfg(feature = "afc")]
166 afc_keys,
167 };
168
169 Ok(client)
170 }
171 .await
172 .inspect_err(
173 |err: &crate::Error| error!(error = %err.report(), "unable to connect to daemon"),
174 )
175 }
176}
177
178impl<'a> ClientBuilder<'a> {
179 #[cfg(unix)]
181 #[cfg_attr(docsrs, doc(cfg(unix)))]
182 pub fn with_daemon_uds_path(mut self, sock: &'a Path) -> Self {
183 self.daemon_uds_path = Some(sock);
184 self
185 }
186}
187
188#[derive(Debug)]
196pub struct Client {
197 pub(crate) daemon: DaemonApiClient,
199 #[cfg(feature = "afc")]
201 afc_keys: Arc<AfcChannelKeys>,
202}
203
204impl Client {
205 pub fn builder<'a>() -> ClientBuilder<'a> {
207 ClientBuilder::new()
208 }
209
210 pub async fn local_addr(&self) -> Result<Addr> {
212 self.daemon
213 .aranya_local_addr(create_ctx())
214 .await
215 .map_err(IpcError::new)?
216 .map_err(aranya_error)
217 }
218
219 #[deprecated(note = "Use `get_public_key_bundle`")]
221 pub async fn get_key_bundle(&self) -> Result<PublicKeyBundle> {
222 self.get_public_key_bundle().await
223 }
224
225 pub async fn get_public_key_bundle(&self) -> Result<PublicKeyBundle> {
227 self.daemon
228 .get_public_key_bundle(create_ctx())
229 .await
230 .map_err(IpcError::new)?
231 .map_err(aranya_error)
232 .map(PublicKeyBundle::from_api)
233 }
234
235 pub async fn get_device_id(&self) -> Result<DeviceId> {
237 self.daemon
238 .get_device_id(create_ctx())
239 .await
240 .map_err(IpcError::new)?
241 .map_err(aranya_error)
242 .map(DeviceId::from_api)
243 }
244
245 pub async fn create_team(&self, cfg: CreateTeamConfig) -> Result<Team<'_>> {
247 let team_id = self
248 .daemon
249 .create_team(create_ctx(), cfg.into())
250 .await
251 .map_err(IpcError::new)?
252 .map_err(aranya_error)
253 .map(TeamId::from_api)?;
254 Ok(Team {
255 client: self,
256 id: team_id.into_api(),
257 })
258 }
259
260 pub async fn rand(&self, buf: &mut [u8]) {
263 <Rng as Csprng>::fill_bytes(&Rng, buf);
264 }
265
266 pub fn team(&self, team_id: TeamId) -> Team<'_> {
268 Team {
269 client: self,
270 id: team_id.into_api(),
271 }
272 }
273
274 pub async fn add_team(&self, cfg: AddTeamConfig) -> Result<Team<'_>> {
276 let cfg = aranya_daemon_api::AddTeamConfig::from(cfg);
277 let team_id = TeamId::from_api(cfg.team_id);
278
279 self.daemon
280 .add_team(create_ctx(), cfg)
281 .await
282 .map_err(IpcError::new)?
283 .map_err(aranya_error)?;
284 Ok(Team {
285 client: self,
286 id: team_id.into_api(),
287 })
288 }
289
290 pub async fn remove_team(&self, team_id: TeamId) -> Result<()> {
292 self.daemon
293 .remove_team(create_ctx(), team_id.into_api())
294 .await
295 .map_err(IpcError::new)?
296 .map_err(aranya_error)
297 }
298
299 #[cfg(feature = "afc")]
301 #[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
302 pub fn afc(&self) -> AfcChannels {
303 AfcChannels::new(self.daemon.clone(), self.afc_keys.clone())
304 }
305}
306
307pub(crate) fn create_ctx() -> context::Context {
310 let mut ctx = context::current();
311 ctx.deadline = Instant::now()
312 .checked_add(IPC_TIMEOUT)
313 .expect("IPC_TIMEOUT should not overflow");
314
315 ctx
316}