Skip to content

Commit

Permalink
working on better configuration management
Browse files Browse the repository at this point in the history
  • Loading branch information
jgraef committed Jul 11, 2024
1 parent 4f87b32 commit adb4abd
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 10 deletions.
2 changes: 2 additions & 0 deletions skunk-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dirs = "5.0.1"
dotenvy = "0.15.7"
futures-util = "0.3.30"
mime = "0.3.17"
murmur3 = "0.5.2"
notify = { version = "6.1.1", default-features = false, features = ["fsevent-sys", "macos_fsevent"] }
parking_lot = "0.12.3"
rmp-serde = "1.3.0"
Expand All @@ -49,6 +50,7 @@ serde = { version = "1.0.201", features = ["derive"] }
thiserror = "1.0.61"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "signal"] }
toml = "0.8.12"
toml_edit = { version = "0.22.15", features = ["serde"] }
tower-http = { version = "0.5.2", features = ["fs"] }
tower-layer = "0.3.2"
tower-service = "0.3.2"
Expand Down
24 changes: 24 additions & 0 deletions skunk-cli/src/api/capture.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
use std::{
collections::BTreeMap,
net::SocketAddr,
};

use axum::Router;
use serde::{
Deserialize,
Serialize,
};

use super::Context;

pub(crate) fn router() -> Router<Context> {
todo!();
}

#[derive(Debug)]
pub struct Captures {
inner: BTreeMap<Id, ()>,
}

impl Captures {}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Id {
SocksProxy { bind_address: SocketAddr },
HttpProxy { bind_address: SocketAddr },
PacketCapture { interface: String },
}
201 changes: 201 additions & 0 deletions skunk-cli/src/config_new/config_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use std::{
borrow::Cow,
path::{
Path,
PathBuf,
},
sync::Arc,
time::Duration,
};

use serde::Deserialize;
use tokio::sync::RwLock;
use toml_edit::DocumentMut;
use tracing::Instrument;

use super::{
Error,
FileHash,
DEFAULT_CONFIG,
};
use crate::util::watch::{
FileWatcher,
WatchModified,
};

#[derive(Clone, Debug)]
pub struct ConfigFile {
inner: Arc<RwLock<Inner>>,
}

pub struct Loader {
inner: Inner,
watch: WatchModified,
hash: FileHash,
}

#[derive(Debug, Deserialize)]
pub struct Data {
data_dir: Option<PathBuf>,
}

#[derive(Debug)]
struct Inner {
path: PathBuf,
hash: FileHash,
document: DocumentMut,
data: Data,
}

impl ConfigFile {
const DEBOUNCE: Duration = Duration::from_secs(1);

pub fn open(path: impl AsRef<Path>) -> Result<Loader, Error> {
open(path.as_ref())
}
}

impl Loader {
pub fn load(self) -> ConfigFile {
let path = self.inner.path.clone();
let inner = Arc::new(RwLock::new(self.inner));

let span = tracing::info_span!("watch-config");
tokio::spawn(
WatchConfig {
inner: inner.clone(),
watch: self.watch,
path,
hash: self.hash,
}
.run()
.instrument(span),
);

ConfigFile { inner }
}

pub fn data_dir(&self) -> Option<&Path> {
self.inner.data.data_dir.as_deref()
}
}

fn open(path: &Path) -> Result<Loader, Error> {
let mut toml: Option<Cow<'static, str>> = None;

if !path.exists() {
std::fs::write(path, DEFAULT_CONFIG).map_err(|error| {
Error::WriteFile {
error,
path: path.to_owned(),
}
})?;

toml = Some(DEFAULT_CONFIG.into());
}

let watcher = FileWatcher::new().map_err(|error| {
Error::WatchFile {
error,
path: path.to_owned(),
}
})?;

let watch = WatchModified::new(watcher, ConfigFile::DEBOUNCE).map_err(|error| {
Error::WatchFile {
error,
path: path.to_owned(),
}
})?;

let toml = if let Some(toml) = toml {
toml
}
else {
std::fs::read_to_string(path)
.map_err(|error| {
Error::ReadFile {
error,
path: path.to_owned(),
}
})?
.into()
};

let hash = FileHash::hash(toml.as_bytes());

let document: DocumentMut = toml.parse().map_err(|error| {
Error::ParseToml {
error,
path: path.to_owned(),
toml: toml.clone().into_owned(),
}
})?;

let data: Data = toml_edit::de::from_document(document.clone()).map_err(|error| {
Error::ParseToml {
error: error.into(),
path: path.to_owned(),
toml: toml.into_owned(),
}
})?;

Ok(Loader {
inner: Inner {
path: path.to_owned(),
hash,
document,
data,
},
watch,
hash,
})
}

