1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
//! Daemon configuration.
use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
#[cfg(feature = "afc")]
use aranya_fast_channels::shm;
use aranya_util::Addr;
use serde::{
de::{self, DeserializeOwned},
Deserialize, Serialize,
};
mod toggle;
pub use toggle::Toggle;
/// Options for configuring the daemon.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Config {
/// The name of the daemon, used for logging and debugging
/// purposes.
pub name: String,
/// The directory where the daemon stores non-essential
/// runtime files and other file objects (sockets, etc.).
///
/// # Multiple Daemon Support
///
/// This directory should be unique for each instance of the
/// daemon.
///
/// # Example
///
/// For example, this could be `/var/run/aranya`.
///
/// See also: systemd `RuntimeDirectory=` and
/// `$XDG_RUNTIME_DIR`.
#[serde(deserialize_with = "non_empty_path")]
pub runtime_dir: PathBuf,
/// The directory where the daemon stores non-portable data
/// that should persist between application restarts.
///
/// # Multiple Daemon Support
///
/// This directory should be unique for each instance of the
/// daemon.
///
/// # Example
///
/// For example, this could be `/var/lib/aranya`.
///
/// See also: systemd `StateDirectory=` and
/// `$XDG_STATE_HOME`.
#[serde(deserialize_with = "non_empty_path")]
pub state_dir: PathBuf,
/// The directory where the daemon stores non-essential data
/// files.
///
/// # Multiple Daemon Support
///
/// This directory should be unique for each instance of the
/// daemon.
///
/// # Example
///
/// For example, this could be `/var/cache/aranya`.
///
/// See also: systemd `CacheDirectory=` and
/// `$XDG_CACHE_HOME`.
#[serde(deserialize_with = "non_empty_path")]
pub cache_dir: PathBuf,
/// The directory where the daemon writes log files.
///
/// # Multiple Daemon Support
///
/// This directory should be unique for each instance of the
/// daemon.
///
/// # Example
///
/// For example, this could be `/var/log/aranya`.
///
/// See also: systemd `LogsDirectory=`.
#[serde(deserialize_with = "non_empty_path")]
pub logs_dir: PathBuf,
/// The directory where the daemon can find additional
/// configuration files.
///
/// # Multiple Daemon Support
///
/// This directory should be unique for each instance of the
/// daemon.
///
/// # Example
///
/// For example, this could be `/etc/aranya`.
///
/// See also: systemd `ConfigDirectory=` and
/// `$XDG_CONFIG_HOME`.
#[serde(deserialize_with = "non_empty_path")]
pub config_dir: PathBuf,
/// AFC configuration.
#[cfg(feature = "afc")]
#[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
#[serde(default)]
pub afc: Toggle<AfcConfig>,
/// QUIC syncer config
#[serde(default)]
pub sync: SyncConfig,
}
impl Config {
/// Reads the configuration from `path`.
pub fn load<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let cfg: Self = read_toml(path.as_ref())
.with_context(|| format!("unable to parse config: {}", path.as_ref().display()))?;
Ok(cfg)
}
/// Path to the PID file.
pub fn pid_path(&self) -> PathBuf {
self.runtime_dir.join("daemon.pid")
}
/// Path to the [`DefaultEngine`]'s key wrapping key.
pub(crate) fn key_wrap_key_path(&self) -> PathBuf {
self.state_dir.join("key_wrap_key")
}
/// Path to the [`PublicKeyBundle`].
pub(crate) fn public_key_bundle_path(&self) -> PathBuf {
self.state_dir.join("key_bundle")
}
/// The directory where keystore files are written.
pub(crate) fn keystore_path(&self) -> PathBuf {
self.state_dir.join("keystore")
}
/// The directory where the root keystore exists.
///
/// The Aranaya keystore contains Aranya's key material.
pub(crate) fn aranya_keystore_path(&self) -> PathBuf {
self.keystore_path().join("aranya")
}
/// The directory where the local keystore exists.
///
/// The local keystore contains key material for the daemon.
/// E.g., its API key.
pub(crate) fn local_keystore_path(&self) -> PathBuf {
self.keystore_path().join("local")
}
/// Path to the runtime's storage.
pub(crate) fn storage_path(&self) -> PathBuf {
self.state_dir.join("storage")
}
/// Path to file containing the seed IDs.
pub(crate) fn seed_id_path(&self) -> PathBuf {
self.state_dir.join("seeds")
}
/// Path to the daemon's UDS API socket.
pub fn uds_api_sock(&self) -> PathBuf {
self.runtime_dir.join("uds.sock")
}
/// Path to the daemon's API public key.
pub fn api_pk_path(&self) -> PathBuf {
self.runtime_dir.join("api.pk")
}
}
/// Reads TOML from `path`.
fn read_toml<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
let buf = fs::read_to_string(path.as_ref())?;
Ok(toml::from_str(&buf)?)
}
/// Sync configuration
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SyncConfig {
/// QUIC syncer config
#[serde(default)]
pub quic: Toggle<QuicSyncConfig>,
}
/// AFC configuration.
#[cfg(feature = "afc")]
#[cfg_attr(docsrs, doc(cfg(feature = "afc")))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AfcConfig {
/// Shared memory path.
pub shm_path: Box<shm::Path>,
/// Maximum number of channels AFC should support.
pub max_chans: usize,
}
/// QUIC syncer configuration.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QuicSyncConfig {
/// Network address of Aranya sync server.
pub addr: Addr,
/// Client bind address.
pub client_addr: Option<Addr>,
}
fn non_empty_path<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
if path.components().next().is_none() {
Err(de::Error::custom("path cannot be empty"))
} else {
Ok(path)
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::net::Ipv4Addr;
use pretty_assertions::assert_eq;
use toml::toml;
use super::*;
#[test]
fn test_example_config() -> Result<()> {
const DIR: &str = env!("CARGO_MANIFEST_DIR");
let path = Path::new(DIR).join("example.toml");
let got = Config::load(path)?;
let want = Config {
name: "my-aranya-daemon".into(),
runtime_dir: "/var/run/aranya".parse()?,
state_dir: "/var/lib/aranya".parse()?,
cache_dir: "/var/cache/aranya".parse()?,
logs_dir: "/var/log/aranya".parse()?,
config_dir: "/etc/aranya".parse()?,
sync: SyncConfig {
quic: Toggle::Enabled(QuicSyncConfig {
addr: Addr::from((Ipv4Addr::UNSPECIFIED, 4321)),
client_addr: None,
}),
},
#[cfg(feature = "afc")]
afc: Toggle::Enabled(AfcConfig {
shm_path: "/afc\0"
.try_into()
.context("unable to parse AFC shared memory path")?,
max_chans: 100,
}),
};
assert_eq!(got, want);
Ok(())
}
#[test]
fn test_config() {
#![allow(clippy::disallowed_macros, reason = "toml! uses unreachable!")]
// Missing a required field.
let data = toml! {
name = "aranya"
runtime_dir = "/var/run/aranya"
state_dir = "/var/lib/aranya"
logs_dir = "/var/log/aranya"
config_dir = "/etc/aranya"
};
data.try_into::<Config>()
.expect_err("missing `cache_dir` should be rejected");
// A required field is empty.
let data = toml! {
name = "aranya"
runtime_dir = "/var/run/aranya"
state_dir = "/var/lib/aranya"
cache_dir = ""
logs_dir = "/var/log/aranya"
config_dir = "/etc/aranya"
};
data.try_into::<Config>()
.expect_err("empty `cache_dir` should be rejected");
}
}