Skip to main content

nginx_lint_plugin/
helpers.rs

1//! Helper functions for plugin development
2//!
3//! This module provides common utilities for nginx configuration linting.
4
5/// Check if the given host is a domain name (not an IP address or special value)
6///
7/// Returns `true` for domain names like `example.com`, `api.backend.internal`, `localhost`
8/// Returns `false` for IP addresses, unix sockets, variables, or upstream names without dots
9///
10/// # Examples
11///
12/// ```
13/// use nginx_lint_plugin::helpers::is_domain_name;
14///
15/// // Domain names
16/// assert!(is_domain_name("example.com"));
17/// assert!(is_domain_name("api.example.com"));
18/// assert!(is_domain_name("localhost"));
19/// assert!(is_domain_name("backend.internal"));
20/// assert!(is_domain_name("example.com:8080"));
21///
22/// // Not domain names
23/// assert!(!is_domain_name("127.0.0.1"));
24/// assert!(!is_domain_name("127.0.0.1:8080"));
25/// assert!(!is_domain_name("[::1]"));
26/// assert!(!is_domain_name("[::1]:8080"));
27/// assert!(!is_domain_name("$backend"));
28/// assert!(!is_domain_name("unix:/var/run/app.sock"));
29/// assert!(!is_domain_name("backend")); // upstream name without dots
30/// ```
31pub fn is_domain_name(host: &str) -> bool {
32    // Empty host
33    if host.is_empty() {
34        return false;
35    }
36
37    // Variable reference (e.g., $backend)
38    if host.starts_with('$') {
39        return false;
40    }
41
42    // Unix socket
43    if host.starts_with("unix:") {
44        return false;
45    }
46
47    // IPv6 address (e.g., [::1])
48    if host.starts_with('[') && host.contains(']') {
49        return false;
50    }
51
52    // IPv4 address (all parts are numeric)
53    let host_without_port = host.split(':').next().unwrap_or(host);
54    if is_ipv4_address(host_without_port) {
55        return false;
56    }
57
58    // localhost or contains a dot (domain name)
59    host_without_port == "localhost" || host_without_port.contains('.')
60}
61
62/// Check if the string is a valid IPv4 address
63///
64/// # Examples
65///
66/// ```
67/// use nginx_lint_plugin::helpers::is_ipv4_address;
68///
69/// assert!(is_ipv4_address("127.0.0.1"));
70/// assert!(is_ipv4_address("192.168.1.1"));
71/// assert!(is_ipv4_address("0.0.0.0"));
72/// assert!(is_ipv4_address("255.255.255.255"));
73///
74/// assert!(!is_ipv4_address("example.com"));
75/// assert!(!is_ipv4_address("127.0.0.1.1"));
76/// assert!(!is_ipv4_address("256.0.0.1"));
77/// assert!(!is_ipv4_address("localhost"));
78/// ```
79pub fn is_ipv4_address(s: &str) -> bool {
80    let parts: Vec<&str> = s.split('.').collect();
81    if parts.len() != 4 {
82        return false;
83    }
84    parts.iter().all(|p| p.parse::<u8>().is_ok())
85}
86
87/// Extract host from a proxy_pass URL
88///
89/// Extracts the host (and optional port) from a URL like `http://example.com:8080/path`
90///
91/// # Examples
92///
93/// ```
94/// use nginx_lint_plugin::helpers::extract_host_from_url;
95///
96/// assert_eq!(extract_host_from_url("http://example.com"), Some("example.com"));
97/// assert_eq!(extract_host_from_url("http://example.com:8080"), Some("example.com:8080"));
98/// assert_eq!(extract_host_from_url("http://example.com/path"), Some("example.com"));
99/// assert_eq!(extract_host_from_url("https://api.example.com:443/api/v1"), Some("api.example.com:443"));
100/// assert_eq!(extract_host_from_url("http://[::1]:8080/path"), Some("[::1]:8080"));
101/// assert_eq!(extract_host_from_url("http://unix:/var/run/app.sock"), Some("unix:/var/run/app.sock"));
102///
103/// // No protocol
104/// assert_eq!(extract_host_from_url("example.com"), None);
105/// assert_eq!(extract_host_from_url("backend"), None);
106/// ```
107pub fn extract_host_from_url(url: &str) -> Option<&str> {
108    // Remove protocol
109    let after_protocol = {
110        let pos = url.find("://")?;
111        &url[pos + 3..]
112    };
113
114    // Handle unix socket URLs (e.g., "unix:/var/run/app.sock")
115    // The entire "unix:/path/to/socket" is the host
116    if after_protocol.starts_with("unix:") {
117        return Some(after_protocol);
118    }
119
120    // Remove path
121    let host_and_port = if let Some(pos) = after_protocol.find('/') {
122        &after_protocol[..pos]
123    } else {
124        after_protocol
125    };
126
127    Some(host_and_port)
128}
129
130/// Extract domain name (without port) from a host string
131///
132/// # Examples
133///
134/// ```
135/// use nginx_lint_plugin::helpers::extract_domain;
136///
137/// assert_eq!(extract_domain("example.com"), "example.com");
138/// assert_eq!(extract_domain("example.com:8080"), "example.com");
139/// assert_eq!(extract_domain("localhost:3000"), "localhost");
140/// ```
141pub fn extract_domain(host: &str) -> &str {
142    host.split(':').next().unwrap_or(host)
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_is_domain_name() {
151        // Should be detected as domain
152        assert!(is_domain_name("example.com"));
153        assert!(is_domain_name("api.example.com"));
154        assert!(is_domain_name("localhost"));
155        assert!(is_domain_name("backend.internal"));
156        assert!(is_domain_name("example.com:8080"));
157
158        // Should NOT be detected as domain
159        assert!(!is_domain_name("127.0.0.1"));
160        assert!(!is_domain_name("127.0.0.1:8080"));
161        assert!(!is_domain_name("[::1]"));
162        assert!(!is_domain_name("[::1]:8080"));
163        assert!(!is_domain_name("$backend"));
164        assert!(!is_domain_name("unix:/var/run/app.sock"));
165        assert!(!is_domain_name("backend")); // upstream name without dots
166        assert!(!is_domain_name(""));
167    }
168
169    #[test]
170    fn test_is_ipv4_address() {
171        assert!(is_ipv4_address("127.0.0.1"));
172        assert!(is_ipv4_address("192.168.1.1"));
173        assert!(is_ipv4_address("0.0.0.0"));
174        assert!(is_ipv4_address("255.255.255.255"));
175
176        assert!(!is_ipv4_address("example.com"));
177        assert!(!is_ipv4_address("127.0.0.1.1"));
178        assert!(!is_ipv4_address("256.0.0.1"));
179        assert!(!is_ipv4_address("localhost"));
180        assert!(!is_ipv4_address(""));
181    }
182
183    #[test]
184    fn test_extract_host_from_url() {
185        assert_eq!(
186            extract_host_from_url("http://example.com"),
187            Some("example.com")
188        );
189        assert_eq!(
190            extract_host_from_url("http://example.com:8080"),
191            Some("example.com:8080")
192        );
193        assert_eq!(
194            extract_host_from_url("http://example.com/path"),
195            Some("example.com")
196        );
197        assert_eq!(
198            extract_host_from_url("https://api.example.com:443/api/v1"),
199            Some("api.example.com:443")
200        );
201        assert_eq!(
202            extract_host_from_url("http://[::1]:8080/path"),
203            Some("[::1]:8080")
204        );
205        assert_eq!(
206            extract_host_from_url("http://unix:/var/run/app.sock"),
207            Some("unix:/var/run/app.sock")
208        );
209
210        // No protocol
211        assert_eq!(extract_host_from_url("example.com"), None);
212        assert_eq!(extract_host_from_url("backend"), None);
213    }
214
215    #[test]
216    fn test_extract_domain() {
217        assert_eq!(extract_domain("example.com"), "example.com");
218        assert_eq!(extract_domain("example.com:8080"), "example.com");
219        assert_eq!(extract_domain("localhost:3000"), "localhost");
220        assert_eq!(extract_domain("127.0.0.1:80"), "127.0.0.1");
221    }
222}