Skip to main content

platform/config/
mod.rs

1//! 统一配置管理系统
2//!
3//! 本模块是 Actrix 辅助服务配置的"单一真理之源"。
4//! 所有配置项的定义、文档、默认值都在这里统一管理。
5
6pub mod ais;
7pub mod bind;
8pub mod config_store;
9pub mod control;
10pub mod registry;
11pub mod resolver;
12pub mod services;
13pub mod signaling;
14pub mod signer;
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16pub mod turn;
17
18pub use crate::config::ais::AisConfig;
19pub use crate::config::bind::BindConfig;
20pub use crate::config::control::{AdminUiConfig, ControlConfig, ControlHead};
21pub use crate::config::services::ServicesConfig;
22pub use crate::config::signaling::SignalingConfig;
23pub use crate::config::turn::TurnConfig;
24use ::signer::storage::StorageBackend;
25use std::path::{Path, PathBuf};
26use url::Url;
27
28/// Actrix 辅助服务的主配置结构体
29///
30/// 这是系统的核心配置,包含了所有服务的配置信息。
31/// 配置文件使用 TOML 格式,支持完整的类型安全加载。
32#[derive(Debug, Serialize, Deserialize, Clone)]
33pub struct ActrixConfig {
34    /// Service enable flags (bitmask) - Primary switch for业务服务
35    ///
36    /// This is the primary control mechanism for enabling业务服务。
37    /// `control` 是常驻控制面能力,不受该位掩码控制。
38    ///
39    /// Bit positions(仅业务服务):
40    /// - Bit 0 (1): Signaling service
41    /// - Bit 1 (2): STUN service
42    /// - Bit 2 (4): TURN service
43    /// - Bit 3 (8): AIS (Actor Identity Service)
44    /// - Bit 4 (16): Signer
45    ///
46    /// Examples:
47    /// - `enable = 31` enables all services (1+2+4+8+16=31)
48    /// - `enable = 6` enables STUN + TURN (2+4=6)
49    /// - `enable = 7` enables Signaling + STUN + TURN (1+2+4=7)
50    #[serde(default = "default_enable")]
51    pub enable: u8,
52
53    /// 服务器实例名称
54    ///
55    /// 用于标识不同的服务器实例,在集群部署中用于区分节点。
56    /// 建议使用有意义的命名规则,如:actrix-01, actrix-prod-east-1 等。
57    pub name: String,
58
59    /// 运行环境标识
60    ///
61    /// 指定当前运行环境,影响安全策略和默认行为:
62    /// - "dev": 开发环境,允许 HTTP,证书检查较松
63    /// - "prod": 生产环境,强制 HTTPS,严格的安全检查
64    /// - "test": 测试环境,用于自动化测试
65    pub env: String,
66
67    /// 运行用户(可选)
68    ///
69    /// 指定服务运行的系统用户。服务会在绑定端口后切换到此用户运行,
70    /// 以提高安全性。留空则保持当前用户。
71    pub user: Option<String>,
72
73    /// 运行用户组(可选)
74    ///
75    /// 指定服务运行的系统用户组。与 user 配置配合使用。
76    pub group: Option<String>,
77
78    /// PID 文件路径(可选)
79    ///
80    /// 用于存储进程 ID 的文件路径。系统管理工具可以使用此文件
81    /// 来监控和管理服务进程。
82    pub pid: Option<String>,
83
84    /// 网络绑定配置
85    ///
86    /// 定义各种网络服务的绑定地址和端口配置。
87    pub bind: BindConfig,
88
89    /// TURN 服务特定配置
90    ///
91    /// TURN 中继服务的专用配置,包括公网地址、端口范围、认证域等。
92    pub turn: TurnConfig,
93
94    /// 位置标签
95    ///
96    /// 用于标识服务器的地理位置或逻辑分组,便于运维管理和监控。
97    /// 例如:us-west-1, office-beijing, edge-node-01
98    pub location_tag: String,
99
100    /// 控制面配置(常驻)
101    ///
102    /// 控制面始终可用,不受 `enable` 位掩码控制。
103    #[serde(default)]
104    pub control: ControlConfig,
105
106    /// 服务配置集合
107    ///
108    /// 包含所有业务服务的配置,每个服务可以独立配置自己的参数和依赖。
109    /// 采用服务级别的配置结构,实现高内聚低耦合。
110    #[serde(default)]
111    pub services: ServicesConfig,
112
113    /// SQLite 数据库文件存储目录路径
114    ///
115    /// 指定用于存储所有 SQLite 数据库文件的目录路径。
116    /// 主数据库文件将存储为 `{sqlite_path}/actrix.db`。
117    /// 包括 Realm 信息、访问控制列表、nonce 缓存等。
118    #[serde(
119        serialize_with = "serialize_pathbuf",
120        deserialize_with = "deserialize_pathbuf"
121    )]
122    pub sqlite_path: PathBuf,
123
124    /// Actrix 内部服务通信共享密钥
125    ///
126    /// 用于 Actrix 各服务之间的内部通信认证,如 AIS 与 Signer 之间的通信。
127    /// 这是系统级的内部认证密钥,仅用于服务间通信,不应用于对外业务。
128    ///
129    /// 注意:
130    /// - 此密钥仅限 Actrix 内部服务使用
131    /// - 不应用于 Realm 业务或外部 API 访问
132    /// - 在生产环境中应使用强随机密钥
133    /// - 字段名保留 actrix_shared_key 以保持向后兼容
134    pub actrix_shared_key: String,
135
136    /// 记录管线配置(日志 + 追踪)
137    ///
138    /// 将日志和 OpenTelemetry 追踪配置合并到统一的 recording 段,便于统一管理。
139    #[serde(default)]
140    pub recording: RecordingConfig,
141
142    /// Monitoring endpoints configuration
143    ///
144    /// Controls authentication for `/health` and `/metrics` endpoints.
145    /// Supports IP whitelist (CIDR) and/or HTTP Basic Auth (htpasswd file).
146    /// When both are configured, a request matching either is allowed (OR logic).
147    /// Per-service overrides replace the global config for that service.
148    #[serde(default)]
149    pub monitoring: MonitoringConfig,
150}
151
152/// 记录管线配置
153#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
154pub struct RecordingConfig {
155    /// 全局记录出口 URI(single sink)
156    ///
157    /// 作为所有通道默认出口;可被 [recording.<channel>] 覆盖。
158    ///
159    /// 支持:
160    /// - file://...
161    /// - otlp+http://...
162    /// - otlp+grpc://...
163    #[serde(default)]
164    pub sink: Option<String>,
165
166    /// OTLP 上报 service.name
167    #[serde(default = "default_recording_service_name")]
168    pub service_name: String,
169
170    /// observability 通道配置
171    #[serde(default = "default_observability_channel")]
172    pub observability: RecordingChannelConfig,
173    /// audit 通道配置
174    #[serde(default = "default_audit_channel")]
175    pub audit: RecordingChannelConfig,
176    /// security 通道配置
177    #[serde(default = "default_security_channel")]
178    pub security: RecordingChannelConfig,
179    /// operations 通道配置
180    #[serde(default = "default_operations_channel")]
181    pub operations: RecordingChannelConfig,
182}
183
184/// 单通道配置:语义过滤器 + 出口覆盖
185#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
186pub struct RecordingChannelConfig {
187    /// 通道语义过滤器
188    ///
189    /// 每个通道有自己的过滤词汇:
190    /// - observability: off / digest / detailed / full
191    /// - audit: off / mutations / all
192    /// - security: off / critical / high / medium / all
193    /// - operations: off / lifecycle / detailed
194    #[serde(default)]
195    pub filter: String,
196
197    /// 记录出口 URI(single sink)
198    ///
199    /// 支持:
200    /// - file://...
201    /// - otlp+http://...
202    /// - otlp+grpc://...
203    #[serde(default)]
204    pub sink: Option<String>,
205}
206
207fn default_audit_channel() -> RecordingChannelConfig {
208    RecordingChannelConfig {
209        filter: "mutations".to_string(),
210        sink: None,
211    }
212}
213
214fn default_security_channel() -> RecordingChannelConfig {
215    RecordingChannelConfig {
216        filter: "all".to_string(),
217        sink: None,
218    }
219}
220
221fn default_operations_channel() -> RecordingChannelConfig {
222    RecordingChannelConfig {
223        filter: "lifecycle".to_string(),
224        sink: None,
225    }
226}
227
228fn default_observability_channel() -> RecordingChannelConfig {
229    RecordingChannelConfig {
230        filter: "digest".to_string(),
231        sink: None,
232    }
233}
234
235impl Default for RecordingConfig {
236    fn default() -> Self {
237        Self {
238            sink: None,
239            service_name: default_recording_service_name(),
240            observability: default_observability_channel(),
241            audit: default_audit_channel(),
242            security: default_security_channel(),
243            operations: default_operations_channel(),
244        }
245    }
246}
247
248fn normalized_optional_uri(uri: &Option<String>) -> Option<String> {
249    uri.as_deref()
250        .map(str::trim)
251        .filter(|value| !value.is_empty())
252        .map(ToString::to_string)
253}
254
255fn any_recording_file_sink(recording: &RecordingConfig) -> bool {
256    [
257        &recording.sink,
258        &recording.observability.sink,
259        &recording.audit.sink,
260        &recording.security.sink,
261        &recording.operations.sink,
262    ]
263    .into_iter()
264    .filter_map(normalized_optional_uri)
265    .any(|sink| sink.starts_with("file://"))
266}
267
268fn validate_recording_sink_field(field: &str, value: &Option<String>, errors: &mut Vec<String>) {
269    let Some(uri) = normalized_optional_uri(value) else {
270        return;
271    };
272
273    let parsed = match Url::parse(&uri) {
274        Ok(parsed) => parsed,
275        Err(error) => {
276            errors.push(format!(
277                "Invalid URI in {field}: {uri} (parse error: {error})"
278            ));
279            return;
280        }
281    };
282
283    match parsed.scheme() {
284        "file" => {
285            if parsed.to_file_path().is_err() {
286                errors.push(format!(
287                    "Invalid file URI in {field}: {uri} (cannot convert to local file path)"
288                ));
289            }
290        }
291        "otlp+http" | "otlp+grpc" => {
292            if parsed.host_str().is_none() {
293                errors.push(format!(
294                    "Invalid OTLP URI in {field}: {uri} (host is required)"
295                ));
296            }
297        }
298        _ => {
299            errors.push(format!(
300                "Invalid URI scheme in {field}: expected file://, otlp+http:// or otlp+grpc://, got {}",
301                parsed.scheme()
302            ));
303        }
304    }
305}
306
307/// Monitoring endpoint auth configuration.
308///
309/// Both `allowed_ips` and `htpasswd_file` can be used simultaneously.
310/// When both are configured, a request matching either is allowed (OR logic).
311/// When neither is configured, endpoints are publicly accessible.
312#[derive(Debug, Clone, Serialize, Deserialize, Default)]
313pub struct MonitoringConfig {
314    /// IP whitelist in CIDR notation (e.g. "10.0.0.0/8", "127.0.0.1").
315    /// A bare IP without prefix length implies /32 (IPv4) or /128 (IPv6).
316    #[serde(default)]
317    pub allowed_ips: Vec<String>,
318
319    /// Path to an htpasswd-format file for HTTP Basic Auth.
320    /// Each line: `username:password` (plaintext).
321    #[serde(default)]
322    pub htpasswd_file: String,
323
324    /// Per-service overrides. When a service key is present here,
325    /// it completely replaces the global config for that service.
326    #[serde(default)]
327    pub services: std::collections::HashMap<String, MonitoringServiceOverride>,
328}
329
330/// Per-service monitoring auth override.
331#[derive(Debug, Clone, Serialize, Deserialize, Default)]
332pub struct MonitoringServiceOverride {
333    #[serde(default)]
334    pub allowed_ips: Vec<String>,
335    #[serde(default)]
336    pub htpasswd_file: String,
337}
338
339impl MonitoringConfig {
340    /// Returns true if no auth is configured (globally).
341    pub fn is_open(&self) -> bool {
342        self.allowed_ips.is_empty() && self.htpasswd_file.is_empty()
343    }
344
345    /// Get the effective auth config for a given service.
346    /// Per-service override replaces global when present.
347    pub fn effective_for(&self, service: &str) -> (&[String], &str) {
348        if let Some(ovr) = self.services.get(service) {
349            (ovr.allowed_ips.as_slice(), ovr.htpasswd_file.as_str())
350        } else {
351            (self.allowed_ips.as_slice(), self.htpasswd_file.as_str())
352        }
353    }
354
355    /// Returns true if no auth is configured for the given service.
356    pub fn is_open_for(&self, service: &str) -> bool {
357        let (ips, htpasswd) = self.effective_for(service);
358        ips.is_empty() && htpasswd.is_empty()
359    }
360}
361
362fn default_enable() -> u8 {
363    31 // Signaling(1) + STUN(2) + TURN(4) + AIS(8) + Signer(16)
364}
365
366fn default_recording_service_name() -> String {
367    "actrix".to_string()
368}
369
370fn serialize_pathbuf<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
371where
372    S: Serializer,
373{
374    path.display().to_string().serialize(serializer)
375}
376
377fn deserialize_pathbuf<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
378where
379    D: Deserializer<'de>,
380{
381    let s = String::deserialize(deserializer)?;
382    Ok(PathBuf::from(s))
383}
384
385impl Default for ActrixConfig {
386    fn default() -> Self {
387        Self {
388            enable: default_enable(), // 默认启用 STUN + TURN
389            name: "actrix-default".to_string(),
390            env: "dev".to_string(),
391            user: None,
392            group: None,
393            pid: Some("logs/actrix.pid".to_string()),
394            bind: BindConfig::default(),
395            turn: TurnConfig::default(),
396            location_tag: "default-location".to_string(),
397            control: ControlConfig::default(),
398            services: ServicesConfig::default(),
399            sqlite_path: PathBuf::from("database"),
400            actrix_shared_key: "XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg=".to_string(),
401            recording: RecordingConfig::default(),
402            monitoring: MonitoringConfig::default(),
403        }
404    }
405}
406
407// 服务启用标志位常量
408pub const ENABLE_SIGNALING: u8 = 0b00001;
409pub const ENABLE_STUN: u8 = 0b00010;
410pub const ENABLE_TURN: u8 = 0b00100;
411pub const ENABLE_AIS: u8 = 0b01000;
412pub const ENABLE_SIGNER: u8 = 0b10000;
413
414impl ActrixConfig {
415    /// 检查是否启用了信令服务
416    ///
417    /// Service is enabled if the ENABLE_SIGNALING bit is set in the enable bitmask.
418    pub fn is_signaling_enabled(&self) -> bool {
419        self.enable & ENABLE_SIGNALING != 0
420    }
421
422    /// 检查是否启用了 STUN 服务
423    pub fn is_stun_enabled(&self) -> bool {
424        self.enable & ENABLE_STUN != 0
425    }
426
427    /// 检查是否启用了 TURN 服务
428    pub fn is_turn_enabled(&self) -> bool {
429        self.enable & ENABLE_TURN != 0
430    }
431
432    /// 检查是否启用了 AIS (AId Issue Service) 身份认证服务
433    ///
434    /// Service is enabled if the ENABLE_AIS bit is set in the enable bitmask.
435    pub fn is_ais_enabled(&self) -> bool {
436        self.enable & ENABLE_AIS != 0
437    }
438
439    /// 检查是否启用了 Signer 密钥服务
440    ///
441    /// Service is enabled if the ENABLE_SIGNER bit is set in the enable bitmask.
442    pub fn is_signer_enabled(&self) -> bool {
443        self.enable & ENABLE_SIGNER != 0
444    }
445
446    /// 检查是否启用了 ICE 服务(STUN 或 TURN)
447    pub fn is_ice_enabled(&self) -> bool {
448        self.is_stun_enabled() || self.is_turn_enabled()
449    }
450
451    /// 当前控制面头类型
452    pub fn control_head(&self) -> ControlHead {
453        if self.control.grpc_api_enabled() && !self.control.admin_ui_enabled() {
454            ControlHead::GrpcApi
455        } else {
456            ControlHead::AdminUi
457        }
458    }
459
460    /// Admin UI 是否启用。
461    pub fn admin_ui_enabled(&self) -> bool {
462        self.control.admin_ui_enabled()
463    }
464
465    /// NodeAdminService gRPC API 是否启用。
466    pub fn grpc_api_enabled(&self) -> bool {
467        self.control.grpc_api_enabled()
468    }
469
470    /// 是否处于 superv 接管模式。
471    pub fn superv_managed(&self) -> bool {
472        self.control.superv_managed()
473    }
474
475    /// 获取 PID 文件路径,如果没有配置则使用默认值
476    pub fn get_pid_path(&self) -> Option<String> {
477        self.pid.clone().or_else(|| {
478            // 如果没有配置 pid,使用默认值 logs/actrix.pid
479            Some("logs/actrix.pid".to_string())
480        })
481    }
482
483    /// 获取 Actrix 内部服务通信共享密钥
484    ///
485    /// 此密钥用于 Actrix 系统内部服务间的认证通信,
486    /// 如 AIS 与 Signer 之间的服务调用。
487    ///
488    /// 注意:此密钥仅用于内部服务通信,不应用于对外业务
489    pub fn get_actrix_shared_key(&self) -> &str {
490        &self.actrix_shared_key
491    }
492
493    /// 返回记录管线配置引用
494    pub fn recording_config(&self) -> &RecordingConfig {
495        &self.recording
496    }
497
498    /// 返回 OTLP service.name
499    pub fn recording_service_name(&self) -> &str {
500        &self.recording.service_name
501    }
502
503    /// 获取复合日志/追踪过滤级别,优先使用 RUST_LOG。
504    ///
505    /// With semantic filters, channel targets are set to TRACE so that
506    /// all events reach the recording layer (which applies its own gate).
507    /// The base level stays `info` to suppress third-party noise.
508    pub fn get_filter_level(&self) -> String {
509        if let Ok(rust_log) = std::env::var("RUST_LOG") {
510            let trimmed = rust_log.trim().to_string();
511            if !trimmed.is_empty() {
512                return trimmed;
513            }
514        }
515
516        "info,actrix::observability=trace,actrix::audit=trace,actrix::security=trace,actrix::operations=trace".to_string()
517    }
518
519    /// 从文件加载配置
520    pub fn from_file<P: AsRef<std::path::Path>>(
521        path: P,
522    ) -> Result<Self, Box<dyn std::error::Error>> {
523        let path_ref = path.as_ref();
524
525        // Check if file exists
526        if !path_ref.exists() {
527            return Err(format!("Configuration file does not exist: {path_ref:?}").into());
528        }
529
530        // Check if path is a file, not a directory
531        if !path_ref.is_file() {
532            return Err(format!("Path is not a valid file: {path_ref:?}").into());
533        }
534
535        // Read file content
536        let content = std::fs::read_to_string(path_ref)?;
537
538        // Parse TOML content
539        let config: ActrixConfig = toml::from_str(&content)?;
540
541        Ok(config)
542    }
543
544    /// 从 TOML 字符串加载配置
545    pub fn from_toml(content: &str) -> Result<Self, toml::de::Error> {
546        toml::from_str(content)
547    }
548
549    /// 将配置序列化为 TOML 字符串
550    pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
551        toml::to_string(self)
552    }
553
554    /// 验证配置有效性
555    ///
556    /// 检查所有配置项的合法性,包括:
557    /// - 必需字段是否存在
558    /// - 数值范围是否合理
559    /// - 文件路径是否有效
560    /// - 服务配置是否一致
561    pub fn validate(&self) -> Result<(), Vec<String>> {
562        let mut errors = Vec::new();
563
564        // 验证位掩码值范围 (0-31, 5 bits)
565        if self.enable > 31 {
566            errors.push(format!(
567                "Invalid enable bitmask value: {}. Must be between 0 and 31 (5 bits)",
568                self.enable
569            ));
570        }
571
572        // 验证实例名称
573        if self.name.trim().is_empty() {
574            errors.push("Instance name cannot be empty".to_string());
575        }
576
577        // 验证环境
578        if !["dev", "prod", "test"].contains(&self.env.as_str()) {
579            errors.push(format!(
580                "Invalid environment '{}', must be one of: dev, prod, test",
581                self.env
582            ));
583        }
584
585        // control 始终挂载在主 HTTP 端口,必须存在绑定。
586        if self.bind.http.is_none() {
587            errors.push(
588                "Control plane requires bind.http because /admin is always served".to_string(),
589            );
590        }
591
592        // 验证各通道语义过滤器(空字符串 = 使用通道默认值,跳过验证)
593        {
594            let check = |name: &str, value: &str, valid: &[&str]| -> Option<String> {
595                if value.is_empty() || valid.contains(&value) {
596                    None
597                } else {
598                    Some(format!(
599                        "Invalid recording.{name}.filter '{value}', must be one of: {}",
600                        valid.join(", ")
601                    ))
602                }
603            };
604            if let Some(e) = check(
605                "observability",
606                &self.recording.observability.filter,
607                &["off", "digest", "detailed", "full"],
608            ) {
609                errors.push(e);
610            }
611            if let Some(e) = check(
612                "audit",
613                &self.recording.audit.filter,
614                &["off", "mutations", "all"],
615            ) {
616                errors.push(e);
617            }
618            if let Some(e) = check(
619                "security",
620                &self.recording.security.filter,
621                &["off", "critical", "high", "medium", "all"],
622            ) {
623                errors.push(e);
624            }
625            if let Some(e) = check(
626                "operations",
627                &self.recording.operations.filter,
628                &["off", "lifecycle", "detailed"],
629            ) {
630                errors.push(e);
631            }
632        }
633
634        if self.recording.service_name.trim().is_empty() {
635            errors.push("recording.service_name cannot be empty".to_string());
636        }
637
638        // 验证新的 URI 记录出口配置(single sink, global + per-channel)
639        validate_recording_sink_field("recording.sink", &self.recording.sink, &mut errors);
640        validate_recording_sink_field(
641            "recording.observability.sink",
642            &self.recording.observability.sink,
643            &mut errors,
644        );
645        validate_recording_sink_field(
646            "recording.audit.sink",
647            &self.recording.audit.sink,
648            &mut errors,
649        );
650        validate_recording_sink_field(
651            "recording.security.sink",
652            &self.recording.security.sink,
653            &mut errors,
654        );
655        validate_recording_sink_field(
656            "recording.operations.sink",
657            &self.recording.operations.sink,
658            &mut errors,
659        );
660
661        // 验证 actrix_shared_key
662        if self.actrix_shared_key.contains("default") || self.actrix_shared_key.contains("change") {
663            errors.push("Security warning: actrix_shared_key appears to be a default value. Please change it!".to_string());
664        }
665        if self.actrix_shared_key.len() < 16 {
666            errors.push("Security warning: actrix_shared_key is too short, recommend at least 16 characters".to_string());
667        }
668
669        // 验证 SQLite 路径
670        if self
671            .sqlite_path
672            .to_str()
673            .map(|s| s.trim().is_empty())
674            .unwrap_or(true)
675        {
676            errors.push("SQLite database path cannot be empty".to_string());
677        }
678
679        // 验证 ICE 配置(如果启用 STUN 或 TURN)
680        if self.is_ice_enabled() {
681            if self.bind.ice.advertised_ip.trim().is_empty() {
682                errors.push(
683                    "bind.ice.advertised_ip is required when ICE services are enabled".to_string(),
684                );
685            }
686            // 验证 advertised_ip 格式
687            if self
688                .bind
689                .ice
690                .advertised_ip
691                .parse::<std::net::IpAddr>()
692                .is_err()
693            {
694                errors.push(format!(
695                    "Invalid bind.ice.advertised_ip '{}', must be a valid IP address",
696                    self.bind.ice.advertised_ip
697                ));
698            }
699        }
700
701        // 验证 TURN 配置(如果启用)
702        if self.is_turn_enabled() && self.turn.realm.trim().is_empty() {
703            errors.push("TURN realm is required when TURN is enabled".to_string());
704        }
705
706        // 验证 Signer 配置(如果启用)
707        if self.is_signer_enabled() {
708            if let Some(ref signer_cfg) = self.services.signer {
709                // 验证存储配置
710                match signer_cfg.storage.backend {
711                    StorageBackend::Sqlite => {}
712                    StorageBackend::Postgres => {
713                        if signer_cfg.storage.postgres.is_none() {
714                            errors.push(
715                                "Signer is configured to use PostgreSQL but postgres config is missing"
716                                    .to_string(),
717                            );
718                        }
719                    }
720                }
721            } else {
722                // Signer 位掩码已设置但 services.signer 配置缺失
723                errors.push(
724                    "Signer service is enabled (ENABLE_SIGNER bit is set) but services.signer configuration is missing".to_string(),
725                );
726            }
727        }
728
729        // 验证 AIS 配置(如果启用)
730        if self.is_ais_enabled() {
731            // 检查是否能获取 KS 配置(显式配置或自动默认)
732            if let Some(ref ais) = self.services.ais {
733                if ais.get_signer_client_config(self).is_none() {
734                    errors.push(
735                        "AIS service is enabled but no Signer available: \
736                        either configure services.ais.dependencies.signer or enable local Signer service"
737                            .to_string(),
738                    );
739                }
740                // 验证 renewal_token_secret 配置
741                if ais.server.renewal_token_secret.is_empty() {
742                    errors.push("services.ais.server.renewal_token_secret is required".to_string());
743                } else {
744                    match base64::Engine::decode(
745                        &base64::engine::general_purpose::STANDARD,
746                        &ais.server.renewal_token_secret,
747                    ) {
748                        Ok(decoded) if decoded.len() >= 32 => {}
749                        Ok(decoded) => {
750                            errors.push(format!(
751                                "services.ais.server.renewal_token_secret must decode to \
752                                 at least 32 bytes, got {} byte(s)",
753                                decoded.len()
754                            ));
755                        }
756                        Err(e) => {
757                            errors.push(format!(
758                                "services.ais.server.renewal_token_secret is not valid \
759                                 base64: {e}"
760                            ));
761                        }
762                    }
763                }
764            } else {
765                // AIS 位掩码已设置但 services.ais 配置缺失
766                errors.push(
767                    "AIS service is enabled (ENABLE_AIS bit is set) but services.ais configuration is missing".to_string(),
768                );
769            }
770        }
771
772        // 验证 Signaling 配置(如果启用)
773        if self.is_signaling_enabled() {
774            if let Some(ref signaling) = self.services.signaling {
775                if signaling.dependencies.signer.is_none()
776                    && !(self.is_signer_enabled() && self.services.signer.is_some())
777                {
778                    errors.push(
779                    "Signaling dependencies.signer is not configured; enable Signer (bitmask + services.signer) to use local defaults"
780                        .to_string(),
781                );
782                }
783
784                if signaling.dependencies.ais.is_none()
785                    && !(self.is_ais_enabled() && self.services.ais.is_some())
786                {
787                    errors.push(
788                    "Signaling dependencies.ais is not configured; enable AIS (bitmask + services.ais) to use local defaults"
789                        .to_string(),
790                );
791                }
792            } else {
793                errors.push(
794                    "Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing"
795                        .to_string(),
796                );
797            }
798        }
799
800        // 生产环境额外检查
801        if self.env == "prod" {
802            // 生产环境应使用 HTTPS (bind.http 必须存在且 is_tls() == true)
803            if let Some(ref http) = self.bind.http {
804                if !http.is_tls() {
805                    errors.push(
806                        "Production environment should enable HTTPS (configure cert and key in bind.http)".to_string(),
807                    );
808                }
809            } else {
810                errors.push("Production environment requires bind.http configuration".to_string());
811            }
812
813            // 生产环境建议至少配置一个 file:// 出口
814            if !any_recording_file_sink(&self.recording) {
815                errors.push("Warning: Production environment should configure recording.sink (file://...) or channel-specific recording.<channel>.sink".to_string());
816            }
817        }
818
819        if let Err(e) = self.control.validate() {
820            errors.push(format!("Control configuration error: {e}"));
821        }
822
823        if errors.is_empty() {
824            Ok(())
825        } else {
826            Err(errors)
827        }
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834    use ::signer::SignerServiceConfig;
835
836    fn valid_ais_server_config() -> ais::AisServerConfig {
837        ais::AisServerConfig {
838            renewal_token_secret: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string(),
839            ..Default::default()
840        }
841    }
842
843    #[test]
844    fn example_config_loads_and_validates() {
845        let path =
846            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../config.example.toml");
847        let cfg = ActrixConfig::from_file(&path).expect("example config should parse");
848        cfg.validate().expect("example config should validate");
849        assert!(
850            cfg.admin_ui_enabled(),
851            "example should have admin_ui enabled"
852        );
853        assert!(
854            !cfg.grpc_api_enabled(),
855            "example should have grpc_api disabled by default"
856        );
857    }
858
859    #[test]
860    fn test_default_config() {
861        let config = ActrixConfig::default();
862        assert_eq!(config.enable, 31); // 默认启用所有服务
863        assert_eq!(config.name, "actrix-default");
864        assert_eq!(config.env, "dev");
865        assert!(config.is_signaling_enabled());
866        assert!(config.is_stun_enabled());
867        assert!(config.is_turn_enabled());
868        assert!(config.is_ais_enabled());
869        assert!(config.is_signer_enabled());
870        assert_eq!(config.recording.service_name, "actrix");
871        assert!(config.recording.sink.is_none());
872        assert_eq!(config.recording.observability.filter, "digest");
873        assert_eq!(config.recording.audit.filter, "mutations");
874        assert_eq!(config.recording.security.filter, "all");
875        assert_eq!(config.recording.operations.filter, "lifecycle");
876    }
877
878    #[test]
879    fn test_recording_global_plus_spec_deserialize() {
880        let mut config = ActrixConfig::default();
881        config.recording.sink = Some("file:///tmp/actrix.log".to_string());
882        config.recording.audit.sink = Some("otlp+http://127.0.0.1:4318/v1/logs".to_string());
883        config.recording.security.sink = Some("otlp+grpc://127.0.0.1:4317".to_string());
884
885        let toml = config.to_toml().expect("config should serialize");
886        let parsed = ActrixConfig::from_toml(&toml).expect("config should deserialize");
887        assert_eq!(
888            parsed.recording.sink.as_deref(),
889            Some("file:///tmp/actrix.log")
890        );
891        assert_eq!(
892            parsed.recording.audit.sink.as_deref(),
893            Some("otlp+http://127.0.0.1:4318/v1/logs")
894        );
895        assert_eq!(
896            parsed.recording.security.sink.as_deref(),
897            Some("otlp+grpc://127.0.0.1:4317")
898        );
899    }
900
901    #[test]
902    fn test_recording_sink_validation_rejects_invalid_scheme() {
903        let mut config = ActrixConfig::default();
904        config.recording.sink = Some("http://127.0.0.1:4317".to_string());
905
906        let errors = config.validate().expect_err("validation should fail");
907        assert!(
908            errors
909                .iter()
910                .any(|error| error.contains("Invalid URI scheme in recording.sink")),
911            "expected invalid scheme error, got: {errors:?}"
912        );
913    }
914
915    #[test]
916    fn test_recording_sink_validation_accepts_supported_schemes() {
917        let mut config = ActrixConfig {
918            enable: 0,
919            ..Default::default()
920        };
921        config.control.admin_ui.password = "testpassword123".to_string();
922        config.recording.sink = Some("file:///tmp/actrix.log".to_string());
923        config.recording.audit.sink = Some("otlp+http://127.0.0.1:4318/v1/logs".to_string());
924        config.recording.security.sink = Some("otlp+grpc://127.0.0.1:4317".to_string());
925
926        assert!(
927            config.validate().is_ok(),
928            "supported sink schemes should pass validation"
929        );
930    }
931
932    #[test]
933    fn test_recording_sink_validation_rejects_otlp_uri_without_host() {
934        let mut config = ActrixConfig::default();
935        config.recording.sink = Some("otlp+grpc:///v1/traces".to_string());
936
937        let errors = config.validate().expect_err("validation should fail");
938        assert!(
939            errors
940                .iter()
941                .any(|error| error.contains("Invalid OTLP URI in recording.sink")),
942            "expected missing-host error, got: {errors:?}"
943        );
944    }
945
946    #[test]
947    fn test_recording_sink_validation_rejects_invalid_file_uri() {
948        let mut config = ActrixConfig::default();
949        config.recording.sink = Some("file://relative-path.log".to_string());
950
951        let errors = config.validate().expect_err("validation should fail");
952        assert!(
953            errors
954                .iter()
955                .any(|error| error.contains("Invalid file URI in recording.sink")),
956            "expected invalid file-uri error, got: {errors:?}"
957        );
958    }
959
960    #[test]
961    fn test_toml_serialization() {
962        let config = ActrixConfig::default();
963        let toml_str = config.to_toml().unwrap();
964        assert!(toml_str.contains("enable = 31")); // all services
965        assert!(toml_str.contains("name = \"actrix-default\""));
966        assert!(
967            toml_str
968                .contains("actrix_shared_key = \"XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg=\"")
969        );
970
971        let parsed_config = ActrixConfig::from_toml(&toml_str).unwrap();
972        assert_eq!(parsed_config.enable, config.enable);
973        assert_eq!(parsed_config.name, config.name);
974        assert_eq!(parsed_config.actrix_shared_key, config.actrix_shared_key);
975    }
976
977    #[test]
978    fn test_actrix_shared_key() {
979        let config = ActrixConfig::default();
980        assert_eq!(
981            config.get_actrix_shared_key(),
982            "XDDYE8d+yMfdXcdWMrXprcUk2uzjnmoX6nCfFw1gGIg="
983        );
984
985        // 测试自定义共享密钥
986        let mut custom_config = config;
987        custom_config.actrix_shared_key = "custom-shared-key".to_string();
988        assert_eq!(custom_config.get_actrix_shared_key(), "custom-shared-key");
989    }
990
991    #[test]
992    fn test_service_flags() {
993        let mut config = ActrixConfig {
994            enable: 0,
995            ..ActrixConfig::default()
996        };
997
998        // Test Signaling service: bitmask-only control
999        // Case 1: Bitmask not set, service not enabled
1000        config.enable = 0;
1001        config.services.signaling = Some(SignalingConfig {
1002            server: signaling::SignalingServerConfig::default(),
1003            dependencies: signaling::SignalingDependencies::default(),
1004        });
1005        assert!(!config.is_signaling_enabled());
1006
1007        // Case 2: Bitmask set -> enabled (regardless of services.* config)
1008        config.enable = ENABLE_SIGNALING;
1009        assert!(config.is_signaling_enabled());
1010
1011        // Case 3: Bitmask set, with services.* config -> still enabled
1012        config.services.signaling = Some(SignalingConfig {
1013            server: signaling::SignalingServerConfig::default(),
1014            dependencies: signaling::SignalingDependencies::default(),
1015        });
1016        assert!(config.is_signaling_enabled());
1017
1018        // Case 4: Bitmask set, no services.* config -> enabled
1019        config.services.signaling = None;
1020        assert!(config.is_signaling_enabled());
1021
1022        // Test AIS service: bitmask-only control
1023        config.enable = 0;
1024        config.services.ais = Some(AisConfig {
1025            server: valid_ais_server_config(),
1026            dependencies: ais::AisDependencies::default(),
1027        });
1028        assert!(!config.is_ais_enabled());
1029
1030        // Case 2: Bitmask set -> enabled (regardless of services.* config)
1031        config.enable = ENABLE_AIS;
1032        assert!(config.is_ais_enabled());
1033
1034        // Case 3: Bitmask set, with services.* config -> still enabled
1035        config.services.ais = Some(AisConfig {
1036            server: valid_ais_server_config(),
1037            dependencies: ais::AisDependencies::default(),
1038        });
1039        assert!(config.is_ais_enabled());
1040
1041        // Case 4: Bitmask set, no services.* config -> enabled
1042        config.services.ais = None;
1043        assert!(config.is_ais_enabled());
1044
1045        // Test KS service: bitmask-only control
1046        config.enable = 0;
1047        config.services.signer = Some(SignerServiceConfig {
1048            ..Default::default()
1049        });
1050        assert!(!config.is_signer_enabled());
1051
1052        // Case 2: Bitmask set -> enabled (regardless of services.* config)
1053        config.enable = ENABLE_SIGNER;
1054        assert!(config.is_signer_enabled());
1055
1056        // Case 3: Bitmask set, with services.* config -> still enabled
1057        config.services.signer = Some(SignerServiceConfig {
1058            ..Default::default()
1059        });
1060        assert!(config.is_signer_enabled());
1061
1062        // Case 4: Bitmask set, no services.* config -> enabled
1063        config.services.signer = None;
1064        assert!(config.is_signer_enabled());
1065
1066        // Test ICE services (STUN/TURN use bitmask only)
1067        config.enable = ENABLE_STUN;
1068        assert!(config.is_stun_enabled());
1069        assert!(config.is_ice_enabled());
1070
1071        config.enable = ENABLE_TURN;
1072        assert!(config.is_turn_enabled());
1073        assert!(config.is_ice_enabled());
1074    }
1075
1076    #[test]
1077    fn test_ais_auto_ks_config() {
1078        let mut config = ActrixConfig {
1079            enable: ENABLE_SIGNER | ENABLE_AIS,
1080            ..ActrixConfig::default()
1081        };
1082
1083        // 场景 1: 启用本地 KS,AIS 不配置 KS 客户端,应自动使用本地 KS
1084        config.enable = ENABLE_SIGNER | ENABLE_AIS; // Enable both KS and AIS via bitmask
1085        config.services.signer = Some(SignerServiceConfig {
1086            ..Default::default()
1087        });
1088
1089        config.services.ais = Some(AisConfig {
1090            server: valid_ais_server_config(),
1091            dependencies: ais::AisDependencies { signer: None }, // 未配置 KS
1092        });
1093
1094        // 应该能获取到自动生成的 KS 配置
1095        let ks_config = config
1096            .services
1097            .ais
1098            .as_ref()
1099            .unwrap()
1100            .get_signer_client_config(&config);
1101        assert!(ks_config.is_some());
1102        let ks_config = ks_config.unwrap();
1103        // KS gRPC 复用主 HTTP 端口(默认 8080)
1104        assert_eq!(ks_config.endpoint, "http://127.0.0.1:8080");
1105
1106        // 场景 2: 显式配置 KS 客户端,应使用显式配置
1107        config.services.ais = Some(AisConfig {
1108            server: valid_ais_server_config(),
1109            dependencies: ais::AisDependencies {
1110                signer: Some(crate::config::signer::SignerClientConfig {
1111                    endpoint: "http://remote-ks:8080".to_string(),
1112                    timeout_seconds: 10,
1113                    enable_tls: false,
1114                    tls_domain: None,
1115                    ca_cert: None,
1116                    client_cert: None,
1117                    client_key: None,
1118                }),
1119            },
1120        });
1121
1122        let ks_config = config
1123            .services
1124            .ais
1125            .as_ref()
1126            .unwrap()
1127            .get_signer_client_config(&config);
1128        assert!(ks_config.is_some());
1129        let ks_config = ks_config.unwrap();
1130        assert_eq!(ks_config.endpoint, "http://remote-ks:8080"); // 使用显式配置
1131
1132        // 场景 3: 没有本地 KS,也没有显式配置,应返回 None
1133        config.services.signer = None;
1134        config.services.ais = Some(AisConfig {
1135            server: valid_ais_server_config(),
1136            dependencies: ais::AisDependencies { signer: None },
1137        });
1138        config.enable = ENABLE_AIS; // Enable AIS via bitmask
1139
1140        let ks_config = config
1141            .services
1142            .ais
1143            .as_ref()
1144            .unwrap()
1145            .get_signer_client_config(&config);
1146        assert!(ks_config.is_none());
1147    }
1148
1149    #[test]
1150    fn test_signaling_auto_ks_config() {
1151        let mut config = ActrixConfig {
1152            enable: ENABLE_SIGNER | ENABLE_SIGNALING,
1153            ..ActrixConfig::default()
1154        };
1155
1156        // 启用本地 KS 和 Signaling
1157        config.enable = ENABLE_SIGNER | ENABLE_SIGNALING; // Enable both via bitmask
1158        config.services.signer = Some(SignerServiceConfig {
1159            ..Default::default()
1160        });
1161
1162        config.services.signaling = Some(SignalingConfig {
1163            server: signaling::SignalingServerConfig::default(),
1164            dependencies: signaling::SignalingDependencies {
1165                signer: None,
1166                ais: None,
1167            },
1168        });
1169
1170        // Signaling 应该能获取到自动生成的 KS 配置
1171        let ks_config = config
1172            .services
1173            .signaling
1174            .as_ref()
1175            .unwrap()
1176            .get_signer_client_config(&config);
1177        assert!(ks_config.is_some());
1178        let ks_config = ks_config.unwrap();
1179        // KS gRPC 复用主 HTTP 端口(默认 8080)
1180        assert_eq!(ks_config.endpoint, "http://127.0.0.1:8080");
1181    }
1182
1183    #[test]
1184    fn test_validate_bitmask_consistency() {
1185        let mut config = ActrixConfig {
1186            enable: ENABLE_AIS,
1187            ..ActrixConfig::default()
1188        };
1189
1190        // Case 1: AIS bitmask set but services.ais config missing - should warn
1191        config.enable = ENABLE_AIS;
1192        config.services.ais = None;
1193
1194        let result = config.validate();
1195        assert!(result.is_err());
1196        let errors = result.unwrap_err();
1197        assert!(errors.iter().any(|e| e.contains(
1198            "AIS service is enabled (ENABLE_AIS bit is set) but services.ais configuration is missing"
1199        )));
1200
1201        // Case 2: Signer bitmask set but services.signer config missing - should warn
1202        config.enable = ENABLE_SIGNER;
1203        config.services.ais = None;
1204        config.services.signer = None;
1205
1206        let result = config.validate();
1207        assert!(result.is_err());
1208        let errors = result.unwrap_err();
1209        assert!(errors.iter().any(|e| {
1210            e.contains("Signer service is enabled (ENABLE_SIGNER bit is set) but services.signer configuration is missing")
1211        }));
1212
1213        // Case 3: Signaling bitmask set but services.signaling config missing - should error
1214        config.enable = ENABLE_SIGNALING;
1215        config.services.signaling = None;
1216        config.services.signer = None;
1217
1218        let result = config.validate();
1219        assert!(result.is_err());
1220        let errors = result.unwrap_err();
1221        assert!(errors.iter().any(|e| {
1222            e.contains("Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing")
1223        }));
1224
1225        // Case 4: Signaling enabled with config present - should pass (if other validations pass)
1226        // Note: Unlike AIS, Signaling validation does not check KS availability
1227        config.enable = ENABLE_SIGNALING;
1228        config.services.signaling = Some(SignalingConfig {
1229            server: signaling::SignalingServerConfig::default(),
1230            dependencies: signaling::SignalingDependencies {
1231                signer: None,
1232                ais: None,
1233            },
1234        });
1235        config.services.signer = None; // No local KS
1236
1237        let result = config.validate();
1238        // Signaling with config should not have bitmask consistency errors
1239        // (may have other validation errors like sqlite_path, etc.)
1240        if let Err(errors) = result {
1241            assert!(
1242                !errors
1243                    .iter()
1244                    .any(|e| e.contains("Signaling service is enabled (ENABLE_SIGNALING bit is set) but services.signaling configuration is missing"))
1245            );
1246            assert!(
1247                !errors
1248                    .iter()
1249                    .any(|e| e.contains("bit is not set in enable bitmask"))
1250            );
1251        }
1252
1253        // Case 5: Bitmask set and services.* config present - should pass (if other validations pass)
1254        config.enable = ENABLE_AIS | ENABLE_SIGNER;
1255        config.services.ais = Some(AisConfig {
1256            server: valid_ais_server_config(),
1257            dependencies: ais::AisDependencies {
1258                signer: Some(crate::config::signer::SignerClientConfig {
1259                    endpoint: "http://127.0.0.1:8080".to_string(),
1260                    timeout_seconds: 10,
1261                    enable_tls: false,
1262                    tls_domain: None,
1263                    ca_cert: None,
1264                    client_cert: None,
1265                    client_key: None,
1266                }),
1267            },
1268        });
1269        config.services.signer = Some(SignerServiceConfig {
1270            storage: ::signer::storage::StorageConfig {
1271                backend: ::signer::storage::StorageBackend::Sqlite,
1272                key_ttl_seconds: 3600,
1273                sqlite: Some(::signer::storage::SqliteConfig {}),
1274                postgres: None,
1275            },
1276            kek: None,
1277            kek_env: None,
1278            kek_file: None,
1279            tolerance_seconds: 3600,
1280        });
1281
1282        // Should not have bitmask consistency errors (may have other validation errors)
1283        let result = config.validate();
1284        if let Err(errors) = result {
1285            assert!(
1286                !errors
1287                    .iter()
1288                    .any(|e| e.contains("bit is not set in enable bitmask"))
1289            );
1290        }
1291    }
1292
1293    #[test]
1294    fn test_signaling_dependencies_require_local_services_when_missing_clients() {
1295        let mut config = ActrixConfig {
1296            enable: ENABLE_SIGNALING,
1297            ..ActrixConfig::default()
1298        };
1299        config.services.signaling = Some(SignalingConfig {
1300            server: signaling::SignalingServerConfig::default(),
1301            dependencies: signaling::SignalingDependencies::default(),
1302        });
1303        config.services.signer = None;
1304        config.services.ais = None;
1305
1306        let result = config.validate();
1307        assert!(result.is_err());
1308        let errors = result.unwrap_err();
1309        assert!(
1310            errors
1311                .iter()
1312                .any(|e| e.contains("Signaling dependencies.signer is not configured"))
1313        );
1314        assert!(
1315            errors
1316                .iter()
1317                .any(|e| e.contains("Signaling dependencies.ais is not configured"))
1318        );
1319    }
1320
1321    #[test]
1322    fn test_signaling_dependencies_accept_local_services_when_enabled() {
1323        let mut config = ActrixConfig {
1324            enable: ENABLE_SIGNALING | ENABLE_SIGNER | ENABLE_AIS,
1325            ..ActrixConfig::default()
1326        };
1327        config.control.admin_ui.password = "testpassword123".to_string();
1328        config.services.signaling = Some(SignalingConfig {
1329            server: signaling::SignalingServerConfig::default(),
1330            dependencies: signaling::SignalingDependencies::default(),
1331        });
1332        config.services.signer = Some(SignerServiceConfig {
1333            ..Default::default()
1334        });
1335        config.services.ais = Some(AisConfig {
1336            server: valid_ais_server_config(),
1337            dependencies: ais::AisDependencies::default(),
1338        });
1339
1340        let result = config.validate();
1341        assert!(result.is_ok());
1342    }
1343}