221 lines
6.3 KiB
Rust
221 lines
6.3 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerConfig {
|
|
/// Server host address
|
|
#[serde(default = "default_host")]
|
|
pub host: String,
|
|
|
|
/// Server port
|
|
#[serde(default = "default_port")]
|
|
pub port: u16,
|
|
|
|
/// Redis connection URL
|
|
#[serde(default = "default_redis_url")]
|
|
pub redis_url: String,
|
|
|
|
/// Enable authentication
|
|
#[serde(default)]
|
|
pub auth: bool,
|
|
|
|
/// Enable TLS/WSS
|
|
#[serde(default)]
|
|
pub tls: bool,
|
|
|
|
/// Path to TLS certificate file
|
|
pub cert: Option<String>,
|
|
|
|
/// Path to TLS private key file
|
|
pub key: Option<String>,
|
|
|
|
/// Separate port for TLS connections
|
|
pub tls_port: Option<u16>,
|
|
|
|
/// Enable webhook handling
|
|
#[serde(default)]
|
|
pub webhooks: bool,
|
|
|
|
/// Circles configuration - maps circle names to lists of member public keys
|
|
#[serde(default)]
|
|
pub circles: HashMap<String, Vec<String>>,
|
|
}
|
|
|
|
impl Default for ServerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
host: default_host(),
|
|
port: default_port(),
|
|
redis_url: default_redis_url(),
|
|
auth: false,
|
|
tls: false,
|
|
cert: None,
|
|
key: None,
|
|
tls_port: None,
|
|
webhooks: false,
|
|
circles: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ServerConfig {
|
|
/// Load configuration from a JSON file
|
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
|
let content = fs::read_to_string(path.as_ref())
|
|
.map_err(|e| ConfigError::FileRead(path.as_ref().to_path_buf(), e))?;
|
|
|
|
let config: ServerConfig = serde_json::from_str(&content)
|
|
.map_err(|e| ConfigError::JsonParse(e))?;
|
|
|
|
config.validate()?;
|
|
Ok(config)
|
|
}
|
|
|
|
/// Save configuration to a JSON file
|
|
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
|
|
let content = serde_json::to_string_pretty(self)
|
|
.map_err(|e| ConfigError::JsonSerialize(e))?;
|
|
|
|
fs::write(path.as_ref(), content)
|
|
.map_err(|e| ConfigError::FileWrite(path.as_ref().to_path_buf(), e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate the configuration
|
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
|
// Validate TLS configuration
|
|
if self.tls && (self.cert.is_none() || self.key.is_none()) {
|
|
return Err(ConfigError::InvalidTlsConfig(
|
|
"TLS is enabled but certificate or key path is missing".to_string()
|
|
));
|
|
}
|
|
|
|
// Validate that circles are not empty if auth is enabled
|
|
if self.auth && self.circles.is_empty() {
|
|
return Err(ConfigError::InvalidAuthConfig(
|
|
"Authentication is enabled but no circles are configured".to_string()
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Create a sample configuration file
|
|
pub fn create_sample() -> Self {
|
|
let mut circles = HashMap::new();
|
|
circles.insert(
|
|
"example_circle".to_string(),
|
|
vec![
|
|
"0x1234567890abcdef1234567890abcdef12345678".to_string(),
|
|
"0xabcdef1234567890abcdef1234567890abcdef12".to_string(),
|
|
]
|
|
);
|
|
|
|
Self {
|
|
host: "127.0.0.1".to_string(),
|
|
port: 8443,
|
|
redis_url: "redis://127.0.0.1/".to_string(),
|
|
auth: true,
|
|
tls: false,
|
|
cert: Some("cert.pem".to_string()),
|
|
key: Some("key.pem".to_string()),
|
|
tls_port: Some(8444),
|
|
webhooks: false,
|
|
circles,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ConfigError {
|
|
#[error("Failed to read config file {0}: {1}")]
|
|
FileRead(std::path::PathBuf, std::io::Error),
|
|
|
|
#[error("Failed to write config file {0}: {1}")]
|
|
FileWrite(std::path::PathBuf, std::io::Error),
|
|
|
|
#[error("Failed to parse JSON config: {0}")]
|
|
JsonParse(serde_json::Error),
|
|
|
|
#[error("Failed to serialize JSON config: {0}")]
|
|
JsonSerialize(serde_json::Error),
|
|
|
|
#[error("Invalid TLS configuration: {0}")]
|
|
InvalidTlsConfig(String),
|
|
|
|
#[error("Invalid authentication configuration: {0}")]
|
|
InvalidAuthConfig(String),
|
|
}
|
|
|
|
// Default value functions
|
|
fn default_host() -> String {
|
|
"127.0.0.1".to_string()
|
|
}
|
|
|
|
fn default_port() -> u16 {
|
|
8443
|
|
}
|
|
|
|
fn default_redis_url() -> String {
|
|
"redis://127.0.0.1/".to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[test]
|
|
fn test_config_serialization() {
|
|
let config = ServerConfig::create_sample();
|
|
let json = serde_json::to_string_pretty(&config).unwrap();
|
|
let deserialized: ServerConfig = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(config.host, deserialized.host);
|
|
assert_eq!(config.port, deserialized.port);
|
|
assert_eq!(config.circles.len(), deserialized.circles.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_file_operations() {
|
|
let config = ServerConfig::create_sample();
|
|
let temp_file = NamedTempFile::new().unwrap();
|
|
|
|
// Test writing
|
|
config.to_file(temp_file.path()).unwrap();
|
|
|
|
// Test reading
|
|
let loaded_config = ServerConfig::from_file(temp_file.path()).unwrap();
|
|
assert_eq!(config.host, loaded_config.host);
|
|
assert_eq!(config.circles.len(), loaded_config.circles.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_validation() {
|
|
let mut config = ServerConfig::default();
|
|
|
|
// Valid config should pass
|
|
assert!(config.validate().is_ok());
|
|
|
|
// TLS enabled without cert/key should fail
|
|
config.tls = true;
|
|
assert!(config.validate().is_err());
|
|
|
|
// Fix TLS config
|
|
config.cert = Some("cert.pem".to_string());
|
|
config.key = Some("key.pem".to_string());
|
|
assert!(config.validate().is_ok());
|
|
|
|
// Auth enabled without circles should fail
|
|
config.auth = true;
|
|
assert!(config.validate().is_err());
|
|
|
|
// Add circles
|
|
config.circles.insert("test".to_string(), vec!["pubkey".to_string()]);
|
|
assert!(config.validate().is_ok());
|
|
}
|
|
}
|