use std::{
collections::HashMap,
os::unix::prelude::{MetadataExt, PermissionsExt},
path::{Path, PathBuf},
time,
};
use anyhow::{bail, Context};
use bytesize::ByteSize;
use nix::{sys::stat, unistd};
use serde::{de::Error as SerdeError, Deserialize, Deserializer};
use url::Url;
use crate::{
common::non_nul_string::NonNulString, npk::manifest::console::Permissions,
runtime::repository::RepositoryId,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub run_dir: PathBuf,
pub data_dir: PathBuf,
pub socket_dir: PathBuf,
pub cgroup: NonNulString,
#[serde(default = "default_event_buffer_size")]
pub event_buffer_size: usize,
#[serde(default = "default_notification_buffer_size")]
pub notification_buffer_size: usize,
#[serde(default)]
pub console: Console,
#[serde(with = "humantime_serde", default = "default_loop_device_timeout")]
pub loop_device_timeout: time::Duration,
#[serde(default)]
pub repositories: HashMap<RepositoryId, Repository>,
pub debug: Option<Debug>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ConsoleGlobal {
#[serde(deserialize_with = "console_url")]
pub bind: Url,
pub permissions: Permissions,
pub options: Option<ConsoleOptions>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConsoleOptions {
#[serde(with = "humantime_serde", default = "default_token_validity")]
pub token_validity: time::Duration,
#[serde(default = "default_max_requests_per_second")]
pub max_requests_per_sec: usize,
#[serde(deserialize_with = "bytesize", default = "default_max_request_size")]
pub max_request_size: u64,
#[serde(
deserialize_with = "bytesize",
default = "default_max_npk_install_size"
)]
pub max_npk_install_size: u64,
#[serde(with = "humantime_serde", default = "default_npk_stream_timeout")]
pub npk_stream_timeout: time::Duration,
}
impl Default for ConsoleOptions {
fn default() -> Self {
Self {
token_validity: default_token_validity(),
max_requests_per_sec: default_max_requests_per_second(),
max_request_size: default_max_request_size(),
max_npk_install_size: default_max_npk_install_size(),
npk_stream_timeout: default_npk_stream_timeout(),
}
}
}
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Console {
pub global: Option<ConsoleGlobal>,
pub options: Option<ConsoleOptions>,
}
#[derive(Clone, Debug, Deserialize)]
pub enum RepositoryType {
#[serde(rename = "fs")]
Fs {
dir: PathBuf,
},
#[serde(rename = "mem")]
Memory,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Repository {
pub r#type: RepositoryType,
pub key: Option<PathBuf>,
#[serde(default)]
pub mount_on_start: bool,
pub capacity_num: Option<u32>,
#[serde(default, deserialize_with = "bytesize")]
pub capacity_size: Option<u64>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Debug {
#[serde(default)]
pub commands: Vec<String>,
}
impl Config {
pub(crate) fn check(&self) -> anyhow::Result<()> {
check_rw_directory(&self.run_dir).context("checking run_dir")?;
check_rw_directory(&self.data_dir).context("checking data_dir")?;
check_rw_directory(&self.socket_dir).context("checking socket_dir")?;
Ok(())
}
}
fn check_rw_directory(path: &Path) -> anyhow::Result<()> {
if !path.exists() {
bail!("{} does not exist", path.display());
} else if !is_rw(path) {
bail!("{} is not read and/or writeable", path.display());
} else {
Ok(())
}
}
fn is_rw(path: &Path) -> bool {
match std::fs::metadata(path) {
Ok(stat) => {
let same_uid = stat.uid() == unistd::getuid().as_raw();
let same_gid = stat.gid() == unistd::getgid().as_raw();
let mode = stat::Mode::from_bits_truncate(stat.permissions().mode());
let is_readable = (same_uid && mode.contains(stat::Mode::S_IRUSR))
|| (same_gid && mode.contains(stat::Mode::S_IRGRP))
|| mode.contains(stat::Mode::S_IROTH);
let is_writable = (same_uid && mode.contains(stat::Mode::S_IWUSR))
|| (same_gid && mode.contains(stat::Mode::S_IWGRP))
|| mode.contains(stat::Mode::S_IWOTH);
is_readable && is_writable
}
Err(_) => false,
}
}
fn console_url<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
let url = Url::deserialize(deserializer)?;
if url.scheme() != "tcp" && url.scheme() != "unix" {
Err(D::Error::custom("console scheme must be tcp or unix"))
} else {
Ok(url)
}
}
fn bytesize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: From<u64>,
{
String::deserialize(deserializer)
.and_then(|s| s.parse::<ByteSize>().map_err(D::Error::custom))
.map(|s| s.as_u64().into())
}
const fn default_loop_device_timeout() -> time::Duration {
time::Duration::from_secs(10)
}
const fn default_event_buffer_size() -> usize {
256
}
const fn default_notification_buffer_size() -> usize {
128
}
const fn default_token_validity() -> time::Duration {
time::Duration::from_secs(60)
}
const fn default_max_requests_per_second() -> usize {
1000
}
const fn default_max_npk_install_size() -> u64 {
256 * 1024 * 1024
}
const fn default_npk_stream_timeout() -> time::Duration {
time::Duration::from_secs(10)
}
const fn default_max_request_size() -> u64 {
1024 * 1024
}
#[test]
#[allow(clippy::unwrap_used)]
fn validate_console_url() {
let config = r#"
data_dir = "target/northstar/data"
run_dir = "target/northstar/run"
socket_dir = "target/northstar/sockets"
cgroup = "northstar"
[console.global]
bind = "tcp://localhost:4200"
permissions = "full"
"#;
toml::from_str::<Config>(config).unwrap();
let config = r#"
data_dir = "target/northstar/data"
run_dir = "target/northstar/run"
socket_dir = "target/northstar/sockets"
cgroup = "northstar"
[console.global]
bind = "http://localhost:4200"
permissions = "full"
"#;
assert!(toml::from_str::<Config>(config).is_err());
}
#[test]
fn repository_size() {
let config = r#"
data_dir = "target/northstar/data"
run_dir = "target/northstar/run"
socket_dir = "target/northstar/sockets"
cgroup = "northstar"
[repositories.memory]
type = "mem"
key = "examples/northstar.pub"
capacity_num = 10
capacity_size = "100MB"
"#;
let config = toml::from_str::<Config>(config).expect("failed to parse config");
let memory = config
.repositories
.get("memory")
.expect("failed to find memory repository");
assert_eq!(memory.key, Some("examples/northstar.pub".into()));
assert_eq!(memory.capacity_num, Some(10));
assert_eq!(memory.capacity_size, Some(100000000));
}