Add base subcrates
This commit is contained in:
12
crates/wtfnet-core/Cargo.toml
Normal file
12
crates/wtfnet-core/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "wtfnet-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
|
||||
487
crates/wtfnet-core/src/lib.rs
Normal file
487
crates/wtfnet-core/src/lib.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandEnvelope<T> {
|
||||
pub meta: Meta,
|
||||
pub command: CommandInfo,
|
||||
pub data: T,
|
||||
pub warnings: Vec<WarnItem>,
|
||||
pub errors: Vec<ErrItem>,
|
||||
}
|
||||
|
||||
impl<T> CommandEnvelope<T> {
|
||||
pub fn new(meta: Meta, command: CommandInfo, data: T) -> Self {
|
||||
Self {
|
||||
meta,
|
||||
command,
|
||||
data,
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Meta {
|
||||
pub tool: String,
|
||||
pub version: String,
|
||||
pub timestamp: String,
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub privileges: Privileges,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn new(tool: impl Into<String>, version: impl Into<String>, is_admin: bool) -> Self {
|
||||
Self {
|
||||
tool: tool.into(),
|
||||
version: version.into(),
|
||||
timestamp: now_rfc3339(),
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
privileges: Privileges {
|
||||
is_admin,
|
||||
notes: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_privilege_note(&mut self, note: impl Into<String>) {
|
||||
self.privileges.notes.push(note.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Privileges {
|
||||
pub is_admin: bool,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandInfo {
|
||||
pub name: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandInfo {
|
||||
pub fn new(name: impl Into<String>, args: Vec<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WarnItem {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub details: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl WarnItem {
|
||||
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code: code.into(),
|
||||
message: message.into(),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, details: serde_json::Value) -> Self {
|
||||
self.details = Some(details);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ErrItem {
|
||||
pub code: ErrorCode,
|
||||
pub message: String,
|
||||
pub details: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ErrItem {
|
||||
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, details: serde_json::Value) -> Self {
|
||||
self.details = Some(details);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
PermissionDenied,
|
||||
Timeout,
|
||||
NotSupported,
|
||||
IoError,
|
||||
InvalidArgs,
|
||||
Partial,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ExitKind {
|
||||
Ok,
|
||||
Failed,
|
||||
Usage,
|
||||
Permission,
|
||||
Timeout,
|
||||
Partial,
|
||||
}
|
||||
|
||||
impl ExitKind {
|
||||
pub fn code(self) -> i32 {
|
||||
match self {
|
||||
ExitKind::Ok => 0,
|
||||
ExitKind::Failed => 1,
|
||||
ExitKind::Usage => 2,
|
||||
ExitKind::Permission => 3,
|
||||
ExitKind::Timeout => 4,
|
||||
ExitKind::Partial => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LogFormat {
|
||||
Text,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl LogFormat {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"text" => Some(LogFormat::Text),
|
||||
"json" => Some(LogFormat::Json),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"error" => Some(LogLevel::Error),
|
||||
"warn" => Some(LogLevel::Warn),
|
||||
"info" => Some(LogLevel::Info),
|
||||
"debug" => Some(LogLevel::Debug),
|
||||
"trace" => Some(LogLevel::Trace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_level_filter(self) -> tracing_subscriber::filter::LevelFilter {
|
||||
match self {
|
||||
LogLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR,
|
||||
LogLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN,
|
||||
LogLevel::Info => tracing_subscriber::filter::LevelFilter::INFO,
|
||||
LogLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG,
|
||||
LogLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: LogLevel,
|
||||
pub format: LogFormat,
|
||||
pub log_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct LoggingHandle {
|
||||
_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||
}
|
||||
|
||||
pub fn init_logging(config: &LoggingConfig) -> Result<LoggingHandle, Box<dyn std::error::Error>> {
|
||||
let level_filter = config.level.to_level_filter();
|
||||
match config.format {
|
||||
LogFormat::Text => init_logging_text(level_filter, config.log_file.as_ref()),
|
||||
LogFormat::Json => init_logging_json(level_filter, config.log_file.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging_text(
|
||||
level_filter: tracing_subscriber::filter::LevelFilter,
|
||||
log_file: Option<&PathBuf>,
|
||||
) -> Result<LoggingHandle, Box<dyn std::error::Error>> {
|
||||
let (writer, guard) = logging_writer(log_file)?;
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
.with_filter(level_filter);
|
||||
tracing_subscriber::registry().with(layer).init();
|
||||
Ok(LoggingHandle { _guard: guard })
|
||||
}
|
||||
|
||||
fn init_logging_json(
|
||||
level_filter: tracing_subscriber::filter::LevelFilter,
|
||||
log_file: Option<&PathBuf>,
|
||||
) -> Result<LoggingHandle, Box<dyn std::error::Error>> {
|
||||
let (writer, guard) = logging_writer(log_file)?;
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
.json()
|
||||
.with_filter(level_filter);
|
||||
tracing_subscriber::registry().with(layer).init();
|
||||
Ok(LoggingHandle { _guard: guard })
|
||||
}
|
||||
|
||||
fn logging_writer(
|
||||
log_file: Option<&PathBuf>,
|
||||
) -> Result<
|
||||
(
|
||||
tracing_subscriber::fmt::writer::BoxMakeWriter,
|
||||
Option<tracing_appender::non_blocking::WorkerGuard>,
|
||||
),
|
||||
Box<dyn std::error::Error>,
|
||||
> {
|
||||
use tracing_subscriber::fmt::writer::{BoxMakeWriter, MakeWriterExt};
|
||||
|
||||
if let Some(path) = log_file {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)?;
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file);
|
||||
let writer = std::io::stderr.and(non_blocking);
|
||||
Ok((BoxMakeWriter::new(writer), Some(guard)))
|
||||
} else {
|
||||
Ok((BoxMakeWriter::new(std::io::stderr), None))
|
||||
}
|
||||
}
|
||||
|
||||
fn now_rfc3339() -> String {
|
||||
OffsetDateTime::now_utc()
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub geoip: GeoIpConfig,
|
||||
pub dns: DnsConfig,
|
||||
pub probe: ProbeConfig,
|
||||
pub http: HttpConfig,
|
||||
pub logging: LoggingSettings,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
geoip: GeoIpConfig::default(),
|
||||
dns: DnsConfig::default(),
|
||||
probe: ProbeConfig::default(),
|
||||
http: HttpConfig::default(),
|
||||
logging: LoggingSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeoIpConfig {
|
||||
pub country_db: Option<PathBuf>,
|
||||
pub asn_db: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for GeoIpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
country_db: None,
|
||||
asn_db: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DnsConfig {
|
||||
pub detect_servers: Vec<String>,
|
||||
pub timeout_ms: u64,
|
||||
pub repeat: u32,
|
||||
}
|
||||
|
||||
impl Default for DnsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detect_servers: vec![
|
||||
"1.1.1.1".to_string(),
|
||||
"8.8.8.8".to_string(),
|
||||
"9.9.9.9".to_string(),
|
||||
],
|
||||
timeout_ms: 2000,
|
||||
repeat: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProbeConfig {
|
||||
pub timeout_ms: u64,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl Default for ProbeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout_ms: 800,
|
||||
count: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HttpConfig {
|
||||
pub timeout_ms: u64,
|
||||
pub follow_redirects: u32,
|
||||
pub max_body_bytes: u64,
|
||||
}
|
||||
|
||||
impl Default for HttpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout_ms: 3000,
|
||||
follow_redirects: 3,
|
||||
max_body_bytes: 8192,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingSettings {
|
||||
pub level: String,
|
||||
pub format: String,
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for LoggingSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "info".to_string(),
|
||||
format: "text".to_string(),
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
Io(std::io::Error),
|
||||
Parse(serde_json::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ConfigError::Io(err) => write!(f, "config io error: {err}"),
|
||||
ConfigError::Parse(err) => write!(f, "config parse error: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
impl From<std::io::Error> for ConfigError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ConfigError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ConfigError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ConfigError::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(path: Option<&Path>) -> Result<Config, ConfigError> {
|
||||
let mut config = match path {
|
||||
Some(path) => load_config_from_path(path)?,
|
||||
None => load_default_config().unwrap_or_else(Config::default),
|
||||
};
|
||||
|
||||
apply_env_overrides(&mut config);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load_default_config() -> Option<Config> {
|
||||
let path = default_config_path()?;
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
load_config_from_path(&path).ok()
|
||||
}
|
||||
|
||||
pub fn load_config_from_path(path: &Path) -> Result<Config, ConfigError> {
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
let config = serde_json::from_str(&contents)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn default_config_path() -> Option<PathBuf> {
|
||||
if cfg!(windows) {
|
||||
std::env::var("APPDATA")
|
||||
.ok()
|
||||
.map(|root| PathBuf::from(root).join("wtfnet").join("config.json"))
|
||||
} else {
|
||||
let base = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(xdg)
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home).join(".config")
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(base.join("wtfnet").join("config.json"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_env_overrides(config: &mut Config) {
|
||||
if let Ok(value) = std::env::var("NETTOOL_GEOIP_COUNTRY_DB") {
|
||||
if !value.trim().is_empty() {
|
||||
config.geoip.country_db = Some(PathBuf::from(value));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("NETTOOL_GEOIP_ASN_DB") {
|
||||
if !value.trim().is_empty() {
|
||||
config.geoip.asn_db = Some(PathBuf::from(value));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("NETTOOL_LOG_LEVEL") {
|
||||
if !value.trim().is_empty() {
|
||||
config.logging.level = value;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("NETTOOL_LOG_FORMAT") {
|
||||
if !value.trim().is_empty() {
|
||||
config.logging.format = value;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(value) = std::env::var("NETTOOL_LOG_FILE") {
|
||||
if !value.trim().is_empty() {
|
||||
config.logging.file = Some(PathBuf::from(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user