lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit edb617171cbf78ee3eb05238f43e2f346c7552ec
parent 57bb7bfa11143c15644e9859e386223255c71963
Author: triesap <tyson@radroots.org>
Date:   Fri,  3 Oct 2025 19:24:09 +0100

net-core: add `keys` module with Nostr key management

Diffstat:
MCargo.lock | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
MCargo.toml | 2+-
Mcrates/log/Cargo.toml | 14+++++++++-----
Mcrates/log/src/error.rs | 9++++++++-
Mcrates/log/src/init.rs | 3+--
Mcrates/log/src/lib.rs | 26++++++++++++++++++++++++++
Mcrates/net-core/Cargo.toml | 11+++++++++++
Mcrates/net-core/src/config.rs | 16++++++++++++++++
Mcrates/net-core/src/error.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++----
Acrates/net-core/src/keys.rs | 336+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/net-core/src/lib.rs | 3+++
Mcrates/net-core/src/logging.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/net-core/src/net.rs | 13++++++++++---
13 files changed, 612 insertions(+), 30 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -577,6 +577,27 @@ dependencies = [ ] [[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -800,6 +821,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "hex-conservative" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1161,6 +1188,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1365,6 +1402,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] name = "ordered-multimap" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1655,11 +1698,19 @@ dependencies = [ name = "radroots-net-core" version = "0.1.0" dependencies = [ + "directories", + "hex", + "nostr", "radroots-log", + "secrecy", "serde", + "serde_json", + "tempfile", "thiserror 1.0.69", "tokio", "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] @@ -1765,6 +1816,17 @@ dependencies = [ ] [[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.16", +] + +[[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2017,6 +2079,15 @@ dependencies = [ ] [[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2484,7 +2555,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] @@ -2501,17 +2571,6 @@ dependencies = [ ] [[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -46,7 +46,7 @@ tempfile = { version = "3" } thiserror = { version = "1" } tokio = { version = "1" } toml = { version = "0.8" } -tracing = { version = "0.1" } +tracing = { version = "0.1", default-features = false } tracing-appender = { version = "0.2" } tracing-log = { version = "0.2" } tracing-subscriber = { version = "0.3" } diff --git a/crates/log/Cargo.toml b/crates/log/Cargo.toml @@ -6,8 +6,12 @@ authors = ["Radroots Authors"] rust-version.workspace = true license.workspace = true +[features] +default = ["std"] +std = ["dep:thiserror", "dep:tracing-subscriber", "dep:tracing-appender"] + [dependencies] -thiserror = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } -tracing-appender = { workspace = true } -\ No newline at end of file +tracing = { workspace = true, default-features = false } +thiserror = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true, features = ["fmt", "env-filter"] } +tracing-appender = { workspace = true, optional = true } +\ No newline at end of file diff --git a/crates/log/src/error.rs b/crates/log/src/error.rs @@ -1,9 +1,16 @@ +use alloc::string::String; + +#[cfg(feature = "std")] use thiserror::Error; -#[derive(Debug, Error)] +#[cfg_attr(feature = "std", derive(Error))] +#[derive(Debug)] pub enum Error { + #[cfg(feature = "std")] #[error("{0}")] Msg(String), + + #[cfg(feature = "std")] #[error("logging init failed: {0}")] Init(&'static str), } diff --git a/crates/log/src/init.rs b/crates/log/src/init.rs @@ -27,7 +27,6 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { }; let env = EnvFilter::from_default_env().add_directive(Level::INFO.into()); - let fmt_layer_file = writer.as_ref().map(|w| fmt::layer().with_writer(w.clone())); let fmt_layer_stdout = if opts.also_stdout() { Some(fmt::layer()) @@ -60,7 +59,7 @@ pub fn init_logging(opts: LoggingOptions) -> Result<()> { pub fn init_stdout() -> Result<()> { init_logging(LoggingOptions { dir: None, + file_name: "radroots.log".into(), stdout: true, - ..Default::default() }) } diff --git a/crates/log/src/lib.rs b/crates/log/src/lib.rs @@ -1,9 +1,19 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + mod error; + +#[cfg(feature = "std")] mod init; +#[cfg(feature = "std")] mod options; pub use error::{Error, Result}; + +#[cfg(feature = "std")] pub use init::{init_logging, init_stdout}; +#[cfg(feature = "std")] pub use options::LoggingOptions; use tracing::{debug, error, info}; @@ -22,3 +32,19 @@ pub fn log_error<S: AsRef<str>>(msg: S) { pub fn log_debug<S: AsRef<str>>(msg: S) { debug!("{}", msg.as_ref()); } + +#[cfg(not(feature = "std"))] +pub fn init_no_std() -> Result<()> { + Ok(()) +} + +pub fn init_default() -> Result<()> { + #[cfg(feature = "std")] + { + return init_stdout(); + } + #[cfg(not(feature = "std"))] + { + return init_no_std(); + } +} diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml @@ -10,10 +10,21 @@ license.workspace = true default = ["std"] std = [] rt = ["dep:tokio"] +nostr-client = ["dep:nostr", "dep:secrecy", "dep:hex", "dep:tempfile", "dep:serde_json"] +directories = ["dep:directories"] +fs-persistence = [] [dependencies] radroots-log = { workspace = true } +directories = { workspace = true, optional = true } +hex = { workspace = true, optional = true } +nostr = { workspace = true, optional = true } +secrecy = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, optional = true } +tempfile = { workspace = true, optional = true } thiserror = { workspace = true } tokio = { workspace = true, optional = true, features = ["rt-multi-thread"] } tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } +tracing-appender = { workspace = true } diff --git a/crates/net-core/src/config.rs b/crates/net-core/src/config.rs @@ -1,4 +1,20 @@ use serde::{Deserialize, Serialize}; +use std::path::PathBuf; #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct NetConfig {} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum KeyFormat { + Json, + Nsec, + Hex, + Bin, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeyPersistenceConfig { + pub path: Option<PathBuf>, + pub format: KeyFormat, + pub no_overwrite: bool, +} diff --git a/crates/net-core/src/error.rs b/crates/net-core/src/error.rs @@ -2,7 +2,7 @@ use alloc::string::String; use thiserror::Error; #[derive(Debug, Error)] -pub enum Error { +pub enum NetError { #[error("{0}")] Msg(String), @@ -12,12 +12,57 @@ pub enum Error { #[cfg(feature = "std")] #[error("I/O error: {0}")] Io(#[from] std::io::Error), + + #[error("missing key")] + MissingKey, + + #[error("invalid hex32")] + InvalidHex32, + + #[error("invalid bech32")] + InvalidBech32, + + #[error("invalid key file")] + InvalidKeyFile, + + #[error("key I/O")] + KeyIo, + + #[error("overwrite denied")] + OverwriteDenied, + + #[error("persistence unsupported")] + PersistenceUnsupported, + + #[error("logging init failed: {0}")] + LoggingInit(&'static str), } -impl Error { +impl NetError { pub fn msg<M: Into<String>>(msg: M) -> Self { - Error::Msg(msg.into()) + NetError::Msg(msg.into()) + } +} + +impl Clone for NetError { + fn clone(&self) -> Self { + match self { + NetError::Msg(m) => NetError::Msg(m.clone()), + NetError::Poisoned => NetError::Poisoned, + #[cfg(feature = "std")] + NetError::Io(_) => { + panic!("cannot clone std::io::Error"); + } + NetError::MissingKey => NetError::MissingKey, + NetError::InvalidHex32 => NetError::InvalidHex32, + NetError::InvalidBech32 => NetError::InvalidBech32, + NetError::InvalidKeyFile => NetError::InvalidKeyFile, + NetError::KeyIo => NetError::KeyIo, + NetError::OverwriteDenied => NetError::OverwriteDenied, + NetError::PersistenceUnsupported => NetError::PersistenceUnsupported, + NetError::LoggingInit(s) => NetError::LoggingInit(s), + } } } -pub type Result<T> = core::result::Result<T, Error>; +pub type Result<T> = core::result::Result<T, NetError>; diff --git a/crates/net-core/src/keys.rs b/crates/net-core/src/keys.rs @@ -0,0 +1,336 @@ +#[cfg(feature = "nostr-client")] +use crate::config::{KeyFormat, KeyPersistenceConfig}; +#[cfg(feature = "nostr-client")] +use crate::error::{NetError, Result}; +#[cfg(feature = "nostr-client")] +use serde::Deserialize; +#[cfg(feature = "nostr-client")] +use std::path::{Path, PathBuf}; +#[cfg(feature = "nostr-client")] +use std::str::FromStr; + +#[cfg(feature = "nostr-client")] +#[derive(Debug, Clone, Deserialize, serde::Serialize)] +#[serde(deny_unknown_fields)] +struct KeysFile { + pub key: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub npub: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option<String>, +} + +#[cfg(feature = "nostr-client")] +#[derive(Debug, Clone, Default)] +pub struct KeysState { + pub loaded: bool, + pub source: Option<PathBuf>, + pub npub: Option<String>, + pub last_error: Option<NetError>, +} + +#[cfg(feature = "nostr-client")] +#[derive(Debug, Clone, Default)] +pub struct KeysManager { + pub keys: Option<nostr::Keys>, + pub state: KeysState, +} + +#[cfg(feature = "nostr-client")] +#[derive(Debug, Clone)] +pub enum LoadOutcome { + FromFile(PathBuf), + GeneratedEphemeral, +} + +#[cfg(feature = "nostr-client")] +impl KeysManager { + pub fn new() -> Self { + Self::default() + } + + pub fn is_valid_hex32(s: &str) -> bool { + let is_hex = s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()); + is_hex + } + pub fn is_valid_nsec(s: &str) -> bool { + s.starts_with("nsec1") + } + + pub fn load_from_secret_bytes(&mut self, sk: &[u8; 32]) -> Result<()> { + use nostr::secp256k1::SecretKey as SecpSecret; + let secp = SecpSecret::from_slice(&sk[..]).map_err(|_| NetError::InvalidHex32)?; + let nostr_sk = nostr::SecretKey::from(secp); + let keys = nostr::Keys::new(nostr_sk); + self.set_keys(keys); + Ok(()) + } + + pub fn load_from_hex32(&mut self, hex: &str) -> Result<()> { + use secrecy::{ExposeSecret, SecretString}; + let secret = SecretString::new(hex.to_owned().into()); + let k = nostr::SecretKey::from_str(secret.expose_secret()) + .map_err(|_| NetError::InvalidHex32)?; + let keys = nostr::Keys::new(k); + self.set_keys(keys); + Ok(()) + } + + pub fn load_from_nsec(&mut self, nsec: &str) -> Result<()> { + use secrecy::{ExposeSecret, SecretString}; + let secret = SecretString::new(nsec.to_owned().into()); + let keys = + nostr::Keys::parse(secret.expose_secret()).map_err(|_| NetError::InvalidBech32)?; + self.set_keys(keys); + Ok(()) + } + + pub fn set_keys(&mut self, keys: nostr::Keys) { + use nostr::nips::nip19::ToBech32; + + let npub = keys.public_key().to_bech32().ok(); + self.keys = Some(keys); + self.state.loaded = true; + self.state.source = None; + self.state.npub = npub; + self.state.last_error = None; + } + + pub fn clear(&mut self) { + *self = Self::default(); + } + + pub fn require(&self) -> Result<&nostr::Keys> { + self.keys.as_ref().ok_or(NetError::MissingKey) + } + + pub fn load_from_path_auto(&mut self, path: impl AsRef<Path>) -> Result<()> { + let p = path.as_ref(); + match std::fs::read_to_string(p) { + Ok(s) => { + if let Ok(jf) = serde_json::from_str::<KeysFile>(&s) { + return self.load_from_hex32(&jf.key).map(|_| { + self.state.source = Some(p.to_path_buf()); + }); + } + let trimmed = s.trim(); + if Self::is_valid_nsec(trimmed) { + return self.load_from_nsec(trimmed).map(|_| { + self.state.source = Some(p.to_path_buf()); + }); + } + if Self::is_valid_hex32(trimmed) { + return self.load_from_hex32(trimmed).map(|_| { + self.state.source = Some(p.to_path_buf()); + }); + } + } + Err(_) => {} + } + match std::fs::read(p) { + Ok(bytes) if bytes.len() == 32 => { + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + self.load_from_secret_bytes(&arr)?; + self.state.source = Some(p.to_path_buf()); + Ok(()) + } + _ => Err(NetError::InvalidKeyFile), + } + } + + pub fn load_from_file(&mut self, path: impl AsRef<Path>) -> Result<()> { + self.load_from_path_auto(path) + } + + pub fn save_to_path_with_format( + &self, + path: impl AsRef<Path>, + format: KeyFormat, + no_overwrite: bool, + ) -> Result<()> { + if no_overwrite && path.as_ref().exists() { + return Err(NetError::OverwriteDenied); + } + match format { + KeyFormat::Json => self.save_json(path), + KeyFormat::Nsec => self.save_nsec_text(path, no_overwrite), + KeyFormat::Hex => self.save_hex_text(path, no_overwrite), + KeyFormat::Bin => self.save_raw_bin(path, no_overwrite), + } + } + + fn require_secret_hex(&self) -> Result<String> { + let keys = self.require()?; + Ok(keys.secret_key().to_secret_hex()) + } + + pub fn export_secret_hex(&self) -> Result<String> { + self.require_secret_hex() + } + + fn save_json(&self, path: impl AsRef<Path>) -> Result<()> { + use std::time::{SystemTime, UNIX_EPOCH}; + let keys = self.require()?; + let secret_hex = keys.secret_key().to_secret_hex(); + let payload = KeysFile { + key: secret_hex, + npub: self.npub(), + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()), + note: None, + }; + let json = serde_json::to_string_pretty(&payload).map_err(|_| NetError::KeyIo)?; + write_secret_atomically_noclobber(path.as_ref(), json.as_bytes()) + .map_err(|_| NetError::KeyIo)?; + Ok(()) + } + + fn save_nsec_text(&self, path: impl AsRef<Path>, no_overwrite: bool) -> Result<()> { + use nostr::nips::nip19::ToBech32; + if no_overwrite && path.as_ref().exists() { + return Err(NetError::OverwriteDenied); + } + let keys = self.require()?; + let nsec = keys.secret_key().to_bech32().map_err(|_| NetError::KeyIo)?; + write_secret_atomically_noclobber(path.as_ref(), nsec.as_bytes()) + .map_err(|_| NetError::KeyIo)?; + Ok(()) + } + + fn save_hex_text(&self, path: impl AsRef<Path>, no_overwrite: bool) -> Result<()> { + if no_overwrite && path.as_ref().exists() { + return Err(NetError::OverwriteDenied); + } + let hex = self.require_secret_hex()?; + write_secret_atomically_noclobber(path.as_ref(), hex.as_bytes()) + .map_err(|_| NetError::KeyIo)?; + Ok(()) + } + + fn save_raw_bin(&self, path: impl AsRef<Path>, no_overwrite: bool) -> Result<()> { + if no_overwrite && path.as_ref().exists() { + return Err(NetError::OverwriteDenied); + } + let hex = self.require_secret_hex()?; + let mut out = [0u8; 32]; + hex::decode_to_slice(hex, &mut out).map_err(|_| NetError::KeyIo)?; + write_secret_atomically_noclobber(path.as_ref(), &out).map_err(|_| NetError::KeyIo)?; + Ok(()) + } + + pub fn generate_in_memory(&mut self) -> &nostr::Keys { + let keys = nostr::Keys::generate(); + self.set_keys(keys); + self.keys.as_ref().unwrap() + } + + pub fn ensure_loaded_from_file_outcome( + &mut self, + path: impl AsRef<Path>, + allow_generate: bool, + ) -> Result<LoadOutcome> { + let p = path.as_ref(); + if p.exists() { + self.load_from_path_auto(p)?; + return Ok(LoadOutcome::FromFile(p.to_path_buf())); + } + if !allow_generate { + self.state.last_error = Some(NetError::MissingKey); + return Err(NetError::MissingKey); + } + let _ = self.generate_in_memory(); + Ok(LoadOutcome::GeneratedEphemeral) + } + + pub fn ensure_loaded_from_file( + &mut self, + path: impl AsRef<Path>, + allow_generate: bool, + ) -> Result<()> { + let _ = self.ensure_loaded_from_file_outcome(path, allow_generate)?; + Ok(()) + } + + pub fn npub(&self) -> Option<String> { + self.state.npub.clone() + } + + #[cfg(all(feature = "directories", feature = "fs-persistence"))] + pub fn default_key_path() -> Option<PathBuf> { + directories::ProjectDirs::from("com", "Radroots", "radroots") + .map(|d| d.config_dir().join("identity.json")) + } + + #[cfg(all(feature = "directories", feature = "fs-persistence"))] + pub fn persist_best_practice(&self) -> Result<PathBuf> { + let path = Self::default_key_path().ok_or(NetError::PersistenceUnsupported)?; + if path.exists() { + return Err(NetError::OverwriteDenied); + } + self.save_to_path_with_format(&path, KeyFormat::Json, true)?; + Ok(path) + } + + #[cfg(not(all(feature = "directories", feature = "fs-persistence")))] + pub fn persist_best_practice(&self) -> Result<PathBuf> { + Err(NetError::PersistenceUnsupported) + } + + #[cfg(feature = "fs-persistence")] + pub fn persist_with_config(&self, cfg: &KeyPersistenceConfig) -> Result<PathBuf> { + let path = if let Some(p) = &cfg.path { + p.clone() + } else { + #[cfg(all(feature = "directories", feature = "fs-persistence"))] + { + Self::default_key_path().ok_or(NetError::PersistenceUnsupported)? + } + #[cfg(not(all(feature = "directories", feature = "fs-persistence")))] + { + return Err(NetError::PersistenceUnsupported); + } + }; + self.save_to_path_with_format(&path, cfg.format, cfg.no_overwrite)?; + Ok(path) + } + + #[cfg(not(feature = "fs-persistence"))] + pub fn persist_with_config(&self, _cfg: &KeyPersistenceConfig) -> Result<PathBuf> { + Err(NetError::PersistenceUnsupported) + } +} + +#[cfg(feature = "nostr-client")] +fn write_secret_atomically_noclobber(path: &Path, data: &[u8]) -> crate::error::Result<()> { + use std::io::Write; + let dir = path.parent().unwrap_or_else(|| Path::new(".")); + std::fs::create_dir_all(dir)?; + + let mut tmp = tempfile::NamedTempFile::new_in(dir)?; + tmp.write_all(data)?; + tmp.flush()?; + + let persist_result = tmp.persist_noclobber(path); + + if let Err(e) = persist_result { + if e.error.kind() == std::io::ErrorKind::AlreadyExists { + return Err(crate::error::NetError::OverwriteDenied); + } else { + return Err(crate::error::NetError::KeyIo); + } + } + + #[cfg(unix)] + { + use std::fs::Permissions; + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(path, Permissions::from_mode(0o600)); + } + + Ok(()) +} diff --git a/crates/net-core/src/lib.rs b/crates/net-core/src/lib.rs @@ -11,4 +11,7 @@ pub mod logging; pub mod builder; pub mod config; +#[cfg(feature = "nostr-client")] +pub mod keys; + pub use net::{Net, NetHandle, NetInfo}; diff --git a/crates/net-core/src/logging.rs b/crates/net-core/src/logging.rs @@ -1,2 +1,71 @@ -#[cfg(feature = "std")] -pub use radroots_log::{LoggingOptions, init_logging, init_stdout}; +use crate::error::{NetError, Result}; +use std::sync::OnceLock; +use std::{fs, path::PathBuf}; +use tracing::{Level, info}; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{EnvFilter, fmt}; + +#[derive(Debug, Clone)] +pub struct LoggingOptions { + pub dir: Option<PathBuf>, + pub file_name: String, + pub also_stdout: bool, +} + +impl Default for LoggingOptions { + fn default() -> Self { + Self { + dir: None, + file_name: "radroots.log".into(), + also_stdout: true, + } + } +} + +static GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new(); +static INIT: OnceLock<()> = OnceLock::new(); + +pub fn init_logging(opts: LoggingOptions) -> Result<()> { + if INIT.get().is_some() { + return Ok(()); + } + + let writer = if let Some(dir) = &opts.dir { + fs::create_dir_all(dir).map_err(|_| NetError::LoggingInit("mkdir"))?; + let file_appender = tracing_appender::rolling::daily(dir, &opts.file_name); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + let _ = GUARD.set(guard); + Some(non_blocking) + } else { + None + }; + + let env = EnvFilter::from_default_env().add_directive(Level::INFO.into()); + let fmt_layer_file = writer.as_ref().map(|w| fmt::layer().with_writer(w.clone())); + let fmt_layer_stdout = if opts.also_stdout { + Some(fmt::layer()) + } else { + None + }; + + let subscriber = tracing_subscriber::registry() + .with(env) + .with(fmt_layer_file) + .with(fmt_layer_stdout); + + match subscriber.try_init() { + Ok(()) => { + let _ = INIT.set(()); + info!( + "logging initialized (file: {}, stdout: {})", + opts.dir + .as_ref() + .map(|d| d.join(&opts.file_name).display().to_string()) + .unwrap_or_else(|| "<disabled>".into()), + opts.also_stdout + ); + Ok(()) + } + Err(_) => Ok(()), + } +} diff --git a/crates/net-core/src/net.rs b/crates/net-core/src/net.rs @@ -1,7 +1,9 @@ use serde::Serialize; use std::sync::{Arc, Mutex, MutexGuard}; -use crate::error::{Error, Result}; +use crate::error::{NetError, Result}; +#[cfg(feature = "nostr-client")] +use crate::keys::KeysManager; #[derive(Debug, Clone, Serialize)] pub struct BuildInfo { @@ -26,6 +28,9 @@ pub struct Net { pub info: NetInfo, pub config: crate::config::NetConfig, + #[cfg(feature = "nostr-client")] + pub keys: KeysManager, + #[cfg(feature = "rt")] pub rt: Option<tokio::runtime::Runtime>, } @@ -44,6 +49,8 @@ impl Net { }, }, config: cfg, + #[cfg(feature = "nostr-client")] + keys: KeysManager::default(), #[cfg(feature = "rt")] rt: None, } @@ -66,7 +73,7 @@ impl Net { .worker_threads(threads) .enable_all() .build() - .map_err(|e| Error::msg(format!("failed to build tokio runtime: {e}")))?; + .map_err(|e| NetError::msg(format!("failed to build tokio runtime: {e}")))?; self.rt = Some(rt); Ok(()) @@ -82,7 +89,7 @@ impl NetHandle { } pub fn lock(&self) -> Result<MutexGuard<'_, Net>> { - self.0.lock().map_err(|_| Error::Poisoned) + self.0.lock().map_err(|_| NetError::Poisoned) } }