294 lines
9.6 KiB
Rust
294 lines
9.6 KiB
Rust
//! `brokerd` configuration: read `brokerd.toml` into a typed `Config`.
|
|
//!
|
|
//! This is our own format, so unknown keys are errors in every table: a
|
|
//! misspelt key that silently fell back to its default would be a setting the
|
|
//! owner believes is set and is not.
|
|
|
|
use serde::Deserialize;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
#[serde(deny_unknown_fields, default)]
|
|
pub struct Paths {
|
|
pub home: PathBuf,
|
|
pub grants: PathBuf,
|
|
}
|
|
|
|
impl Default for Paths {
|
|
fn default() -> Self {
|
|
Self {
|
|
home: std::env::var_os("BOXMAKER_HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| PathBuf::from("/var/lib/boxmaker")),
|
|
grants: PathBuf::from("/etc/boxmaker/grants"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An empty path means "the default under `home`".
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields, default)]
|
|
pub struct Sockets {
|
|
pub broker: PathBuf,
|
|
pub admin: PathBuf,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
#[serde(deny_unknown_fields, default)]
|
|
pub struct Approvals {
|
|
pub ttl_ms: u64,
|
|
}
|
|
|
|
impl Default for Approvals {
|
|
fn default() -> Self {
|
|
Self { ttl_ms: 900_000 }
|
|
}
|
|
}
|
|
|
|
/// `[gateway]`: where `gatewayd` takes approvals to post in Mattermost. Without the section,
|
|
/// Mattermost approvals are off and only `bxctl` answers. An empty path means the default.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields, default)]
|
|
pub struct Gateway {
|
|
pub socket: PathBuf,
|
|
}
|
|
|
|
fn default_podman() -> PathBuf {
|
|
PathBuf::from("podman")
|
|
}
|
|
fn default_egress_network() -> String {
|
|
"pasta".to_string()
|
|
}
|
|
fn default_output_cap() -> u64 {
|
|
262_144
|
|
}
|
|
fn default_memory() -> String {
|
|
"512m".to_string()
|
|
}
|
|
fn default_pids() -> u32 {
|
|
128
|
|
}
|
|
fn default_read_file_ms() -> u64 {
|
|
30_000
|
|
}
|
|
fn default_write_file_ms() -> u64 {
|
|
30_000
|
|
}
|
|
fn default_shell_ms() -> u64 {
|
|
100_000
|
|
}
|
|
fn default_http_fetch_ms() -> u64 {
|
|
60_000
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Runner {
|
|
#[serde(default = "default_podman")]
|
|
pub podman: PathBuf,
|
|
pub image: String,
|
|
#[serde(default = "default_egress_network")]
|
|
pub egress_network: String,
|
|
#[serde(default = "default_output_cap")]
|
|
pub output_cap: u64,
|
|
#[serde(default = "default_memory")]
|
|
pub memory: String,
|
|
#[serde(default = "default_pids")]
|
|
pub pids: u32,
|
|
#[serde(default = "default_read_file_ms")]
|
|
pub read_file_ms: u64,
|
|
#[serde(default = "default_write_file_ms")]
|
|
pub write_file_ms: u64,
|
|
#[serde(default = "default_shell_ms")]
|
|
pub shell_ms: u64,
|
|
#[serde(default = "default_http_fetch_ms")]
|
|
pub http_fetch_ms: u64,
|
|
}
|
|
|
|
impl Runner {
|
|
/// The time limit for one call of `tool`: the matching `_ms` field.
|
|
pub fn time_limit(&self, tool: crate::args::ToolName) -> std::time::Duration {
|
|
let ms = match tool {
|
|
crate::args::ToolName::ReadFile => self.read_file_ms,
|
|
crate::args::ToolName::WriteFile => self.write_file_ms,
|
|
crate::args::ToolName::Shell => self.shell_ms,
|
|
crate::args::ToolName::HttpFetch => self.http_fetch_ms,
|
|
};
|
|
std::time::Duration::from_millis(ms)
|
|
}
|
|
}
|
|
|
|
/// `memory` is well formed if it is one or more digits followed by `b`, `k`, `m` or `g`.
|
|
fn valid_memory(memory: &str) -> bool {
|
|
let bytes = memory.as_bytes();
|
|
bytes.len() >= 2
|
|
&& matches!(bytes[bytes.len() - 1], b'b' | b'k' | b'm' | b'g')
|
|
&& bytes[..bytes.len() - 1].iter().all(|&c| c.is_ascii_digit())
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Config {
|
|
#[serde(default)]
|
|
pub paths: Paths,
|
|
#[serde(default)]
|
|
pub sockets: Sockets,
|
|
#[serde(default)]
|
|
pub approvals: Approvals,
|
|
#[serde(default)]
|
|
pub runner: Option<Runner>,
|
|
#[serde(default)]
|
|
pub gateway: Option<Gateway>,
|
|
}
|
|
|
|
/// The longest an approval may wait: a day, the longest `loopd` waits after a pending frame.
|
|
pub const MAX_TTL_MS: u64 = 86_400_000;
|
|
|
|
#[derive(Debug)]
|
|
pub enum ConfigError {
|
|
Read(PathBuf, std::io::Error),
|
|
Parse(PathBuf, toml::de::Error),
|
|
/// The file parses, but a value is outside what brokerd accepts.
|
|
Invalid(PathBuf, String),
|
|
}
|
|
|
|
impl std::fmt::Display for ConfigError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
ConfigError::Read(path, err) => write!(f, "{}: {err}", path.display()),
|
|
ConfigError::Parse(path, err) => write!(f, "{}: {err}", path.display()),
|
|
ConfigError::Invalid(path, why) => write!(f, "{}: {why}", path.display()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ConfigError {}
|
|
|
|
impl Config {
|
|
pub fn parse(text: &str) -> Result<Config, toml::de::Error> {
|
|
toml::from_str(text)
|
|
}
|
|
pub fn load(path: &Path) -> Result<Config, ConfigError> {
|
|
let text =
|
|
std::fs::read_to_string(path).map_err(|e| ConfigError::Read(path.to_path_buf(), e))?;
|
|
let config: Config =
|
|
toml::from_str(&text).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))?;
|
|
// `loopd` waits at most a day after a pending frame, so a longer approval would be given up
|
|
// by the caller while `bxctl approvals` still listed it.
|
|
if !(1..=MAX_TTL_MS).contains(&config.approvals.ttl_ms) {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
format!(
|
|
"[approvals] ttl_ms is {}; it must be from 1 to {MAX_TTL_MS} (a day)",
|
|
config.approvals.ttl_ms
|
|
),
|
|
));
|
|
}
|
|
if let Some(runner) = &config.runner {
|
|
let where_image = format!(
|
|
"[runner] image is {:?}; it must be named by digest: <name>@sha256:<64 hex digits>",
|
|
runner.image
|
|
);
|
|
let (name, hex) = match runner.image.rsplit_once("@sha256:") {
|
|
Some(pair) => pair,
|
|
None => {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
where_image.clone(),
|
|
));
|
|
}
|
|
};
|
|
if name.is_empty()
|
|
|| hex.len() != 64
|
|
|| !hex.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
|
|
{
|
|
return Err(ConfigError::Invalid(path.to_path_buf(), where_image));
|
|
}
|
|
if !valid_memory(&runner.memory) {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
format!(
|
|
"[runner] memory is {:?}; it must be a number and one of b, k, m, g",
|
|
runner.memory
|
|
),
|
|
));
|
|
}
|
|
if runner.egress_network.is_empty() || runner.podman.as_os_str().is_empty() {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] podman and egress_network must not be empty".to_string(),
|
|
));
|
|
}
|
|
if runner.output_cap == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] output_cap must be at least 1".to_string(),
|
|
));
|
|
}
|
|
if runner.pids == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] pids must be at least 1".to_string(),
|
|
));
|
|
}
|
|
if runner.read_file_ms == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] read_file_ms must be at least 1".to_string(),
|
|
));
|
|
}
|
|
if runner.write_file_ms == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] write_file_ms must be at least 1".to_string(),
|
|
));
|
|
}
|
|
if runner.shell_ms == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] shell_ms must be at least 1".to_string(),
|
|
));
|
|
}
|
|
if runner.http_fetch_ms == 0 {
|
|
return Err(ConfigError::Invalid(
|
|
path.to_path_buf(),
|
|
"[runner] http_fetch_ms must be at least 1".to_string(),
|
|
));
|
|
}
|
|
}
|
|
Ok(config)
|
|
}
|
|
pub fn broker_socket(&self) -> PathBuf {
|
|
if self.sockets.broker.as_os_str().is_empty() {
|
|
self.paths.home.join("run/loop-broker/broker.sock")
|
|
} else {
|
|
self.sockets.broker.clone()
|
|
}
|
|
}
|
|
pub fn admin_socket(&self) -> PathBuf {
|
|
if self.sockets.admin.as_os_str().is_empty() {
|
|
self.paths.home.join("run/owner-broker/admin.sock")
|
|
} else {
|
|
self.sockets.admin.clone()
|
|
}
|
|
}
|
|
/// `gateway.sock`, when `[gateway]` is set.
|
|
pub fn gateway_socket(&self) -> Option<PathBuf> {
|
|
let gateway = self.gateway.as_ref()?;
|
|
if gateway.socket.as_os_str().is_empty() {
|
|
Some(self.paths.home.join("run/gateway/gateway.sock"))
|
|
} else {
|
|
Some(gateway.socket.clone())
|
|
}
|
|
}
|
|
pub fn audit_dir(&self) -> PathBuf {
|
|
self.paths.home.join("audit")
|
|
}
|
|
pub fn state_dir(&self) -> PathBuf {
|
|
self.paths.home.join("broker/sessions")
|
|
}
|
|
pub fn egress_dir(&self) -> PathBuf {
|
|
self.paths.home.join("run/egress")
|
|
}
|
|
}
|