diff --git a/apps/nxmesh-master/src/service/mod.rs b/apps/nxmesh-master/src/service/mod.rs index 1599a17..c0e1601 100644 --- a/apps/nxmesh-master/src/service/mod.rs +++ b/apps/nxmesh-master/src/service/mod.rs @@ -6,6 +6,7 @@ use crate::{connector::agent::AgentConnectorTrait, service::certificate::Certifi pub mod agent; pub mod certificate; +pub mod proxy; pub async fn start_master_server( settings: crate::config::settings::Settings, diff --git a/apps/nxmesh-master/src/service/proxy/mod.rs b/apps/nxmesh-master/src/service/proxy/mod.rs new file mode 100644 index 0000000..60223fe --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/mod.rs @@ -0,0 +1,12 @@ +use crate::service::proxy::types::{ProxyConfig, ProxyServiceResult}; + +pub(crate) mod nginx; +pub(crate) mod repo; + +pub mod service; +pub mod types; + +#[async_trait::async_trait] +pub trait ProxyServiceTrait: Send + Sync + 'static { + async fn get_proxy_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult; +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs new file mode 100644 index 0000000..6c16d47 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/access_rule.rs @@ -0,0 +1,7 @@ +use crate::db::entities::access_rule::Model as AccessRule; + +impl std::fmt::Display for AccessRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {};", self.r#type.clone(), self.ip_cidr.clone()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs new file mode 100644 index 0000000..ec79ece --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/cache_zone.rs @@ -0,0 +1,11 @@ +use crate::db::entities::cache_zone::Model as CacheZone; + +impl std::fmt::Display for CacheZone { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "proxy_cache_path {} levels=1:2 keys_zone={}:{};", + self.path, self.name, self.size_limit + ) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs new file mode 100644 index 0000000..2b8aac5 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_rule.rs @@ -0,0 +1,19 @@ +use crate::db::entities::limit_rule::Model as LimitRule; + +pub struct LimitRuleRender { + pub rule: LimitRule, + pub zone_name: String, +} + +impl std::fmt::Display for LimitRuleRender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "limit_req zone={}", self.zone_name)?; + if let Some(burst) = self.rule.burst { + write!(f, " burst={}", burst)?; + } + if self.rule.nodelay.unwrap_or(false) { + write!(f, " nodelay")?; + } + write!(f, ";") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs new file mode 100644 index 0000000..db62052 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/limit_zone.rs @@ -0,0 +1,11 @@ +use crate::db::entities::limit_zone::Model as LimitZone; + +impl std::fmt::Display for LimitZone { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "limit_req_zone {} zone={}:{};", + self.key, self.name, self.rate + ) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs new file mode 100644 index 0000000..d8a6cab --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/location_block.rs @@ -0,0 +1,44 @@ +use crate::db::entities::location_block::Model as LocationBlock; + +pub struct LocationBlockRender { + pub block: LocationBlock, + pub upstream_name: Option, + pub access_rules: Vec, + pub rewrite_rules: Vec, + pub proxy_setting: Option, + pub limit_rules: Vec, +} + +impl std::fmt::Display for LocationBlockRender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, " location {} {{", self.block.path_pattern)?; + + if let Some(ref upstream) = self.upstream_name { + writeln!(f, " proxy_pass http://{};", upstream)?; + writeln!(f, " proxy_set_header Host $host;")?; + writeln!(f, " proxy_set_header X-Real-IP $remote_addr;")?; + } + + if let Some(ref settings) = self.proxy_setting { + for line in settings.lines() { + if !line.is_empty() { + writeln!(f, "{}", line)?; + } + } + } + + for rule in &self.access_rules { + writeln!(f, " {}", rule)?; + } + + for rule in &self.rewrite_rules { + writeln!(f, " {}", rule)?; + } + + for rule in &self.limit_rules { + writeln!(f, " {}", rule)?; + } + + writeln!(f, " }}") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs new file mode 100644 index 0000000..e65f7ae --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/log_setting.rs @@ -0,0 +1,21 @@ +use crate::db::entities::log_setting::Model as LogSetting; + +impl std::fmt::Display for LogSetting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ref path) = self.access_log_path { + if let Some(ref level) = self.log_level { + writeln!(f, " access_log {} {};", path, level)?; + } else { + writeln!(f, " access_log {};", path)?; + } + } + if let Some(ref path) = self.error_log_path { + if let Some(ref level) = self.log_level { + writeln!(f, " error_log {} {};", path, level)?; + } else { + writeln!(f, " error_log {};", path)?; + } + } + Ok(()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs new file mode 100644 index 0000000..f2fcab8 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/mod.rs @@ -0,0 +1,11 @@ +mod access_rule; +mod cache_zone; +mod limit_rule; +mod limit_zone; +mod location_block; +mod log_setting; +mod proxy_setting; +mod rewrite_rule; +mod server_block; +mod ssl_certificate; +mod upstream; diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs new file mode 100644 index 0000000..f275bfd --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/proxy_setting.rs @@ -0,0 +1,35 @@ +use crate::db::entities::proxy_setting::Model as ProxySetting; + +pub struct ProxySettingRender { + pub setting: ProxySetting, + pub cache_zone_name: Option, +} + +impl ProxySettingRender { + fn render_timeout(value: Option, directive: &str) -> Option { + value.map(|v| format!(" {} {}s;", directive, v)) + } +} + +impl std::fmt::Display for ProxySettingRender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(line) = Self::render_timeout(self.setting.read_timeout, "proxy_read_timeout") + { + writeln!(f, "{}", line)?; + } + if let Some(line) = + Self::render_timeout(self.setting.connect_timeout, "proxy_connect_timeout") + { + writeln!(f, "{}", line)?; + } + if let Some(buffer) = self.setting.buffer_size { + writeln!(f, " proxy_buffer_size {};", buffer)?; + } + if self.setting.cache_enabled.unwrap_or(false) { + if let Some(ref zone_name) = self.cache_zone_name { + writeln!(f, " proxy_cache {};", zone_name)?; + } + } + Ok(()) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs new file mode 100644 index 0000000..1a2fa1a --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/rewrite_rule.rs @@ -0,0 +1,11 @@ +use crate::db::entities::rewrite_rule::Model as RewriteRule; + +impl std::fmt::Display for RewriteRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "rewrite {} {}", self.pattern, self.replacement)?; + if let Some(ref flag) = self.flag { + write!(f, " {}", flag)?; + } + write!(f, ";") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs new file mode 100644 index 0000000..d7f77e1 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/server_block.rs @@ -0,0 +1,49 @@ +use crate::db::entities::server_block::Model as ServerBlock; + +pub struct ServerBlockRender { + pub block: ServerBlock, + pub ssl_cert: Option, + pub locations: Vec, + pub access_rules: Vec, + pub log_setting: Option, +} + +impl std::fmt::Display for ServerBlockRender { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "server {{")?; + + if self.block.ssl_enabled.unwrap_or(false) { + writeln!(f, " listen {} ssl;", self.block.listen_port)?; + } else { + writeln!(f, " listen {};", self.block.listen_port)?; + } + + if let Some(ref names) = self.block.server_name { + if !names.is_empty() { + writeln!(f, " server_name {};", names.join(" "))?; + } + } + + if let Some(ref cert) = self.ssl_cert { + writeln!(f, "{}", cert)?; + } + + if let Some(ref log) = self.log_setting { + for line in log.lines() { + if !line.is_empty() { + writeln!(f, "{}", line)?; + } + } + } + + for rule in &self.access_rules { + writeln!(f, " {}", rule)?; + } + + for loc in &self.locations { + writeln!(f, "{}", loc)?; + } + + write!(f, "}}") + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs new file mode 100644 index 0000000..422d9dc --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/ssl_certificate.rs @@ -0,0 +1,8 @@ +use crate::db::entities::ssl_certificate::Model as SslCertificate; + +impl std::fmt::Display for SslCertificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, " ssl_certificate {};", self.cert_path)?; + writeln!(f, " ssl_certificate_key {};", self.key_path) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs b/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs new file mode 100644 index 0000000..5a3cc03 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/config/upstream.rs @@ -0,0 +1,7 @@ +use crate::db::entities::upstream::Model as Upstream; + +impl std::fmt::Display for Upstream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, " server {}:{};", self.target_host, self.target_port) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/nginx/mod.rs b/apps/nxmesh-master/src/service/proxy/nginx/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/nginx/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/apps/nxmesh-master/src/service/proxy/repo.rs b/apps/nxmesh-master/src/service/proxy/repo.rs new file mode 100644 index 0000000..3f531ad --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/repo.rs @@ -0,0 +1,444 @@ +use std::collections::HashMap; + +use sea_orm::{DatabaseConnection, prelude::*}; + +use crate::service::proxy::types::{ + Mergeable, OverrideRef, ProxyConfig, ProxyServiceError, ProxyServiceResult, ProxyType, +}; + +#[async_trait::async_trait] +pub trait ProxyRepo: Send + Sync + 'static { + // get the raw config for the given proxy_id. This should return the config of the given proxy_id without merging it with its parent configs (if any). + async fn get_proxy_raw_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult; + + // get the raw config for the given proxy_id. This should return the config of the given proxy_id and all its parent configs (if any) without merging them. The returned vector is ordered from the leaf (given proxy_id) down to the root config (most specific to least specific). + async fn get_proxy_raw_configs( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult>; + + // get the merged config for the given proxy_id. This should merge the config of the given proxy_id with its parent configs (if any) and return the final merged config. + async fn get_merged_proxy_config( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult; +} + +pub(crate) struct ProxyRepoImpl { + db: DatabaseConnection, +} + +impl ProxyRepoImpl { + pub fn new(db: DatabaseConnection) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl ProxyRepo for ProxyRepoImpl { + async fn get_proxy_raw_configs( + &self, + proxy_config_id: uuid::Uuid, + ) -> ProxyServiceResult> { + let mut proxy_config_id_frontier = vec![proxy_config_id]; + let mut visited = std::collections::HashSet::new(); + + let mut configs: Vec = Vec::new(); + + while let Some(current_id) = proxy_config_id_frontier.pop() { + if visited.contains(¤t_id) { + continue; + } + visited.insert(current_id); + // + let config = self.get_proxy_raw_config(current_id).await?; + // + if let Some(parent_ids) = &config.parent_config_id { + proxy_config_id_frontier.extend(parent_ids); + } + configs.push(config); + } + + Ok(configs) + } + + async fn get_proxy_raw_config(&self, proxy_id: uuid::Uuid) -> ProxyServiceResult { + let proxy_entity = crate::db::entities::proxy_config::Entity::find_by_id(proxy_id) + .one(&self.db) + .await? + .ok_or(ProxyServiceError::ConfigNotFound)?; + + // Resolve parent configs from config_inheritance + let parent_config_id = { + let inheritance = crate::db::entities::config_inheritance::Entity::find() + .filter(crate::db::entities::config_inheritance::Column::ChildConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + if inheritance.is_empty() { + None + } else { + Some( + inheritance + .into_iter() + .map(|ci| ci.parent_config_id) + .collect(), + ) + } + }; + + // Direct children of proxy_config + let server_blocks = crate::db::entities::server_block::Entity::find() + .filter(crate::db::entities::server_block::Column::ConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + + let upstreams = crate::db::entities::upstream::Entity::find() + .filter(crate::db::entities::upstream::Column::ConfigId.eq(proxy_id)) + .all(&self.db) + .await?; + + // Children of server_blocks + let server_block_ids: Vec = server_blocks.iter().map(|sb| sb.id).collect(); + + let (location_blocks, log_settings, server_access_rules) = if server_block_ids.is_empty() { + (vec![], vec![], vec![]) + } else { + ( + crate::db::entities::location_block::Entity::find() + .filter( + crate::db::entities::location_block::Column::ServerId + .is_in(server_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::log_setting::Entity::find() + .filter( + crate::db::entities::log_setting::Column::ServerId + .is_in(server_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::access_rule::Entity::find() + .filter( + crate::db::entities::access_rule::Column::ServerId.is_in(server_block_ids), + ) + .all(&self.db) + .await?, + ) + }; + + // Children of location_blocks + let location_block_ids: Vec = location_blocks.iter().map(|lb| lb.id).collect(); + + let (location_access_rules, limit_rules, proxy_settings, rewrite_rules) = + if location_block_ids.is_empty() { + (vec![], vec![], vec![], vec![]) + } else { + ( + crate::db::entities::access_rule::Entity::find() + .filter( + crate::db::entities::access_rule::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::limit_rule::Entity::find() + .filter( + crate::db::entities::limit_rule::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::proxy_setting::Entity::find() + .filter( + crate::db::entities::proxy_setting::Column::LocationId + .is_in(location_block_ids.clone()), + ) + .all(&self.db) + .await?, + crate::db::entities::rewrite_rule::Entity::find() + .filter( + crate::db::entities::rewrite_rule::Column::LocationId + .is_in(location_block_ids), + ) + .all(&self.db) + .await?, + ) + }; + + // Collect referenced IDs for zone and cert lookups + let limit_zone_ids: Vec = limit_rules.iter().map(|lr| lr.zone_id).collect(); + let cache_zone_ids: Vec = proxy_settings + .iter() + .filter_map(|ps| ps.cache_zone) + .collect(); + let ssl_cert_ids: Vec = server_blocks + .iter() + .filter_map(|sb| sb.ssl_cert_id) + .collect(); + + let limit_zones = if limit_zone_ids.is_empty() { + vec![] + } else { + crate::db::entities::limit_zone::Entity::find() + .filter(crate::db::entities::limit_zone::Column::Id.is_in(limit_zone_ids)) + .all(&self.db) + .await? + }; + + let cache_zones = if cache_zone_ids.is_empty() { + vec![] + } else { + crate::db::entities::cache_zone::Entity::find() + .filter(crate::db::entities::cache_zone::Column::Id.is_in(cache_zone_ids)) + .all(&self.db) + .await? + }; + + let ssl_certificates = if ssl_cert_ids.is_empty() { + vec![] + } else { + crate::db::entities::ssl_certificate::Entity::find() + .filter(crate::db::entities::ssl_certificate::Column::Id.is_in(ssl_cert_ids)) + .all(&self.db) + .await? + }; + + // ── Group child IDs by parent ──────────────────────────────────────── + + // location_block IDs grouped by server_id + let loc_block_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lb in &location_blocks { + map.entry(lb.server_id).or_default().push(OverrideRef { + id: lb.id, + override_of_id: lb.override_of_id, + }); + } + map + }; + + // log_setting IDs grouped by server_id + let log_setting_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ls in &log_settings { + map.entry(ls.server_id).or_default().push(OverrideRef { + id: ls.id, + override_of_id: ls.override_of_id, + }); + } + map + }; + + // server-level access_rule IDs grouped by server_id + let server_ar_ids_by_server: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ar in &server_access_rules { + if let Some(sid) = ar.server_id { + map.entry(sid).or_default().push(OverrideRef { + id: ar.id, + override_of_id: ar.override_of_id, + }); + } + } + map + }; + + // location-level access_rule IDs grouped by location_id + let loc_ar_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ar in &location_access_rules { + if let Some(lid) = ar.location_id { + map.entry(lid).or_default().push(OverrideRef { + id: ar.id, + override_of_id: ar.override_of_id, + }); + } + } + map + }; + + // limit_rule IDs grouped by location_id + let lr_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lr in &limit_rules { + map.entry(lr.location_id).or_default().push(OverrideRef { + id: lr.id, + override_of_id: lr.override_of_id, + }); + } + map + }; + + // proxy_setting IDs grouped by location_id + let ps_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for ps in &proxy_settings { + map.entry(ps.location_id).or_default().push(OverrideRef { + id: ps.id, + override_of_id: ps.override_of_id, + }); + } + map + }; + + // rewrite_rule IDs grouped by location_id + let rr_ids_by_location: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for rr in &rewrite_rules { + map.entry(rr.location_id).or_default().push(OverrideRef { + id: rr.id, + override_of_id: rr.override_of_id, + }); + } + map + }; + + // location_block IDs grouped by proxy_pass_upstream_id (reverse FK) + let loc_block_ids_by_upstream: HashMap> = { + let mut map: HashMap<_, Vec<_>> = HashMap::new(); + for lb in &location_blocks { + if let Some(up_id) = lb.proxy_pass_upstream_id { + map.entry(up_id).or_default().push(OverrideRef { + id: lb.id, + override_of_id: lb.override_of_id, + }); + } + } + map + }; + + // ── Build child-ID tuples for each config type ─────────────────────── + + let location_block_tuples: Vec<( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + )> = location_blocks + .into_iter() + .map(|lb| { + let ar_ids = loc_ar_ids_by_location + .get(&lb.id) + .cloned() + .unwrap_or_default(); + let lr_ids = lr_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + let ps_ids = ps_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + let rr_ids = rr_ids_by_location.get(&lb.id).cloned().unwrap_or_default(); + (lb, ar_ids, lr_ids, ps_ids, rr_ids) + }) + .collect(); + + let server_block_tuples: Vec<( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + )> = server_blocks + .into_iter() + .map(|sb| { + let ar_ids = server_ar_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let lb_ids = loc_block_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let ls_ids = log_setting_ids_by_server + .get(&sb.id) + .cloned() + .unwrap_or_default(); + let sc_ids: Vec = sb + .ssl_cert_id + .into_iter() + .map(|id| OverrideRef { + id, + override_of_id: None, + }) + .collect(); + (sb, ar_ids, lb_ids, ls_ids, sc_ids) + }) + .collect(); + + let upstream_tuples: Vec<(crate::db::entities::upstream::Model, Vec)> = + upstreams + .into_iter() + .map(|u| { + let lb_ids = loc_block_ids_by_upstream + .get(&u.id) + .cloned() + .unwrap_or_default(); + (u, lb_ids) + }) + .collect(); + + // Combine server-level and location-level access rules into one flat list + let all_access_rules: Vec = { + let mut ars = + Vec::with_capacity(server_access_rules.len() + location_access_rules.len()); + ars.extend(server_access_rules); + ars.extend(location_access_rules); + ars + }; + + Ok(ProxyConfig { + id: proxy_entity.id, + name: proxy_entity.name, + r#type: ProxyType::Nginx, + description: proxy_entity.description, + parent_config_id, + server_blocks: server_block_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + upstreams: upstream_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + location_blocks: location_block_tuples + .into_iter() + .map(|m| (m.0.id, m.into())) + .collect(), + access_rules: all_access_rules + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + cache_zones: cache_zones.into_iter().map(|m| (m.id, m.into())).collect(), + limit_rules: limit_rules.into_iter().map(|m| (m.id, m.into())).collect(), + limit_zones: limit_zones.into_iter().map(|m| (m.id, m.into())).collect(), + log_settings: log_settings.into_iter().map(|m| (m.id, m.into())).collect(), + proxy_settings: proxy_settings + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + rewrite_rules: rewrite_rules + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + ssl_certificates: ssl_certificates + .into_iter() + .map(|m| (m.id, m.into())) + .collect(), + }) + } + + async fn get_merged_proxy_config( + &self, + proxy_id: uuid::Uuid, + ) -> ProxyServiceResult { + let configs = self.get_proxy_raw_configs(proxy_id).await?; + + // configs is ordered [leaf, ..., root] (most specific first) + // self.merge(other) means self overrides other + // So start with leaf and merge each ancestor into it + let mut iter = configs.into_iter(); + let mut merged = iter.next().ok_or(ProxyServiceError::ConfigNotFound)?; + for config in iter { + merged.merge(config); + } + Ok(merged) + } +} diff --git a/apps/nxmesh-master/src/service/proxy/service.rs b/apps/nxmesh-master/src/service/proxy/service.rs new file mode 100644 index 0000000..e69de29 diff --git a/apps/nxmesh-master/src/service/proxy/types.rs b/apps/nxmesh-master/src/service/proxy/types.rs new file mode 100644 index 0000000..48cefe9 --- /dev/null +++ b/apps/nxmesh-master/src/service/proxy/types.rs @@ -0,0 +1,478 @@ +use std::collections::HashMap; + +#[derive(Debug)] +pub enum ProxyServiceError { + ConfigNotFound, + InvalidConfig, + DatabaseError(sea_orm::DbErr), +} + +impl From for ProxyServiceError { + fn from(err: sea_orm::DbErr) -> Self { + ProxyServiceError::DatabaseError(err) + } +} + +pub type ProxyServiceResult = Result; + +pub enum ProxyType { + Nginx, +} + +pub trait Mergeable { + // merge with other, self overrides other + fn merge(&mut self, other: T); +} + +#[derive(Debug, Clone)] +pub struct OverrideRef { + pub id: uuid::Uuid, + pub override_of_id: Option, +} + +pub fn merge_override_vecs( + mut child: Vec, + parent: Vec, +) -> Vec { + let overridden_ids: std::collections::HashSet = child + .iter() + .filter_map(|r| r.override_of_id) + .collect(); + for item in parent { + if !overridden_ids.contains(&item.id) { + child.push(item); + } + } + child +} + +pub trait Overridable { + fn override_of_id(&self) -> Option; +} + +pub struct ProxyConfig { + pub id: uuid::Uuid, + pub name: String, + pub r#type: ProxyType, + pub description: Option, + pub parent_config_id: Option>, + // + pub server_blocks: HashMap, + pub upstreams: HashMap, + pub access_rules: HashMap, + pub cache_zones: HashMap, + pub limit_rules: HashMap, + pub limit_zones: HashMap, + pub location_blocks: HashMap, + pub log_settings: HashMap, + pub proxy_settings: HashMap, + pub rewrite_rules: HashMap, + pub ssl_certificates: HashMap, +} + +impl Mergeable for ProxyConfig { + fn merge(&mut self, other: ProxyConfig) { + use std::collections::HashSet; + + macro_rules! merge_overridable_field { + ($field:ident) => { + let overridden: HashSet = self.$field + .values() + .filter_map(|v| v.override_of_id()) + .collect(); + for (id, value) in other.$field { + if !overridden.contains(&id) && !self.$field.contains_key(&id) { + self.$field.insert(id, value); + } + } + }; + } + + // server_blocks: merge matching entries (field-level), handle overrides + { + let overridden: HashSet = self.server_blocks + .values() + .filter_map(|sb| sb.override_of_id) + .collect(); + for (id, block) in other.server_blocks { + if let Some(self_block) = self.server_blocks.get_mut(&id) { + self_block.merge(block); + } else if !overridden.contains(&id) { + self.server_blocks.insert(id, block); + } + } + } + + merge_overridable_field!(upstreams); + merge_overridable_field!(access_rules); + merge_overridable_field!(cache_zones); + merge_overridable_field!(limit_rules); + merge_overridable_field!(limit_zones); + merge_overridable_field!(location_blocks); + merge_overridable_field!(log_settings); + merge_overridable_field!(proxy_settings); + merge_overridable_field!(rewrite_rules); + + // ssl_certificates: simple merge (no override_of_id) + for (id, cert) in other.ssl_certificates { + self.ssl_certificates.entry(id).or_insert(cert); + } + } +} + +pub struct ServerBlockConfig { + pub id: uuid::Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub override_of_id: Option, + // + pub access_rules: Vec, + pub location_blocks: Vec, + pub log_settings: Vec, + pub ssl_certificates: Vec, +} + +impl + From<( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + )> for ServerBlockConfig +{ + fn from( + (model, access_rules, location_blocks, log_settings, ssl_certificates): ( + crate::db::entities::server_block::Model, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + ServerBlockConfig { + id: model.id, + server_name: model.server_name, + listen_port: model.listen_port, + ssl_enabled: model.ssl_enabled, + override_of_id: model.override_of_id, + access_rules, + location_blocks, + log_settings, + ssl_certificates, + } + } +} + +impl Mergeable for ServerBlockConfig { + fn merge(&mut self, other: ServerBlockConfig) { + if let Some(server_name) = other.server_name { + self.server_name = Some(server_name); + } + self.listen_port = other.listen_port; + if let Some(ssl_enabled) = other.ssl_enabled { + self.ssl_enabled = Some(ssl_enabled); + } + self.override_of_id = other.override_of_id; + + self.access_rules = merge_override_vecs( + std::mem::take(&mut self.access_rules), + other.access_rules, + ); + self.location_blocks = merge_override_vecs( + std::mem::take(&mut self.location_blocks), + other.location_blocks, + ); + self.log_settings = merge_override_vecs( + std::mem::take(&mut self.log_settings), + other.log_settings, + ); + self.ssl_certificates = merge_override_vecs( + std::mem::take(&mut self.ssl_certificates), + other.ssl_certificates, + ); + } +} + +pub struct UpstreamConfig { + pub id: uuid::Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, + // + pub location_blocks: Vec, +} + +impl From<(crate::db::entities::upstream::Model, Vec)> for UpstreamConfig { + fn from( + (model, location_blocks): (crate::db::entities::upstream::Model, Vec), + ) -> Self { + UpstreamConfig { + id: model.id, + name: model.name, + target_host: model.target_host, + target_port: model.target_port, + metadata: model.metadata, + override_of_id: model.override_of_id, + location_blocks, + } + } +} + +pub struct AccessRuleConfig { + pub id: uuid::Uuid, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for AccessRuleConfig { + fn from(model: crate::db::entities::access_rule::Model) -> Self { + AccessRuleConfig { + id: model.id, + r#type: model.r#type, + ip_cidr: model.ip_cidr, + description: model.description, + priority: model.priority, + override_of_id: model.override_of_id, + } + } +} + +pub struct CacheZoneConfig { + pub id: uuid::Uuid, + pub name: String, + pub size: String, + pub override_of_id: Option, +} + +impl From for CacheZoneConfig { + fn from(model: crate::db::entities::cache_zone::Model) -> Self { + CacheZoneConfig { + id: model.id, + name: model.name, + size: model.size_limit, + override_of_id: model.override_of_id, + } + } +} + +pub struct LimitRuleConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub zone_id: uuid::Uuid, + pub burst: Option, + pub nodelay: Option, + pub is_deleted: bool, + + pub override_of_id: Option, +} + +impl From for LimitRuleConfig { + fn from(model: crate::db::entities::limit_rule::Model) -> Self { + LimitRuleConfig { + id: model.id, + location_id: model.location_id, + zone_id: model.zone_id, + burst: model.burst, + nodelay: model.nodelay, + is_deleted: model.is_deleted, + override_of_id: model.override_of_id, + } + } +} + +pub struct LimitZoneConfig { + pub id: uuid::Uuid, + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for LimitZoneConfig { + fn from(model: crate::db::entities::limit_zone::Model) -> Self { + LimitZoneConfig { + id: model.id, + name: model.name, + key: model.key, + rate: model.rate, + override_of_id: model.override_of_id, + } + } +} + +pub struct LocationBlockConfig { + pub id: uuid::Uuid, + pub server_id: uuid::Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, + // + pub access_rules: Vec, + pub limit_rules: Vec, + pub proxy_settings: Vec, + pub rewrite_rules: Vec, +} + +impl + From<( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + )> for LocationBlockConfig +{ + fn from( + (model, access_rules, limit_rules, proxy_settings, rewrite_rules): ( + crate::db::entities::location_block::Model, + Vec, + Vec, + Vec, + Vec, + ), + ) -> Self { + LocationBlockConfig { + id: model.id, + server_id: model.server_id, + path_pattern: model.path_pattern, + proxy_pass_upstream_id: model.proxy_pass_upstream_id, + metadata: model.metadata, + override_of_id: model.override_of_id, + access_rules, + limit_rules, + proxy_settings, + rewrite_rules, + } + } +} + +pub struct LogSettingConfig { + pub id: uuid::Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for LogSettingConfig { + fn from(model: crate::db::entities::log_setting::Model) -> Self { + LogSettingConfig { + id: model.id, + access_log_path: model.access_log_path, + error_log_path: model.error_log_path, + log_level: model.log_level, + override_of_id: model.override_of_id, + } + } +} + +pub struct SslCertificateConfig { + pub id: uuid::Uuid, + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: chrono::DateTime, +} + +impl From for SslCertificateConfig { + fn from(model: crate::db::entities::ssl_certificate::Model) -> Self { + SslCertificateConfig { + id: model.id, + name: model.name, + cert_path: model.cert_path, + key_path: model.key_path, + expiry_date: model.expiry_date.and_utc(), + } + } +} + +pub struct ProxySettingConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for ProxySettingConfig { + fn from(model: crate::db::entities::proxy_setting::Model) -> Self { + ProxySettingConfig { + id: model.id, + location_id: model.location_id, + read_timeout: model.read_timeout, + connect_timeout: model.connect_timeout, + buffer_size: model.buffer_size, + cache_enabled: model.cache_enabled, + cache_zone: model.cache_zone, + override_of_id: model.override_of_id, + } + } +} + +pub struct RewriteRuleConfig { + pub id: uuid::Uuid, + pub location_id: uuid::Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for RewriteRuleConfig { + fn from(model: crate::db::entities::rewrite_rule::Model) -> Self { + RewriteRuleConfig { + id: model.id, + location_id: model.location_id, + pattern: model.pattern, + replacement: model.replacement, + flag: model.flag, + priority: model.priority, + override_of_id: model.override_of_id, + } + } +} + +// ── Overridable implementations ── + +impl Overridable for ServerBlockConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for UpstreamConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for AccessRuleConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for CacheZoneConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for LimitRuleConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for LimitZoneConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for LocationBlockConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for LogSettingConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for ProxySettingConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +} +impl Overridable for RewriteRuleConfig { + fn override_of_id(&self) -> Option { self.override_of_id } +}