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 = if let Some(pos) = url.find("://") {
110        &url[pos + 3..]
111    } else {
112        return None;
113    };
114
115    // Handle unix socket URLs (e.g., "unix:/var/run/app.sock")
116    // The entire "unix:/path/to/socket" is the host
117    if after_protocol.starts_with("unix:") {
118        return Some(after_protocol);
119    }
120
121    // Remove path
122    let host_and_port = if let Some(pos) = after_protocol.find('/') {
123        &after_protocol[..pos]
124    } else {
125        after_protocol
126    };
127
128    Some(host_and_port)
129}
130
131/// Extract domain name (without port) from a host string
132///
133/// # Examples
134///
135/// ```
136/// use nginx_lint_plugin::helpers::extract_domain;
137///
138/// assert_eq!(extract_domain("example.com"), "example.com");
139/// assert_eq!(extract_domain("example.com:8080"), "example.com");
140/// assert_eq!(extract_domain("localhost:3000"), "localhost");
141/// ```
142pub fn extract_domain(host: &str) -> &str {
143    host.split(':').next().unwrap_or(host)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_is_domain_name() {
152        // Should be detected as domain
153        assert!(is_domain_name("example.com"));
154        assert!(is_domain_name("api.example.com"));
155        assert!(is_domain_name("localhost"));
156        assert!(is_domain_name("backend.internal"));
157        assert!(is_domain_name("example.com:8080"));
158
159        // Should NOT be detected as domain
160        assert!(!is_domain_name("127.0.0.1"));
161        assert!(!is_domain_name("127.0.0.1:8080"));
162        assert!(!is_domain_name("[::1]"));
163        assert!(!is_domain_name("[::1]:8080"));
164        assert!(!is_domain_name("$backend"));
165        assert!(!is_domain_name("unix:/var/run/app.sock"));
166        assert!(!is_domain_name("backend")); // upstream name without dots
167        assert!(!is_domain_name(""));
168    }
169
170    #[test]
171    fn test_is_ipv4_address() {
172        assert!(is_ipv4_address("127.0.0.1"));
173        assert!(is_ipv4_address("192.168.1.1"));
174        assert!(is_ipv4_address("0.0.0.0"));
175        assert!(is_ipv4_address("255.255.255.255"));
176
177        assert!(!is_ipv4_address("example.com"));
178        assert!(!is_ipv4_address("127.0.0.1.1"));
179        assert!(!is_ipv4_address("256.0.0.1"));
180        assert!(!is_ipv4_address("localhost"));
181        assert!(!is_ipv4_address(""));
182    }
183
184    #[test]
185    fn test_extract_host_from_url() {
186        assert_eq!(
187            extract_host_from_url("http://example.com"),
188            Some("example.com")
189        );
190        assert_eq!(
191            extract_host_from_url("http://example.com:8080"),
192            Some("example.com:8080")
193        );
194        assert_eq!(
195            extract_host_from_url("http://example.com/path"),
196            Some("example.com")
197        );
198        assert_eq!(
199            extract_host_from_url("https://api.example.com:443/api/v1"),
200            Some("api.example.com:443")
201        );
202        assert_eq!(
203            extract_host_from_url("http://[::1]:8080/path"),
204            Some("[::1]:8080")
205        );
206        assert_eq!(
207            extract_host_from_url("http://unix:/var/run/app.sock"),
208            Some("unix:/var/run/app.sock")
209        );
210
211        // No protocol
212        assert_eq!(extract_host_from_url("example.com"), None);
213        assert_eq!(extract_host_from_url("backend"), None);
214    }
215
216    #[test]
217    fn test_extract_domain() {
218        assert_eq!(extract_domain("example.com"), "example.com");
219        assert_eq!(extract_domain("example.com:8080"), "example.com");
220        assert_eq!(extract_domain("localhost:3000"), "localhost");
221        assert_eq!(extract_domain("127.0.0.1:80"), "127.0.0.1");
222    }
223}