- Added `AgentConnectorTrait` and `AgentConnector` for managing agent connections. - Introduced `SshAgentConnector` to handle SSH-related functionalities and start a gRPC server. - Created database entities for `agents`, `certificates`, `organizations`, `public_key_revocations`, `setup_tokens`, `upstreams`, `users`, `virtual_hosts`, and `workspaces` using SeaORM. - Developed `CertificateService` for managing certificate generation and retrieval. - Implemented the main server logic to initialize the database connection and start the agent server. - Configured development settings in `development.toml` for server and database connections.
352 lines
9.8 KiB
Rust
352 lines
9.8 KiB
Rust
use config::{Config, ConfigError, Environment, File};
|
|
use rcgen::string::Ia5String;
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
use std::{net::IpAddr, str::FromStr};
|
|
use tracing::level_filters::LevelFilter;
|
|
|
|
type ValidationError = String;
|
|
|
|
trait Validate {
|
|
fn validate(&self) -> Result<(), ValidationError>;
|
|
}
|
|
|
|
/// Master server settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Settings {
|
|
pub server: ServerSettings,
|
|
pub database: DatabaseSettings,
|
|
pub grpc: GrpcSettings,
|
|
pub auth: AuthSettings,
|
|
#[serde(default)]
|
|
pub log: LogSettings,
|
|
}
|
|
|
|
/// HTTP server settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServerSettings {
|
|
#[serde(default = "default_server_bind_address")]
|
|
pub bind_address: String,
|
|
#[serde(default = "default_server_port")]
|
|
pub port: u16,
|
|
#[serde(default)]
|
|
pub certificate: CertificateSettings,
|
|
#[serde(default)]
|
|
pub cors: Option<CorsSettings>,
|
|
}
|
|
|
|
/// Database connection settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatabaseSettings {
|
|
pub url: String,
|
|
pub max_connections: Option<u32>,
|
|
}
|
|
|
|
/// gRPC server settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct GrpcSettings {
|
|
#[serde(default = "default_grpc_bind_address")]
|
|
pub bind_address: String,
|
|
#[serde(default = "default_grpc_port")]
|
|
pub port: u16,
|
|
#[serde(default)]
|
|
pub certificate: CertificateSettings,
|
|
#[serde(default)]
|
|
pub cors: Option<CorsSettings>,
|
|
}
|
|
|
|
/// Authentication settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AuthSettings {
|
|
pub jwt_secret: String,
|
|
#[serde(default = "default_jwt_expiration_hours")]
|
|
pub jwt_expiration_hours: u64,
|
|
}
|
|
|
|
/// TLS certificate settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct CertificateSettings {
|
|
#[serde(default = "default_cert_folder")]
|
|
pub cert_dir: String,
|
|
#[serde(
|
|
default,
|
|
serialize_with = "serialize_ia5string_vec",
|
|
deserialize_with = "deserialize_ia5string_vec"
|
|
)]
|
|
pub san_dns: Vec<Ia5String>,
|
|
#[serde(default)]
|
|
pub san_ip: Vec<IpAddr>,
|
|
#[serde(default)]
|
|
cert_path: Option<String>,
|
|
#[serde(default)]
|
|
key_path: Option<String>,
|
|
}
|
|
|
|
impl CertificateSettings {
|
|
pub fn cert_path(&self) -> Option<String> {
|
|
self.cert_path
|
|
.as_ref()
|
|
.map(|p| format!("{}/{}", self.cert_dir, p))
|
|
}
|
|
|
|
pub fn key_path(&self) -> Option<String> {
|
|
self.key_path
|
|
.as_ref()
|
|
.map(|p| format!("{}/{}", self.cert_dir, p))
|
|
}
|
|
}
|
|
|
|
/// CORS settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct CorsSettings {
|
|
#[serde(default)]
|
|
pub allowed_origins: Vec<String>,
|
|
#[serde(default)]
|
|
pub allowed_methods: Vec<String>,
|
|
#[serde(default)]
|
|
pub allowed_headers: Vec<String>,
|
|
#[serde(default)]
|
|
pub allow_credentials: bool,
|
|
}
|
|
|
|
/// Logging settings
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct LogSettings {
|
|
#[serde(
|
|
deserialize_with = "deserialize_level_filter",
|
|
serialize_with = "serialize_level_filter"
|
|
)]
|
|
pub level: LevelFilter,
|
|
}
|
|
|
|
impl Default for LogSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
level: default_log_level(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Validate for Settings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
self.server.validate()?;
|
|
self.grpc.validate()?;
|
|
self.database.validate()?;
|
|
self.auth.validate()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Settings {
|
|
/// Load settings from config files and environment
|
|
pub fn load() -> Result<Self, ConfigError> {
|
|
let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
|
|
|
|
let settings = Config::builder()
|
|
.add_source(File::with_name("config/default").required(false))
|
|
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
|
|
.add_source(File::with_name("config/master/default").required(false))
|
|
.add_source(File::with_name(&format!("config/master/{}", run_mode)).required(false))
|
|
.add_source(Environment::with_prefix("NXMESH").separator("__"))
|
|
.build()?;
|
|
|
|
let settings: Self = settings.try_deserialize()?;
|
|
|
|
settings.validate().map_err(ConfigError::Message)?;
|
|
|
|
Ok(settings)
|
|
}
|
|
}
|
|
|
|
impl Validate for ServerSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
if self.bind_address.is_empty() {
|
|
return Err("Server bind address cannot be empty".into());
|
|
}
|
|
if self.port == 0 {
|
|
return Err("Server port must be greater than 0".into());
|
|
}
|
|
self.certificate.validate()?;
|
|
if let Some(cors) = &self.cors {
|
|
cors.validate()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Validate for GrpcSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
if self.bind_address.is_empty() {
|
|
return Err("gRPC bind address cannot be empty".into());
|
|
}
|
|
if self.port == 0 {
|
|
return Err("gRPC port must be greater than 0".into());
|
|
}
|
|
self.certificate.validate()?;
|
|
if let Some(cors) = &self.cors {
|
|
cors.validate()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Validate for DatabaseSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
if self.url.is_empty() {
|
|
return Err("Database URL cannot be empty".into());
|
|
}
|
|
if let Some(max_connections) = self.max_connections
|
|
&& max_connections == 0
|
|
{
|
|
return Err("Max database connections must be greater than 0".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Validate for AuthSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
if self.jwt_secret.is_empty() {
|
|
return Err("JWT secret cannot be empty".into());
|
|
}
|
|
if self.jwt_expiration_hours == 0 {
|
|
return Err("JWT expiration hours must be greater than 0".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Validate for CertificateSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
let base_path = std::path::Path::new(&self.cert_dir);
|
|
if !base_path.exists() {
|
|
// create the cert directory if it doesn't exist
|
|
std::fs::create_dir_all(base_path).map_err(|e| {
|
|
format!(
|
|
"Failed to create certificate directory {:?}: {}",
|
|
base_path, e
|
|
)
|
|
})?;
|
|
}
|
|
let cert_path = self.cert_path.as_ref().map(|p| base_path.join(p));
|
|
let key_path = self.key_path.as_ref().map(|p| base_path.join(p));
|
|
if (cert_path.is_some() && key_path.is_none())
|
|
|| (cert_path.is_none() && key_path.is_some())
|
|
{
|
|
return Err("Both certificate and key paths must be provided for TLS".into());
|
|
}
|
|
if let (Some(cert_path), Some(key_path)) = (&cert_path, &key_path) {
|
|
if !std::path::Path::new(cert_path).exists() {
|
|
return Err(format!("Certificate file not found: {:?}", cert_path));
|
|
}
|
|
if !std::path::Path::new(key_path).exists() {
|
|
return Err(format!("Key file not found: {:?}", key_path));
|
|
}
|
|
}
|
|
|
|
// validate for SAN entries - must be valid DNS names or IP addresses
|
|
for dns in &self.san_dns {
|
|
if dns.to_string().is_empty() {
|
|
return Err("SAN DNS entries cannot be empty".into());
|
|
}
|
|
}
|
|
for ip in &self.san_ip {
|
|
if ip.is_unspecified() {
|
|
return Err("SAN IP entries cannot be unspecified".into());
|
|
}
|
|
}
|
|
// require at least one SAN entry for the generated certificate
|
|
if self.san_dns.is_empty() && self.san_ip.is_empty() {
|
|
return Err(
|
|
"At least one SAN entry (DNS or IP) must be provided for the certificate".into(),
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Validate for CorsSettings {
|
|
fn validate(&self) -> Result<(), ValidationError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn default_jwt_expiration_hours() -> u64 {
|
|
24
|
|
}
|
|
|
|
fn default_server_bind_address() -> String {
|
|
"0.0.0.0".into()
|
|
}
|
|
|
|
fn default_server_port() -> u16 {
|
|
8080
|
|
}
|
|
|
|
fn default_grpc_bind_address() -> String {
|
|
"0.0.0.0".into()
|
|
}
|
|
|
|
fn default_grpc_port() -> u16 {
|
|
50051
|
|
}
|
|
|
|
fn default_log_level() -> LevelFilter {
|
|
LevelFilter::INFO
|
|
}
|
|
|
|
fn default_cert_folder() -> String {
|
|
"./certs".into()
|
|
}
|
|
|
|
fn deserialize_level_filter<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
LevelFilter::from_str(&s).map_err(serde::de::Error::custom)
|
|
}
|
|
|
|
fn serialize_level_filter<S>(level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_str(&level.to_string())
|
|
}
|
|
|
|
fn deserialize_ia5string_vec<'de, D>(deserializer: D) -> Result<Vec<Ia5String>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let vec = Vec::<String>::deserialize(deserializer)?;
|
|
vec.into_iter()
|
|
.map(|s| Ia5String::try_from(s).map_err(serde::de::Error::custom))
|
|
.collect()
|
|
}
|
|
|
|
fn serialize_ia5string_vec<S>(vec: &Vec<Ia5String>, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
let string_vec: Vec<String> = vec.iter().map(|ia5| ia5.to_string()).collect();
|
|
string_vec.serialize(serializer)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_esnure_send_and_sync() {
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
|
assert_send_sync::<Settings>();
|
|
assert_send_sync::<ServerSettings>();
|
|
assert_send_sync::<DatabaseSettings>();
|
|
assert_send_sync::<GrpcSettings>();
|
|
assert_send_sync::<AuthSettings>();
|
|
assert_send_sync::<CertificateSettings>();
|
|
assert_send_sync::<CorsSettings>();
|
|
assert_send_sync::<LogSettings>();
|
|
}
|
|
}
|