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}