//! # Proxymap 映射代理组件解析器
//!
//! Proxymap 是 Postfix 的映射代理服务组件,负责:
//! - 为其他组件提供映射表查询服务
//! - 缓存频繁访问的映射数据
//! - 处理映射表的并发访问
//! - 监控配置文件变更和警告
//!
//! ## 核心功能
//!
//! - **映射代理**: 为其他组件提供统一的映射查询接口
//! - **缓存管理**: 缓存热点映射数据,提高查询性能
//! - **并发控制**: 处理多个组件的并发映射查询
//! - **配置监控**: 检测配置文件参数覆盖和冲突
//!
//! ## 支持的事件类型
//!
//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
//!
//! ## 示例日志格式
//!
//! ```text
//! # 配置覆盖警告
//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
//! warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0
//! ```
use regex::Regex;
use crate::events::proxymap::{ProxymapEvent, ProxymapEventType};
/// PROXYMAP parser for Postfix proxy mapping service
#[derive(Debug)]
pub struct ProxymapParser {
config_override_regex: Regex,
}
impl ProxymapParser {
/// Creates a new ProxymapParser
pub fn new() -> Self {
Self {
config_override_regex: Regex::new(
r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$",
)
.expect("Failed to compile config override regex"),
}
}
/// Parse a complete log line into a ProxymapEvent
pub fn parse_log_line(&self, line: &str) -> Result<ProxymapEvent, String> {
// Basic line format: Month Day Time Host postfix/proxymap[PID]: warning: message
let basic_regex = Regex::new(
r"^((?:\d{4}\s+)?\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+\S+\s+postfix/proxymap\[(\d+)\]:\s+warning:\s+(.+)$"
).map_err(|e| format!("Regex compilation error: {}", e))?;
let captures = basic_regex
.captures(line)
.ok_or_else(|| "Line does not match proxymap log format".to_string())?;
let timestamp = captures.get(1).unwrap().as_str();
let process_id = captures.get(2).unwrap().as_str();
let message = captures.get(3).unwrap().as_str();
// Try to parse as config override warning
if let Some(event) = self.parse_config_override_warning(timestamp, process_id, message) {
return Ok(event);
}
Err(format!("Unknown proxymap message type: {}", message))
}
/// Get the number of supported event types
pub fn supported_event_types(&self) -> usize {
1
}
/// Check if this parser can handle the given line
pub fn matches_component(&self, line: &str) -> bool {
line.contains("postfix/proxymap[")
}
/// Parse config override warning
fn parse_config_override_warning(
&self,
timestamp: &str,
process_id: &str,
message: &str,
) -> Option<ProxymapEvent> {
if let Some(captures) = self.config_override_regex.captures(message) {
let file_path = captures.get(1).unwrap().as_str();
let line_number: u32 = captures.get(2).unwrap().as_str().parse().ok()?;
let parameter = captures.get(3).unwrap().as_str();
let value = captures.get(4).unwrap().as_str();
Some(ProxymapEvent {
timestamp: timestamp.to_string(),
process_id: process_id.to_string(),
event_type: ProxymapEventType::ConfigOverrideWarning {
file_path: file_path.to_string(),
line_number,
parameter: parameter.to_string(),
value: value.to_string(),
},
})
} else {
None
}
}
}
impl Default for ProxymapParser {
fn default() -> Self {
Self::new()
}
}
impl crate::components::ComponentParser for ProxymapParser {
fn parse(
&self,
message: &str,
) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
// Try to parse as config override warning
if let Some(captures) = self.config_override_regex.captures(message) {
let file_path = captures.get(1).unwrap().as_str();
let line_number: u32 = captures.get(2).unwrap().as_str().parse().map_err(|_| {
crate::error::ParseError::ComponentParseError {
component: self.component_name().to_string(),
reason: "Invalid line number format".to_string(),
}
})?;
let parameter = captures.get(3).unwrap().as_str();
let value = captures.get(4).unwrap().as_str();
let event = ProxymapEvent {
timestamp: "0".to_string(), // Temporary timestamp
process_id: "0".to_string(), // Temporary process ID
event_type: ProxymapEventType::ConfigOverrideWarning {
file_path: file_path.to_string(),
line_number,
parameter: parameter.to_string(),
value: value.to_string(),
},
};
return Ok(crate::events::base::ComponentEvent::Proxymap(event));
}
Err(crate::error::ParseError::ComponentParseError {
component: self.component_name().to_string(),
reason: format!("Unable to parse proxymap message: {}", message),
})
}
fn component_name(&self) -> &'static str {
"proxymap"
}
fn can_parse(&self, message: &str) -> bool {
self.config_override_regex.is_match(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::ComponentParser;
fn create_parser() -> ProxymapParser {
ProxymapParser::new()
}
#[test]
fn test_config_override_warning_parsing() {
let parser = create_parser();
let log_line = "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, "Apr 08 17:54:42");
assert_eq!(event.process_id, "80");
let ProxymapEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 820);
assert_eq!(parameter, "smtpd_recipient_restrictions");
assert!(value.contains("check_client_access"));
assert!(value.contains("permit_sasl_authenticated"));
}
#[test]
fn test_client_message_rate_limit_parsing() {
let parser = create_parser();
let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, "Apr 10 11:17:43");
assert_eq!(event.process_id, "80");
let ProxymapEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 806);
assert_eq!(parameter, "smtpd_client_message_rate_limit");
assert_eq!(value, "0");
}
#[test]
fn test_discard_ehlo_keywords_parsing() {
let parser = create_parser();
let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
let ProxymapEventType::ConfigOverrideWarning {
parameter, value, ..
} = event.event_type;
assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
assert_eq!(value, "silent-discard,dsn,etrn");
}
#[test]
fn test_different_line_numbers() {
let parser = create_parser();
let test_cases = vec![
("line 806", 806),
("line 819", 819),
("line 820", 820),
("line 826", 826),
];
for (line_text, expected_line) in test_cases {
let log_line = format!("Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, {}: overriding earlier entry: test_param=test_value", line_text);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
let ProxymapEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
assert_eq!(line_number, expected_line);
}
}
#[test]
fn test_different_process_ids() {
let parser = create_parser();
let test_cases = vec!["80", "84"];
for pid in test_cases {
let log_line = format!("Apr 08 17:54:42 m01 postfix/proxymap[{}]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test_param=test_value", pid);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.process_id, pid);
}
}
#[test]
fn test_component_matching() {
let parser = create_parser();
// 应该匹配的行
let matching_lines = vec![
"Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
"Apr 10 11:17:43 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
];
for line in matching_lines {
assert!(parser.matches_component(line), "Should match: {}", line);
}
// 不应该匹配的行
let non_matching_lines = vec![
"Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
"Apr 08 17:54:42 m01 postfix/smtpd[78]: connect from localhost",
"Apr 08 17:54:42 m01 postfix/cleanup[78]: message-id=<test@example.com>",
];
for line in non_matching_lines {
assert!(
!parser.matches_component(line),
"Should not match: {}",
line
);
}
}
#[test]
fn test_invalid_log_lines() {
let parser = create_parser();
let invalid_lines = vec![
"Invalid log line",
"Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
"Apr 08 17:54:42 m01 postfix/proxymap[80]: info: some other message",
"incomplete line",
];
for line in invalid_lines {
let result = parser.parse_log_line(line);
assert!(result.is_err(), "Should fail to parse: {}", line);
}
}
#[test]
fn test_supported_event_types() {
let parser = create_parser();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_parser_default() {
let parser = ProxymapParser::default();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_component_parser_parse() {
let parser = ProxymapParser::new();
// 测试配置覆盖警告
let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
let result = parser.parse(message);
assert!(result.is_ok());
match result.unwrap() {
crate::events::base::ComponentEvent::Proxymap(event) => {
assert_eq!(event.process_id, "0"); // 临时进程ID
let ProxymapEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
..
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 820);
assert_eq!(parameter, "smtpd_recipient_restrictions");
}
_ => panic!("Expected Proxymap ComponentEvent"),
}
}
#[test]
fn test_component_parser_invalid() {
let parser = ProxymapParser::new();
let message = "some invalid message";
let result = parser.parse(message);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::ParseError::ComponentParseError { component, .. } => {
assert_eq!(component, "proxymap");
}
_ => panic!("Expected ComponentParseError"),
}
}
#[test]
fn test_component_name() {
let parser = ProxymapParser::new();
assert_eq!(parser.component_name(), "proxymap");
}
#[test]
fn test_can_parse() {
let parser = ProxymapParser::new();
// 应该能解析的消息
assert!(parser
.can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
// 不应该解析的消息
assert!(!parser.can_parse("some random message"));
assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
// 缺少overriding
}
#[test]
fn test_parse_real_log_samples() {
let parser = create_parser();
// 真实日志样本
let real_logs = vec![
"Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
"Apr 08 17:58:29 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
"Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0",
"Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 819: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
"Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn",
];
for (i, log_line) in real_logs.iter().enumerate() {
let result = parser.parse_log_line(log_line);
assert!(
result.is_ok(),
"Failed to parse real log sample {}: {}",
i,
log_line
);
let event = result.unwrap();
let ProxymapEventType::ConfigOverrideWarning { file_path, .. } = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
}
}
}