northstar_runtime/runtime/
config.rs1use std::{
2 collections::HashMap,
3 os::unix::prelude::{MetadataExt, PermissionsExt},
4 path::{Path, PathBuf},
5 time,
6};
7
8use anyhow::{bail, Context};
9use bytesize::ByteSize;
10use nix::{sys::stat, unistd};
11use serde::{de::Error as SerdeError, Deserialize, Deserializer};
12use url::Url;
13
14use crate::{
15 common::non_nul_string::NonNulString, npk::manifest::console::Permissions,
16 runtime::repository::RepositoryId,
17};
18
19#[derive(Clone, Debug, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Config {
23 pub run_dir: PathBuf,
25 pub data_dir: PathBuf,
27 pub socket_dir: PathBuf,
29 pub cgroup: NonNulString,
31 #[serde(default = "default_event_buffer_size")]
33 pub event_buffer_size: usize,
34 #[serde(default = "default_notification_buffer_size")]
36 pub notification_buffer_size: usize,
37 #[serde(default)]
39 pub console: Console,
40 #[serde(with = "humantime_serde", default = "default_loop_device_timeout")]
42 pub loop_device_timeout: time::Duration,
43 #[serde(default)]
45 pub repositories: HashMap<RepositoryId, Repository>,
46 pub debug: Option<Debug>,
48}
49
50#[derive(Clone, Debug, Deserialize)]
52pub struct ConsoleGlobal {
53 #[serde(deserialize_with = "console_url")]
55 pub bind: Url,
56 pub permissions: Permissions,
58 pub options: Option<ConsoleOptions>,
60}
61
62#[derive(Clone, Debug, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct ConsoleOptions {
66 #[serde(with = "humantime_serde", default = "default_token_validity")]
68 pub token_validity: time::Duration,
69 #[serde(default = "default_max_requests_per_second")]
71 pub max_requests_per_sec: usize,
72 #[serde(deserialize_with = "bytesize", default = "default_max_request_size")]
74 pub max_request_size: u64,
75 #[serde(
77 deserialize_with = "bytesize",
78 default = "default_max_npk_install_size"
79 )]
80 pub max_npk_install_size: u64,
81 #[serde(with = "humantime_serde", default = "default_npk_stream_timeout")]
83 pub npk_stream_timeout: time::Duration,
84}
85
86impl Default for ConsoleOptions {
87 fn default() -> Self {
88 Self {
89 token_validity: default_token_validity(),
90 max_requests_per_sec: default_max_requests_per_second(),
91 max_request_size: default_max_request_size(),
92 max_npk_install_size: default_max_npk_install_size(),
93 npk_stream_timeout: default_npk_stream_timeout(),
94 }
95 }
96}
97
98#[derive(Clone, Default, Debug, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct Console {
102 pub global: Option<ConsoleGlobal>,
104 pub options: Option<ConsoleOptions>,
106}
107
108#[derive(Clone, Debug, Deserialize)]
110pub enum RepositoryType {
111 #[serde(rename = "fs")]
113 Fs {
114 dir: PathBuf,
116 },
117 #[serde(rename = "mem")]
119 Memory,
120}
121
122#[derive(Clone, Debug, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct Repository {
126 pub r#type: RepositoryType,
128 pub key: Option<PathBuf>,
130 #[serde(default)]
132 pub mount_on_start: bool,
133 pub capacity_num: Option<u32>,
135 #[serde(default, deserialize_with = "bytesize")]
137 pub capacity_size: Option<u64>,
138}
139
140#[derive(Clone, Debug, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct Debug {
144 #[serde(default)]
148 pub commands: Vec<String>,
149}
150
151impl Config {
152 pub(crate) fn check(&self) -> anyhow::Result<()> {
154 check_rw_directory(&self.run_dir).context("checking run_dir")?;
155 check_rw_directory(&self.data_dir).context("checking data_dir")?;
156 check_rw_directory(&self.socket_dir).context("checking socket_dir")?;
157 Ok(())
158 }
159}
160
161fn check_rw_directory(path: &Path) -> anyhow::Result<()> {
163 if !path.exists() {
164 bail!("{} does not exist", path.display());
165 } else if !is_rw(path) {
166 bail!("{} is not read and/or writeable", path.display());
167 } else {
168 Ok(())
169 }
170}
171
172fn is_rw(path: &Path) -> bool {
174 match std::fs::metadata(path) {
175 Ok(stat) => {
176 let same_uid = stat.uid() == unistd::getuid().as_raw();
177 let same_gid = stat.gid() == unistd::getgid().as_raw();
178 let mode = stat::Mode::from_bits_truncate(stat.permissions().mode());
179
180 let is_readable = (same_uid && mode.contains(stat::Mode::S_IRUSR))
181 || (same_gid && mode.contains(stat::Mode::S_IRGRP))
182 || mode.contains(stat::Mode::S_IROTH);
183 let is_writable = (same_uid && mode.contains(stat::Mode::S_IWUSR))
184 || (same_gid && mode.contains(stat::Mode::S_IWGRP))
185 || mode.contains(stat::Mode::S_IWOTH);
186
187 is_readable && is_writable
188 }
189 Err(_) => false,
190 }
191}
192
193fn console_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
195where
196 D: Deserializer<'de>,
197{
198 let url = Url::deserialize(deserializer)?;
199 if url.scheme() != "tcp" && url.scheme() != "unix" {
200 Err(D::Error::custom("console scheme must be tcp or unix"))
201 } else {
202 Ok(url)
203 }
204}
205
206fn bytesize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
208where
209 D: Deserializer<'de>,
210 T: From<u64>,
211{
212 String::deserialize(deserializer)
213 .and_then(|s| s.parse::<ByteSize>().map_err(D::Error::custom))
214 .map(|s| s.as_u64().into())
215}
216
217const fn default_loop_device_timeout() -> time::Duration {
219 time::Duration::from_secs(10)
220}
221
222const fn default_event_buffer_size() -> usize {
224 256
225}
226
227const fn default_notification_buffer_size() -> usize {
229 128
230}
231
232const fn default_token_validity() -> time::Duration {
234 time::Duration::from_secs(60)
235}
236
237const fn default_max_requests_per_second() -> usize {
239 1000
240}
241
242const fn default_max_npk_install_size() -> u64 {
244 256 * 1024 * 1024
245}
246const fn default_npk_stream_timeout() -> time::Duration {
248 time::Duration::from_secs(10)
249}
250
251const fn default_max_request_size() -> u64 {
253 1024 * 1024
254}
255
256#[test]
257#[allow(clippy::unwrap_used)]
258fn validate_console_url() {
259 let config = r#"
260data_dir = "target/northstar/data"
261run_dir = "target/northstar/run"
262socket_dir = "target/northstar/sockets"
263cgroup = "northstar"
264[console.global]
265bind = "tcp://localhost:4200"
266permissions = "full"
267"#;
268
269 toml::from_str::<Config>(config).unwrap();
270
271 let config = r#"
273data_dir = "target/northstar/data"
274run_dir = "target/northstar/run"
275socket_dir = "target/northstar/sockets"
276cgroup = "northstar"
277[console.global]
278bind = "http://localhost:4200"
279permissions = "full"
280"#;
281
282 assert!(toml::from_str::<Config>(config).is_err());
283}
284
285#[test]
286fn repository_size() {
287 let config = r#"
288data_dir = "target/northstar/data"
289run_dir = "target/northstar/run"
290socket_dir = "target/northstar/sockets"
291cgroup = "northstar"
292
293[repositories.memory]
294type = "mem"
295key = "examples/northstar.pub"
296capacity_num = 10
297capacity_size = "100MB"
298"#;
299 let config = toml::from_str::<Config>(config).expect("failed to parse config");
300 let memory = config
301 .repositories
302 .get("memory")
303 .expect("failed to find memory repository");
304 assert_eq!(memory.key, Some("examples/northstar.pub".into()));
305 assert_eq!(memory.capacity_num, Some(10));
306 assert_eq!(memory.capacity_size, Some(100000000));
307}