Skip to main content

nginx_discovery/
discovery.rs

1//! High-level discovery API for NGINX configurations
2//!
3//! This module provides a convenient API for discovering and analyzing NGINX configurations.
4//!
5//! # Examples
6//!
7//! ## Parse from text
8//!
9//! ```
10//! use nginx_discovery::NginxDiscovery;
11//!
12//! let config = r"
13//! http {
14//!     access_log /var/log/nginx/access.log;
15//! }
16//! ";
17//!
18//! let discovery = NginxDiscovery::from_config_text(config)?;
19//! let logs = discovery.access_logs();
20//! assert_eq!(logs.len(), 1);
21//! # Ok::<(), nginx_discovery::Error>(())
22//! ```
23//!
24//! ## Parse from file
25//!
26//! ```no_run
27//! use nginx_discovery::NginxDiscovery;
28//!
29//! let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
30//! let logs = discovery.access_logs();
31//! let formats = discovery.log_formats();
32//! # Ok::<(), nginx_discovery::Error>(())
33//! ```
34
35use crate::ast::Config;
36use crate::error::Result;
37use crate::extract;
38use crate::prelude::Server;
39use crate::types::{AccessLog, LogFormat};
40use std::path::{Path, PathBuf};
41
42/// High-level NGINX configuration discovery
43///
44/// Provides convenient methods to discover and analyze NGINX configurations.
45#[derive(Debug, Clone)]
46pub struct NginxDiscovery {
47    /// Parsed configuration
48    config: Config,
49    /// Path to the configuration file (if loaded from file)
50    config_path: Option<PathBuf>,
51}
52
53impl NginxDiscovery {
54    /// Create a discovery instance from configuration text
55    ///
56    /// # Arguments
57    ///
58    /// * `text` - NGINX configuration as a string
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the configuration cannot be parsed.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use nginx_discovery::NginxDiscovery;
68    ///
69    /// let config = "user nginx;";
70    /// let discovery = NginxDiscovery::from_config_text(config)?;
71    /// # Ok::<(), nginx_discovery::Error>(())
72    /// ```
73    pub fn from_config_text(text: &str) -> Result<Self> {
74        let config = crate::parse(text)?;
75        Ok(Self {
76            config,
77            config_path: None,
78        })
79    }
80
81    /// Create a discovery instance from a configuration file
82    ///
83    /// # Arguments
84    ///
85    /// * `path` - Path to the NGINX configuration file
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if:
90    /// - The file cannot be read
91    /// - The configuration cannot be parsed
92    ///
93    /// # Examples
94    ///
95    /// ```no_run
96    /// use nginx_discovery::NginxDiscovery;
97    ///
98    /// let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
99    /// # Ok::<(), nginx_discovery::Error>(())
100    /// ```
101    pub fn from_config_file(path: impl AsRef<Path>) -> Result<Self> {
102        let path = path.as_ref();
103        let text = std::fs::read_to_string(path)?;
104        let config = crate::parse(&text)?;
105        Ok(Self {
106            config,
107            config_path: Some(path.to_path_buf()),
108        })
109    }
110
111    /// Create a discovery instance from a running NGINX instance
112    ///
113    /// This attempts to:
114    /// 1. Find the nginx binary
115    /// 2. Run `nginx -T` to dump the configuration
116    /// 3. Parse the dumped configuration
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if:
121    /// - nginx binary cannot be found
122    /// - nginx -T fails to execute
123    /// - The configuration cannot be parsed
124    /// - Insufficient permissions to run nginx -T
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use nginx_discovery::NginxDiscovery;
130    ///
131    /// let discovery = NginxDiscovery::from_running_instance()?;
132    /// let logs = discovery.access_logs();
133    /// # Ok::<(), nginx_discovery::Error>(())
134    /// ```
135    #[cfg(feature = "system")]
136    pub fn from_running_instance() -> Result<Self> {
137        crate::system::detect_and_parse()
138    }
139
140    /// Get all access log configurations
141    ///
142    /// Returns all `access_log` directives found in the configuration,
143    /// including those in http, server, and location contexts.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use nginx_discovery::NginxDiscovery;
149    ///
150    /// let config = r"
151    /// http {
152    ///     access_log /var/log/nginx/access.log;
153    ///     server {
154    ///         access_log /var/log/nginx/server.log;
155    ///     }
156    /// }
157    /// ";
158    ///
159    /// let discovery = NginxDiscovery::from_config_text(config)?;
160    /// let logs = discovery.access_logs();
161    /// assert_eq!(logs.len(), 2);
162    /// # Ok::<(), nginx_discovery::Error>(())
163    /// ```
164    #[must_use]
165    pub fn access_logs(&self) -> Vec<AccessLog> {
166        extract::access_logs(&self.config).unwrap_or_default()
167    }
168
169    /// Get all log format definitions
170    ///
171    /// Returns all `log_format` directives found in the configuration.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use nginx_discovery::NginxDiscovery;
177    ///
178    /// let config = r"
179    /// log_format combined '$remote_addr - $remote_user [$time_local]';
180    /// log_format main '$request $status';
181    /// ";
182    ///
183    /// let discovery = NginxDiscovery::from_config_text(config)?;
184    /// let formats = discovery.log_formats();
185    /// assert_eq!(formats.len(), 2);
186    /// # Ok::<(), nginx_discovery::Error>(())
187    /// ```
188    #[must_use]
189    pub fn log_formats(&self) -> Vec<LogFormat> {
190        extract::log_formats(&self.config).unwrap_or_default()
191    }
192
193    /// Get all log file paths (access logs only)
194    ///
195    /// Returns a deduplicated list of all access log file paths.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use nginx_discovery::NginxDiscovery;
201    ///
202    /// let config = r"
203    /// access_log /var/log/nginx/access.log;
204    /// access_log /var/log/nginx/access.log;  # duplicate
205    /// access_log /var/log/nginx/other.log;
206    /// ";
207    ///
208    /// let discovery = NginxDiscovery::from_config_text(config)?;
209    /// let files = discovery.all_log_files();
210    /// assert_eq!(files.len(), 2); // deduplicated
211    /// # Ok::<(), nginx_discovery::Error>(())
212    /// ```
213    #[must_use]
214    pub fn all_log_files(&self) -> Vec<PathBuf> {
215        let mut paths: Vec<PathBuf> = self.access_logs().into_iter().map(|log| log.path).collect();
216
217        // Deduplicate
218        paths.sort();
219        paths.dedup();
220        paths
221    }
222
223    /// Get all server names from server blocks
224    ///
225    /// Returns a list of all server names defined in server blocks.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use nginx_discovery::NginxDiscovery;
231    ///
232    /// let config = r"
233    /// server {
234    ///     server_name example.com www.example.com;
235    /// }
236    /// server {
237    ///     server_name test.com;
238    /// }
239    /// ";
240    ///
241    /// let discovery = NginxDiscovery::from_config_text(config)?;
242    /// let names = discovery.server_names();
243    /// assert_eq!(names.len(), 3);
244    /// # Ok::<(), nginx_discovery::Error>(())
245    /// ```
246    #[must_use]
247    pub fn server_names(&self) -> Vec<String> {
248        let mut names = Vec::new();
249
250        for server in self.config.find_directives_recursive("server") {
251            for server_name_directive in server.find_children("server_name") {
252                names.extend(server_name_directive.args_as_strings());
253            }
254        }
255
256        names
257    }
258
259    /// Export configuration to JSON
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if serialization fails.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use nginx_discovery::NginxDiscovery;
269    ///
270    /// let config = "user nginx;";
271    /// let discovery = NginxDiscovery::from_config_text(config)?;
272    /// let json = discovery.to_json()?;
273    /// assert!(json.contains("user"));
274    /// # Ok::<(), nginx_discovery::Error>(())
275    /// ```
276    #[cfg(feature = "serde")]
277    pub fn to_json(&self) -> Result<String> {
278        serde_json::to_string_pretty(&self.config)
279            .map_err(|e| crate::Error::Serialization(e.to_string()))
280    }
281
282    /// Export configuration to YAML
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if serialization fails.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use nginx_discovery::NginxDiscovery;
292    ///
293    /// let config = "user nginx;";
294    /// let discovery = NginxDiscovery::from_config_text(config)?;
295    /// let yaml = discovery.to_yaml()?;
296    /// assert!(yaml.contains("user"));
297    /// # Ok::<(), nginx_discovery::Error>(())
298    /// ```
299    #[cfg(feature = "serde")]
300    pub fn to_yaml(&self) -> Result<String> {
301        serde_yaml::to_string(&self.config).map_err(|e| crate::Error::Serialization(e.to_string()))
302    }
303
304    /// Get the parsed configuration AST
305    ///
306    /// Provides direct access to the parsed configuration for custom processing.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use nginx_discovery::NginxDiscovery;
312    ///
313    /// let config = "user nginx;";
314    /// let discovery = NginxDiscovery::from_config_text(config)?;
315    /// let ast = discovery.config();
316    /// assert_eq!(ast.directives.len(), 1);
317    /// # Ok::<(), nginx_discovery::Error>(())
318    /// ```
319    #[must_use]
320    pub fn config(&self) -> &Config {
321        &self.config
322    }
323
324    /// Get the configuration file path (if loaded from file)
325    ///
326    /// # Examples
327    ///
328    /// ```no_run
329    /// use nginx_discovery::NginxDiscovery;
330    ///
331    /// let discovery = NginxDiscovery::from_config_file("/etc/nginx/nginx.conf")?;
332    /// assert!(discovery.config_path().is_some());
333    /// # Ok::<(), nginx_discovery::Error>(())
334    /// ```
335    #[must_use]
336    pub fn config_path(&self) -> Option<&Path> {
337        self.config_path.as_deref()
338    }
339
340    /// Generate a summary of the configuration
341    ///
342    /// Returns a human-readable summary including:
343    /// - Number of directives
344    /// - Number of server blocks
345    /// - Number of access logs
346    /// - Number of log formats
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use nginx_discovery::NginxDiscovery;
352    ///
353    /// let config = r"
354    /// user nginx;
355    /// access_log /var/log/nginx/access.log;
356    /// server { listen 80; }
357    /// ";
358    ///
359    /// let discovery = NginxDiscovery::from_config_text(config)?;
360    /// let summary = discovery.summary();
361    /// assert!(summary.contains("directives"));
362    /// # Ok::<(), nginx_discovery::Error>(())
363    /// ```
364    #[must_use]
365    pub fn summary(&self) -> String {
366        let directive_count = self.config.count_directives();
367        let server_count = self.config.find_directives_recursive("server").len();
368        let access_log_count = self.access_logs().len();
369        let format_count = self.log_formats().len();
370
371        format!(
372            "NGINX Configuration Summary:\n\
373            - Total directives: {directive_count}\n\
374            - Server blocks: {server_count}\n\
375            - Access logs: {access_log_count}\n\
376            - Log formats: {format_count}"
377        )
378    }
379
380    // Add these methods to the NginxDiscovery impl block:
381
382    /// Get all server blocks
383    ///
384    /// Returns all `server` blocks found in the configuration.
385    ///
386    /// # Examples
387    ///
388    /// ```
389    /// use nginx_discovery::NginxDiscovery;
390    ///
391    /// let config = r"
392    /// server {
393    ///     listen 80;
394    ///     server_name example.com;
395    /// }
396    /// ";
397    ///
398    /// let discovery = NginxDiscovery::from_config_text(config)?;
399    /// let servers = discovery.servers();
400    /// assert_eq!(servers.len(), 1);
401    /// # Ok::<(), nginx_discovery::Error>(())
402    /// ```
403    #[must_use]
404    pub fn servers(&self) -> Vec<crate::types::Server> {
405        extract::servers(&self.config).unwrap_or_default()
406    }
407
408    /// Get all listening ports
409    ///
410    /// Returns a deduplicated list of all ports that servers are listening on.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// use nginx_discovery::NginxDiscovery;
416    ///
417    /// let config = r"
418    /// server {
419    ///     listen 80;
420    ///     listen 443 ssl;
421    /// }
422    /// ";
423    ///
424    /// let discovery = NginxDiscovery::from_config_text(config)?;
425    /// let ports = discovery.listening_ports();
426    /// assert!(ports.contains(&80));
427    /// assert!(ports.contains(&443));
428    /// # Ok::<(), nginx_discovery::Error>(())
429    /// ```
430    #[must_use]
431    pub fn listening_ports(&self) -> Vec<u16> {
432        let mut ports: Vec<u16> = self
433            .servers()
434            .iter()
435            .flat_map(|s| s.listen.iter().map(|l| l.port))
436            .collect();
437
438        ports.sort_unstable();
439        ports.dedup();
440        ports
441    }
442
443    /// Get all SSL-enabled servers
444    ///
445    /// Returns servers that have SSL configured.
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// use nginx_discovery::NginxDiscovery;
451    ///
452    /// let config = r"
453    /// server {
454    ///     listen 80;
455    ///     server_name example.com;
456    /// }
457    /// server {
458    ///     listen 443 ssl;
459    ///     server_name secure.example.com;
460    /// }
461    /// ";
462    ///
463    /// let discovery = NginxDiscovery::from_config_text(config)?;
464    /// let ssl_servers = discovery.ssl_servers();
465    /// assert_eq!(ssl_servers.len(), 1);
466    /// # Ok::<(), nginx_discovery::Error>(())
467    /// ```
468    #[must_use]
469    pub fn ssl_servers(&self) -> Vec<crate::types::Server> {
470        self.servers().into_iter().filter(Server::has_ssl).collect()
471    }
472
473    /// Get all proxy locations
474    ///
475    /// Returns all location blocks that have `proxy_pass` configured.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use nginx_discovery::NginxDiscovery;
481    ///
482    /// let config = r"
483    /// server {
484    ///     location / {
485    ///         root /var/www;
486    ///     }
487    ///     location /api {
488    ///         proxy_pass http://backend;
489    ///     }
490    /// }
491    /// ";
492    ///
493    /// let discovery = NginxDiscovery::from_config_text(config)?;
494    /// let proxies = discovery.proxy_locations();
495    /// assert_eq!(proxies.len(), 1);
496    /// # Ok::<(), nginx_discovery::Error>(())
497    /// ```
498    #[must_use]
499    pub fn proxy_locations(&self) -> Vec<crate::types::Location> {
500        self.servers()
501            .iter()
502            .flat_map(|s| s.locations.iter())
503            .filter(|l: &&crate::types::Location| l.is_proxy())
504            .cloned()
505            .collect()
506    }
507
508    /// Count total number of location blocks
509    #[must_use]
510    pub fn location_count(&self) -> usize {
511        self.servers().iter().map(|s| s.locations.len()).sum()
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518
519    #[test]
520    fn test_from_config_text() {
521        let config = "user nginx;";
522        let discovery = NginxDiscovery::from_config_text(config).unwrap();
523        assert_eq!(discovery.config.directives.len(), 1);
524    }
525
526    #[test]
527    fn test_access_logs() {
528        let config = r"
529        http {
530            access_log /var/log/nginx/access.log;
531            server {
532                access_log /var/log/nginx/server.log;
533            }
534        }
535        ";
536
537        let discovery = NginxDiscovery::from_config_text(config).unwrap();
538        let logs = discovery.access_logs();
539        assert_eq!(logs.len(), 2);
540    }
541
542    #[test]
543    fn test_log_formats() {
544        let config = r"
545        log_format combined '$remote_addr';
546        log_format main '$request';
547        ";
548
549        let discovery = NginxDiscovery::from_config_text(config).unwrap();
550        let formats = discovery.log_formats();
551        assert_eq!(formats.len(), 2);
552    }
553
554    #[test]
555    fn test_all_log_files() {
556        let config = r"
557        access_log /var/log/nginx/access.log;
558        access_log /var/log/nginx/access.log;
559        access_log /var/log/nginx/other.log;
560        ";
561
562        let discovery = NginxDiscovery::from_config_text(config).unwrap();
563        let files = discovery.all_log_files();
564        assert_eq!(files.len(), 2); // Deduplicated
565    }
566
567    #[test]
568    fn test_server_names() {
569        let config = r"
570        server {
571            server_name example.com www.example.com;
572        }
573        server {
574            server_name test.com;
575        }
576        ";
577
578        let discovery = NginxDiscovery::from_config_text(config).unwrap();
579        let names = discovery.server_names();
580        assert_eq!(names.len(), 3);
581        assert!(names.contains(&"example.com".to_string()));
582        assert!(names.contains(&"www.example.com".to_string()));
583        assert!(names.contains(&"test.com".to_string()));
584    }
585
586    #[test]
587    fn test_summary() {
588        let config = r"
589        user nginx;
590        access_log /var/log/nginx/access.log;
591        server { listen 80; }
592        ";
593
594        let discovery = NginxDiscovery::from_config_text(config).unwrap();
595        let summary = discovery.summary();
596        assert!(summary.contains("directives"));
597        assert!(summary.contains("Server blocks: 1"));
598    }
599
600    #[test]
601    fn test_config_access() {
602        let config = "user nginx;";
603        let discovery = NginxDiscovery::from_config_text(config).unwrap();
604        let ast = discovery.config();
605        assert_eq!(ast.directives.len(), 1);
606    }
607
608    #[test]
609    #[cfg(feature = "serde")]
610    fn test_to_json() {
611        let config = "user nginx;";
612        let discovery = NginxDiscovery::from_config_text(config).unwrap();
613        let json = discovery.to_json().unwrap();
614        assert!(json.contains("user"));
615    }
616
617    #[test]
618    #[cfg(feature = "serde")]
619    fn test_to_yaml() {
620        let config = "user nginx;";
621        let discovery = NginxDiscovery::from_config_text(config).unwrap();
622        let yaml = discovery.to_yaml().unwrap();
623        assert!(yaml.contains("user"));
624    }
625}