#[derive(Debug)]
struct WatchConfig {
inner: Arc<RwLock<Inner>>,
watch: WatchModified,
path: PathBuf,
hash: FileHash,
}

impl WatchConfig {
async fn run(mut self) {
while let Ok(()) = self.watch.wait().await {
match self.reload() {
Ok(None) => {}
Ok(Some(_document)) => todo!(),
Err(_e) => todo!(),
}
}
}

fn reload(&mut self) -> Result<Option<DocumentMut>, Error> {
let toml = std::fs::read_to_string(&self.path).map_err(|error| {
Error::ReadFile {
error,
path: self.path.to_owned(),
}
})?;

let hash = FileHash::hash(toml.as_bytes());

if hash != self.hash {
let document: DocumentMut = toml.parse().map_err(|error| {
Error::ParseToml {
error,
path: self.path.to_owned(),
toml,
}
})?;

self.hash = hash;

Ok(Some(document))
}
else {
Ok(None)
}
}
}
46 changes: 46 additions & 0 deletions skunk-cli/src/config_new/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::path::PathBuf;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Could not determine config directory.")]
ConfigDirectory,

#[error("Could not determine data directory.")]
DataDirectory,

#[error("Could not create directory: {path}")]
CreateDirectory {
#[source]
error: std::io::Error,
path: PathBuf,
},

#[error("Could not read file: {path}")]
ReadFile {
#[source]
error: std::io::Error,
path: PathBuf,
},

#[error("Could not write file: {path}")]
WriteFile {
#[source]
error: std::io::Error,
path: PathBuf,
},

#[error("Could not watch file: {path}")]
WatchFile {
#[source]
error: notify::Error,
path: PathBuf,
},

#[error("Could not parse TOML file: {path}")]
ParseToml {
#[source]
error: toml_edit::TomlError,
path: PathBuf,
toml: String,
},
}
85 changes: 85 additions & 0 deletions skunk-cli/src/config_new/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
mod config_file;
mod error;

use std::{
io::Cursor,
path::{
Path,
PathBuf,
},
};

use config_file::ConfigFile;
use murmur3::murmur3_x64_128;

pub use self::error::Error;

/// Default configuration directory relative to the OS's local configuration
/// directory (e.g. `~/.config`` on Linux).
pub const CONFIG_DIR_NAME: &'static str = "feralsec/skunk";

pub const DATA_DIR_NAME: &'static str = "feralsec/skunk";

/// Main configuration file name.
pub const CONFIG_FILE: &'static str = "skunk.toml";

pub const DEFAULT_CONFIG: &'static str = include_str!("skunk.default.toml");

pub struct Config {
config_dir: PathBuf,
data_dir: PathBuf,
config_file: ConfigFile,
}

impl Config {
/// Open the configuration, either with the given `path` as path to the
/// configuration directory, or using the default [`CONFIG_DIR_NAME`].
pub fn open(config_dir: Option<impl AsRef<Path>>) -> Result<Self, Error> {
// determine path to configuration directory.
let config_dir = config_dir
.map(|path| path.as_ref().to_owned())
.or_else(|| dirs::config_local_dir().map(|path| path.join(CONFIG_DIR_NAME)))
.ok_or(Error::ConfigDirectory)?;

// if the directory doesn't exist, create it.
create_dir_all(&config_dir)?;

// parse the configuration TOML file.
let config_file_path = config_dir.join(CONFIG_FILE);
let config_file = ConfigFile::open(config_file_path)?;

// determine path to data directory
let data_dir = config_file
.data_dir()
.map(ToOwned::to_owned)
.or_else(|| dirs::data_local_dir().map(|path| path.join(DATA_DIR_NAME)))
.ok_or(Error::DataDirectory)?;

create_dir_all(&data_dir)?;

Ok(Self {
config_dir,
data_dir,
config_file: config_file.load(),
})
}
}

fn create_dir_all(path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
std::fs::create_dir_all(path).map_err(|error| {
Error::CreateDirectory {
error,
path: path.to_owned(),
}
})
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct FileHash(u128);

impl FileHash {
pub fn hash(data: &[u8]) -> Self {
Self(murmur3_x64_128(&mut Cursor::new(data), 0).unwrap())
}
}
7 changes: 7 additions & 0 deletions skunk-cli/src/config_new/skunk.default.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

[ca]
# The private key for the CA
# key_file = "ca.key.pem"

# The public key for the CA
# cert_file = "ca.cert.pem"
1 change: 1 addition & 0 deletions skunk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod api;
mod app;
mod args;
mod config;
mod config_new;
mod proxy;
mod util;

Expand Down
Loading

0 comments on commit adb4abd

Please sign in to comment.