Skip to main content

rust_serv/vhost/
matcher.rs

1//! Host matcher for virtual host routing
2
3use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use super::config::VHostConfig;
7use super::host::VirtualHost;
8
9/// Host matcher for routing to virtual hosts
10pub struct HostMatcher {
11    /// Virtual hosts by hostname
12    hosts: RwLock<HashMap<String, Arc<VirtualHost>>>,
13    /// Default virtual host
14    default_host: RwLock<Option<Arc<VirtualHost>>>,
15}
16
17impl HostMatcher {
18    /// Create a new host matcher
19    pub fn new() -> Self {
20        Self {
21            hosts: RwLock::new(HashMap::new()),
22            default_host: RwLock::new(None),
23        }
24    }
25
26    /// Add a virtual host
27    pub fn add_host(&self, config: VHostConfig) {
28        let vhost = Arc::new(VirtualHost::new(config.clone()));
29        let mut hosts = self.hosts.write().unwrap();
30        hosts.insert(config.host.to_lowercase(), vhost);
31    }
32
33    /// Add a default virtual host
34    pub fn set_default(&self, root: impl Into<std::path::PathBuf>) {
35        let vhost = Arc::new(VirtualHost::default_host(root));
36        let mut default = self.default_host.write().unwrap();
37        *default = Some(vhost);
38    }
39
40    /// Match a hostname to a virtual host
41    pub fn match_host(&self, hostname: &str) -> Option<Arc<VirtualHost>> {
42        // First try exact match
43        let hosts = self.hosts.read().unwrap();
44        let hostname_lower = hostname.to_lowercase();
45        
46        if let Some(vhost) = hosts.get(&hostname_lower) {
47            return Some(Arc::clone(vhost));
48        }
49        
50        // Try default host
51        let default = self.default_host.read().unwrap();
52        if let Some(vhost) = default.as_ref() {
53            return Some(Arc::clone(vhost));
54        }
55        
56        None
57    }
58
59    /// Remove a virtual host
60    pub fn remove_host(&self, hostname: &str) -> bool {
61        let mut hosts = self.hosts.write().unwrap();
62        hosts.remove(&hostname.to_lowercase()).is_some()
63    }
64
65    /// Get host count
66    pub fn host_count(&self) -> usize {
67        self.hosts.read().unwrap().len()
68    }
69
70    /// Check if a host exists
71    pub fn has_host(&self, hostname: &str) -> bool {
72        self.hosts.read().unwrap().contains_key(&hostname.to_lowercase())
73    }
74
75    /// Clear all hosts
76    pub fn clear(&self) {
77        let mut hosts = self.hosts.write().unwrap();
78        hosts.clear();
79        
80        let mut default = self.default_host.write().unwrap();
81        *default = None;
82    }
83
84    /// Check if default host exists
85    pub fn has_default(&self) -> bool {
86        self.default_host.read().unwrap().is_some()
87    }
88
89    /// Get all hostnames
90    pub fn hostnames(&self) -> Vec<String> {
91        self.hosts.read().unwrap().keys().cloned().collect()
92    }
93}
94
95impl Default for HostMatcher {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::path::PathBuf;
105
106    #[test]
107    fn test_matcher_creation() {
108        let matcher = HostMatcher::new();
109        assert_eq!(matcher.host_count(), 0);
110        assert!(!matcher.has_default());
111    }
112
113    #[test]
114    fn test_add_host() {
115        let matcher = HostMatcher::new();
116        let config = VHostConfig::new("example.com", "/var/www");
117        
118        matcher.add_host(config);
119        
120        assert_eq!(matcher.host_count(), 1);
121        assert!(matcher.has_host("example.com"));
122        assert!(matcher.has_host("EXAMPLE.COM"));
123    }
124
125    #[test]
126    fn test_match_host_exact() {
127        let matcher = HostMatcher::new();
128        let config = VHostConfig::new("example.com", "/var/www/example");
129        matcher.add_host(config);
130        
131        let vhost = matcher.match_host("example.com").unwrap();
132        assert_eq!(vhost.host(), "example.com");
133        assert_eq!(vhost.root(), &PathBuf::from("/var/www/example"));
134    }
135
136    #[test]
137    fn test_match_host_case_insensitive() {
138        let matcher = HostMatcher::new();
139        let config = VHostConfig::new("Example.Com", "/var/www");
140        matcher.add_host(config);
141        
142        let vhost = matcher.match_host("EXAMPLE.COM").unwrap();
143        assert_eq!(vhost.host(), "Example.Com");
144    }
145
146    #[test]
147    fn test_match_host_default() {
148        let matcher = HostMatcher::new();
149        matcher.set_default("/var/default");
150        
151        // Should match any hostname
152        let vhost = matcher.match_host("anything.com").unwrap();
153        assert!(vhost.is_default);
154    }
155
156    #[test]
157    fn test_match_host_no_match() {
158        let matcher = HostMatcher::new();
159        let config = VHostConfig::new("example.com", "/var/www");
160        matcher.add_host(config);
161        
162        // No default, no match
163        let result = matcher.match_host("other.com");
164        assert!(result.is_none());
165    }
166
167    #[test]
168    fn test_match_host_prefer_exact_over_default() {
169        let matcher = HostMatcher::new();
170        
171        // Add default first
172        matcher.set_default("/var/default");
173        
174        // Add specific host
175        let config = VHostConfig::new("example.com", "/var/www/example");
176        matcher.add_host(config);
177        
178        // Should return specific host, not default
179        let vhost = matcher.match_host("example.com").unwrap();
180        assert_eq!(vhost.host(), "example.com");
181        assert!(!vhost.is_default);
182        
183        // Should return default for other hosts
184        let vhost = matcher.match_host("other.com").unwrap();
185        assert!(vhost.is_default);
186    }
187
188    #[test]
189    fn test_remove_host() {
190        let matcher = HostMatcher::new();
191        let config = VHostConfig::new("example.com", "/var/www");
192        matcher.add_host(config);
193        
194        assert!(matcher.remove_host("example.com"));
195        assert_eq!(matcher.host_count(), 0);
196        assert!(!matcher.has_host("example.com"));
197    }
198
199    #[test]
200    fn test_remove_nonexistent_host() {
201        let matcher = HostMatcher::new();
202        assert!(!matcher.remove_host("nonexistent.com"));
203    }
204
205    #[test]
206    fn test_clear() {
207        let matcher = HostMatcher::new();
208        
209        matcher.add_host(VHostConfig::new("example.com", "/var/www"));
210        matcher.add_host(VHostConfig::new("other.com", "/var/other"));
211        matcher.set_default("/var/default");
212        
213        matcher.clear();
214        
215        assert_eq!(matcher.host_count(), 0);
216        assert!(!matcher.has_default());
217    }
218
219    #[test]
220    fn test_hostnames() {
221        let matcher = HostMatcher::new();
222        
223        matcher.add_host(VHostConfig::new("example.com", "/var/www"));
224        matcher.add_host(VHostConfig::new("other.com", "/var/other"));
225        
226        let mut names = matcher.hostnames();
227        names.sort();
228        
229        assert_eq!(names, vec!["example.com", "other.com"]);
230    }
231
232    #[test]
233    fn test_multiple_hosts() {
234        let matcher = HostMatcher::new();
235        
236        matcher.add_host(VHostConfig::new("blog.example.com", "/var/www/blog"));
237        matcher.add_host(VHostConfig::new("api.example.com", "/var/www/api"));
238        matcher.add_host(VHostConfig::new("www.example.com", "/var/www/main"));
239        
240        assert_eq!(matcher.host_count(), 3);
241        
242        let blog = matcher.match_host("blog.example.com").unwrap();
243        assert_eq!(blog.root(), &PathBuf::from("/var/www/blog"));
244        
245        let api = matcher.match_host("api.example.com").unwrap();
246        assert_eq!(api.root(), &PathBuf::from("/var/www/api"));
247    }
248
249    #[test]
250    fn test_default() {
251        let matcher = HostMatcher::default();
252        assert_eq!(matcher.host_count(), 0);
253    }
254
255    #[test]
256    fn test_overwrite_host() {
257        let matcher = HostMatcher::new();
258        
259        matcher.add_host(VHostConfig::new("example.com", "/var/www/v1"));
260        matcher.add_host(VHostConfig::new("example.com", "/var/www/v2"));
261        
262        // Should only have one host
263        assert_eq!(matcher.host_count(), 1);
264        
265        // Should use the latest config
266        let vhost = matcher.match_host("example.com").unwrap();
267        assert_eq!(vhost.root(), &PathBuf::from("/var/www/v2"));
268    }
269}