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}