postfix_log_parser/components/
proxymap.rs

1//! # Proxymap 映射代理组件解析器
2//!
3//! Proxymap 是 Postfix 的映射代理服务组件,负责:
4//! - 为其他组件提供映射表查询服务
5//! - 缓存频繁访问的映射数据
6//! - 处理映射表的并发访问
7//! - 监控配置文件变更和警告
8//!
9//! ## 核心功能
10//!
11//! - **映射代理**: 为其他组件提供统一的映射查询接口
12//! - **缓存管理**: 缓存热点映射数据,提高查询性能
13//! - **并发控制**: 处理多个组件的并发映射查询
14//! - **配置监控**: 检测配置文件参数覆盖和冲突
15//!
16//! ## 支持的事件类型
17//!
18//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
19//!
20//! ## 示例日志格式
21//!
22//! ```text
23//! # 配置覆盖警告
24//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
25//! warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0
26//! ```
27
28use regex::Regex;
29
30use crate::events::proxymap::{ProxymapEvent, ProxymapEventType};
31
32/// PROXYMAP parser for Postfix proxy mapping service
33#[derive(Debug)]
34pub struct ProxymapParser {
35    config_override_regex: Regex,
36}
37
38impl ProxymapParser {
39    /// Creates a new ProxymapParser
40    pub fn new() -> Self {
41        Self {
42            config_override_regex: Regex::new(
43                r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$",
44            )
45            .expect("Failed to compile config override regex"),
46        }
47    }
48
49    /// Parse a complete log line into a ProxymapEvent
50    pub fn parse_log_line(&self, line: &str) -> Result<ProxymapEvent, String> {
51        // Basic line format: Month Day Time Host postfix/proxymap[PID]: warning: message
52        let basic_regex = Regex::new(
53            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+(.+)$"
54        ).map_err(|e| format!("Regex compilation error: {}", e))?;
55
56        let captures = basic_regex
57            .captures(line)
58            .ok_or_else(|| "Line does not match proxymap log format".to_string())?;
59
60        let timestamp = captures.get(1).unwrap().as_str();
61        let process_id = captures.get(2).unwrap().as_str();
62        let message = captures.get(3).unwrap().as_str();
63
64        // Try to parse as config override warning
65        if let Some(event) = self.parse_config_override_warning(timestamp, process_id, message) {
66            return Ok(event);
67        }
68
69        Err(format!("Unknown proxymap message type: {}", message))
70    }
71
72    /// Get the number of supported event types
73    pub fn supported_event_types(&self) -> usize {
74        1
75    }
76
77    /// Check if this parser can handle the given line
78    pub fn matches_component(&self, line: &str) -> bool {
79        line.contains("postfix/proxymap[")
80    }
81
82    /// Parse config override warning
83    fn parse_config_override_warning(
84        &self,
85        timestamp: &str,
86        process_id: &str,
87        message: &str,
88    ) -> Option<ProxymapEvent> {
89        if let Some(captures) = self.config_override_regex.captures(message) {
90            let file_path = captures.get(1).unwrap().as_str();
91            let line_number: u32 = captures.get(2).unwrap().as_str().parse().ok()?;
92            let parameter = captures.get(3).unwrap().as_str();
93            let value = captures.get(4).unwrap().as_str();
94
95            Some(ProxymapEvent {
96                timestamp: timestamp.to_string(),
97                process_id: process_id.to_string(),
98                event_type: ProxymapEventType::ConfigOverrideWarning {
99                    file_path: file_path.to_string(),
100                    line_number,
101                    parameter: parameter.to_string(),
102                    value: value.to_string(),
103                },
104            })
105        } else {
106            None
107        }
108    }
109}
110
111impl Default for ProxymapParser {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl crate::components::ComponentParser for ProxymapParser {
118    fn parse(
119        &self,
120        message: &str,
121    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
122        // Try to parse as config override warning
123        if let Some(captures) = self.config_override_regex.captures(message) {
124            let file_path = captures.get(1).unwrap().as_str();
125            let line_number: u32 = captures.get(2).unwrap().as_str().parse().map_err(|_| {
126                crate::error::ParseError::ComponentParseError {
127                    component: self.component_name().to_string(),
128                    reason: "Invalid line number format".to_string(),
129                }
130            })?;
131            let parameter = captures.get(3).unwrap().as_str();
132            let value = captures.get(4).unwrap().as_str();
133
134            let event = ProxymapEvent {
135                timestamp: "0".to_string(),  // Temporary timestamp
136                process_id: "0".to_string(), // Temporary process ID
137                event_type: ProxymapEventType::ConfigOverrideWarning {
138                    file_path: file_path.to_string(),
139                    line_number,
140                    parameter: parameter.to_string(),
141                    value: value.to_string(),
142                },
143            };
144
145            return Ok(crate::events::base::ComponentEvent::Proxymap(event));
146        }
147
148        Err(crate::error::ParseError::ComponentParseError {
149            component: self.component_name().to_string(),
150            reason: format!("Unable to parse proxymap message: {}", message),
151        })
152    }
153
154    fn component_name(&self) -> &'static str {
155        "proxymap"
156    }
157
158    fn can_parse(&self, message: &str) -> bool {
159        self.config_override_regex.is_match(message)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::components::ComponentParser;
167
168    fn create_parser() -> ProxymapParser {
169        ProxymapParser::new()
170    }
171
172    #[test]
173    fn test_config_override_warning_parsing() {
174        let parser = create_parser();
175        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";
176
177        let result = parser.parse_log_line(log_line);
178        assert!(result.is_ok());
179
180        let event = result.unwrap();
181        assert_eq!(event.timestamp, "Apr 08 17:54:42");
182        assert_eq!(event.process_id, "80");
183
184        let ProxymapEventType::ConfigOverrideWarning {
185            file_path,
186            line_number,
187            parameter,
188            value,
189        } = event.event_type;
190
191        assert_eq!(file_path, "/etc/postfix/main.cf");
192        assert_eq!(line_number, 820);
193        assert_eq!(parameter, "smtpd_recipient_restrictions");
194        assert!(value.contains("check_client_access"));
195        assert!(value.contains("permit_sasl_authenticated"));
196    }
197
198    #[test]
199    fn test_client_message_rate_limit_parsing() {
200        let parser = create_parser();
201        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";
202
203        let result = parser.parse_log_line(log_line);
204        assert!(result.is_ok());
205
206        let event = result.unwrap();
207        assert_eq!(event.timestamp, "Apr 10 11:17:43");
208        assert_eq!(event.process_id, "80");
209
210        let ProxymapEventType::ConfigOverrideWarning {
211            file_path,
212            line_number,
213            parameter,
214            value,
215        } = event.event_type;
216
217        assert_eq!(file_path, "/etc/postfix/main.cf");
218        assert_eq!(line_number, 806);
219        assert_eq!(parameter, "smtpd_client_message_rate_limit");
220        assert_eq!(value, "0");
221    }
222
223    #[test]
224    fn test_discard_ehlo_keywords_parsing() {
225        let parser = create_parser();
226        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";
227
228        let result = parser.parse_log_line(log_line);
229        assert!(result.is_ok());
230
231        let event = result.unwrap();
232        let ProxymapEventType::ConfigOverrideWarning {
233            parameter, value, ..
234        } = event.event_type;
235
236        assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
237        assert_eq!(value, "silent-discard,dsn,etrn");
238    }
239
240    #[test]
241    fn test_different_line_numbers() {
242        let parser = create_parser();
243        let test_cases = vec![
244            ("line 806", 806),
245            ("line 819", 819),
246            ("line 820", 820),
247            ("line 826", 826),
248        ];
249
250        for (line_text, expected_line) in test_cases {
251            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);
252
253            let result = parser.parse_log_line(&log_line);
254            assert!(result.is_ok());
255
256            let event = result.unwrap();
257            let ProxymapEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
258            assert_eq!(line_number, expected_line);
259        }
260    }
261
262    #[test]
263    fn test_different_process_ids() {
264        let parser = create_parser();
265        let test_cases = vec!["80", "84"];
266
267        for pid in test_cases {
268            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);
269
270            let result = parser.parse_log_line(&log_line);
271            assert!(result.is_ok());
272
273            let event = result.unwrap();
274            assert_eq!(event.process_id, pid);
275        }
276    }
277
278    #[test]
279    fn test_component_matching() {
280        let parser = create_parser();
281
282        // 应该匹配的行
283        let matching_lines = vec![
284            "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
285            "Apr 10 11:17:43 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
286        ];
287
288        for line in matching_lines {
289            assert!(parser.matches_component(line), "Should match: {}", line);
290        }
291
292        // 不应该匹配的行
293        let non_matching_lines = vec![
294            "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
295            "Apr 08 17:54:42 m01 postfix/smtpd[78]: connect from localhost",
296            "Apr 08 17:54:42 m01 postfix/cleanup[78]: message-id=<test@example.com>",
297        ];
298
299        for line in non_matching_lines {
300            assert!(
301                !parser.matches_component(line),
302                "Should not match: {}",
303                line
304            );
305        }
306    }
307
308    #[test]
309    fn test_invalid_log_lines() {
310        let parser = create_parser();
311
312        let invalid_lines = vec![
313            "Invalid log line",
314            "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
315            "Apr 08 17:54:42 m01 postfix/proxymap[80]: info: some other message",
316            "incomplete line",
317        ];
318
319        for line in invalid_lines {
320            let result = parser.parse_log_line(line);
321            assert!(result.is_err(), "Should fail to parse: {}", line);
322        }
323    }
324
325    #[test]
326    fn test_supported_event_types() {
327        let parser = create_parser();
328        assert_eq!(parser.supported_event_types(), 1);
329    }
330
331    #[test]
332    fn test_parser_default() {
333        let parser = ProxymapParser::default();
334        assert_eq!(parser.supported_event_types(), 1);
335    }
336
337    #[test]
338    fn test_component_parser_parse() {
339        let parser = ProxymapParser::new();
340
341        // 测试配置覆盖警告
342        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
343        let result = parser.parse(message);
344
345        assert!(result.is_ok());
346        match result.unwrap() {
347            crate::events::base::ComponentEvent::Proxymap(event) => {
348                assert_eq!(event.process_id, "0"); // 临时进程ID
349                let ProxymapEventType::ConfigOverrideWarning {
350                    file_path,
351                    line_number,
352                    parameter,
353                    ..
354                } = event.event_type;
355
356                assert_eq!(file_path, "/etc/postfix/main.cf");
357                assert_eq!(line_number, 820);
358                assert_eq!(parameter, "smtpd_recipient_restrictions");
359            }
360            _ => panic!("Expected Proxymap ComponentEvent"),
361        }
362    }
363
364    #[test]
365    fn test_component_parser_invalid() {
366        let parser = ProxymapParser::new();
367
368        let message = "some invalid message";
369        let result = parser.parse(message);
370
371        assert!(result.is_err());
372        match result.unwrap_err() {
373            crate::error::ParseError::ComponentParseError { component, .. } => {
374                assert_eq!(component, "proxymap");
375            }
376            _ => panic!("Expected ComponentParseError"),
377        }
378    }
379
380    #[test]
381    fn test_component_name() {
382        let parser = ProxymapParser::new();
383        assert_eq!(parser.component_name(), "proxymap");
384    }
385
386    #[test]
387    fn test_can_parse() {
388        let parser = ProxymapParser::new();
389
390        // 应该能解析的消息
391        assert!(parser
392            .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
393
394        // 不应该解析的消息
395        assert!(!parser.can_parse("some random message"));
396        assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
397        assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
398        // 缺少overriding
399    }
400
401    #[test]
402    fn test_parse_real_log_samples() {
403        let parser = create_parser();
404
405        // 真实日志样本
406        let real_logs = vec![
407            "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",
408            "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",
409            "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",
410            "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",
411            "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",
412        ];
413
414        for (i, log_line) in real_logs.iter().enumerate() {
415            let result = parser.parse_log_line(log_line);
416            assert!(
417                result.is_ok(),
418                "Failed to parse real log sample {}: {}",
419                i,
420                log_line
421            );
422
423            let event = result.unwrap();
424            let ProxymapEventType::ConfigOverrideWarning { file_path, .. } = event.event_type;
425            assert_eq!(file_path, "/etc/postfix/main.cf");
426        }
427    }
428}