cdns_rs/
cfg_host_parser.rs

1/*-
2 * cdns-rs - a simple sync/async DNS query library
3 * 
4 * Copyright (C) 2020  Aleksandr Morozov
5 * 
6 * Copyright 2025 Aleksandr Morozov
7 * 
8 * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by
9 * the European Commission - subsequent versions of the EUPL (the "Licence").
10 * 
11 * You may not use this work except in compliance with the Licence.
12 * 
13 * You may obtain a copy of the Licence at:
14 * 
15 *    https://joinup.ec.europa.eu/software/page/eupl
16 * 
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
19 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
20 * Licence for the specific language governing permissions and limitations
21 * under the Licence.
22 */
23
24/// This file contains the config file parsers.
25
26use std::borrow::Borrow;
27use std::collections::HashSet;
28use std::hash::{Hash, Hasher};
29use std::net::IpAddr;
30
31use crate::common::HOST_CFG_PATH;
32use crate::{error::*, internal_error_map, write_error, QType};
33use crate::tokenizer::*;
34
35
36/// An /etc/hosts file parser
37
38
39#[derive(Clone, Debug)]
40pub struct HostnameEntry
41{
42    ip: IpAddr,
43    hostnames: Vec<String>,
44}
45
46impl Eq for HostnameEntry {}
47
48impl PartialEq for HostnameEntry 
49{
50    fn eq(&self, other: &HostnameEntry) -> bool 
51    {
52        return self.ip == other.ip;
53    }
54}
55
56impl Borrow<IpAddr> for HostnameEntry
57{
58    fn borrow(&self) -> &IpAddr 
59    {
60        return &self.ip;    
61    }
62}
63
64impl Hash for HostnameEntry 
65{
66    fn hash<H: Hasher>(&self, state: &mut H) 
67    {
68        self.ip.hash(state);
69    }
70}
71
72
73impl HostnameEntry
74{
75    /// Returns the IP address
76    pub 
77    fn get_ip(&self) -> &IpAddr
78    {
79        return &self.ip;
80    }
81
82    /// Returns the slice with the hostnames
83    pub 
84    fn get_hostnames(&self) -> &[String]
85    {
86        return self.hostnames.as_slice();
87    }
88
89    /// Returns the iterator of the hostnames
90    pub 
91    fn get_hostnames_iter(&self) -> std::slice::Iter<'_, String>
92    {
93        return self.hostnames.iter();
94    }
95}
96
97#[derive(Clone, Debug)]
98pub struct HostConfig
99{
100    hostnames: HashSet<HostnameEntry>
101}
102
103impl Default for HostConfig
104{
105    fn default() -> Self 
106    {
107        return
108            Self 
109            { 
110                hostnames: Default::default(), 
111            };
112    }
113}
114
115impl HostConfig
116{
117    pub 
118    fn is_empty(&self) -> bool
119    {
120        return self.hostnames.is_empty();
121    }
122
123    pub 
124    fn search_by_ip(&self, ip: &IpAddr) -> Option<&HostnameEntry>
125    {
126        return self.hostnames.get(ip);
127    }
128
129    // Expensive operation. Walks through the list in On(Ologn) or On^2
130    pub 
131    fn search_by_fqdn(&self, qtype: &QType, name: &str) -> Option<&HostnameEntry>
132    {
133        for host in self.hostnames.iter()
134        {
135            for fqdn in host.hostnames.iter()
136            {
137                // check that fqdn match and IP type is same as requested
138                if name == fqdn.as_str() && qtype.ipaddr_match(&host.ip)
139                {
140                    return Some(host);
141                }
142            }
143        }
144
145        return None;
146    }
147
148    pub 
149    fn parse_host_file_internal(file_content: String) -> CDnsResult<Self>
150    {
151        let mut tk = ConfTokenizer::from_str(&file_content)?;
152        let mut he_list: HashSet<HostnameEntry> = HashSet::new();
153
154        loop
155        {
156            let field_ip = tk.read_next()?;
157    
158            if field_ip.is_none() == true
159            {
160                // reached EOF
161                break;
162            }
163
164            //println!("ip: {}", field_ip.as_ref().unwrap());
165    
166            let ip: IpAddr = 
167                match field_ip.unwrap().parse()
168                {
169                    Ok(r) => r,
170                    Err(_e) => 
171                    {
172                        // skip
173                        tk.skip_upto_eol();
174                        continue;
175                    }
176                };
177    
178            let hostnames = tk.read_upto_eol()?;
179
180            if hostnames.len() > 0
181            {
182                let he = HostnameEntry{ ip: ip, hostnames: hostnames };
183                he_list.insert(he);
184            }
185            else
186            {
187                write_error!(
188                    internal_error_map!(CDnsErrorType::ConfigError, 
189                        "in file: '{}' IP is not defined with domain name: '{}'\n", HOST_CFG_PATH, ip)
190                );
191            }
192        }
193
194        if he_list.len() == 0
195        {
196            write_error!(
197                internal_error_map!(CDnsErrorType::ConfigError, "file: '{}' file is empty or damaged!\n", 
198                    HOST_CFG_PATH)
199            );
200        }
201
202        return Ok(
203            Self
204            {
205                hostnames: he_list
206            }
207        );
208    }    
209}
210
211#[cfg(test)]
212mod tests
213{
214    use std::net::IpAddr;
215
216    use crate::cfg_host_parser::HostConfig;
217    
218    #[test]
219    fn test_parse_host_file_0()
220    {
221        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
222        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
223        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
224        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
225
226        let ip1: IpAddr = "127.0.1.1".parse().unwrap();
227        let ip2: IpAddr = "::1".parse().unwrap();
228        let ip3: IpAddr = "ff02::1".parse().unwrap();
229        let ip4: IpAddr = "ff02::2".parse().unwrap();
230
231        let ip_list = 
232            vec![
233                (ip1, hosts1), 
234                (ip2, hosts2), 
235                (ip3, hosts3),
236                (ip4, hosts4)
237            ];
238
239        let test =
240        "127.0. 0.1	localhost
241        127.0.1.1	debian-laptop
242        
243        # The following lines are desirable for IPv6 capable hosts
244        ::1     localhost ip6-localhost ip6-loopback
245        ff02::1 ip6-allnodes
246        ff02::2 ip6-allrouters".to_string();
247
248        let p = HostConfig::parse_host_file_internal(test);
249        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
250
251        let p = p.unwrap();
252
253        for (ip, host) in ip_list
254        {
255            let res = p.hostnames.get(&ip);
256            assert_eq!(res.is_some(), true);
257
258            let res = res.unwrap();
259
260            assert_eq!(res.hostnames, host);
261        }
262
263        return;
264    }
265
266    #[test]
267    fn test_parse_host_file()
268    {
269
270        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
271        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
272        let ip2:IpAddr = "::1".parse().unwrap();
273        let ip3:IpAddr = "ff02::1".parse().unwrap();
274        let ip4:IpAddr = "ff02::2".parse().unwrap();
275
276        let hosts0: Vec<&'static str> = vec!["localhost"];
277        let hosts1: Vec<&'static str> = vec!["debian-laptop"];
278        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
279        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
280        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
281
282        let ip_list = 
283            vec![
284                (ip0, hosts0),
285                (ip1, hosts1), 
286                (ip2, hosts2), 
287                (ip3, hosts3),
288                (ip4, hosts4)
289            ];
290
291        let test =
292        "127.0.0.1	localhost
293        127.0.1.1	debian-laptop
294        
295        # The following lines are desirable for IPv6 capable hosts
296        ::1     localhost ip6-localhost ip6-loopback
297        ff02::1 ip6-allnodes
298        ff02::2 ip6-allrouters".to_string();
299
300
301        let p = HostConfig::parse_host_file_internal(test);
302        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
303
304        let p = p.unwrap();
305
306        for (ip, host) in ip_list
307        {
308            let res = p.hostnames.get(&ip);
309            assert_eq!(res.is_some(), true);
310
311            let res = res.unwrap();
312
313            assert_eq!(res.hostnames, host);
314        }
315    }
316
317    #[test]
318    fn test_parse_host_file_2()
319    {
320
321        let ip0:IpAddr = "127.0.0.1".parse().unwrap();
322        let ip1:IpAddr = "127.0.1.1".parse().unwrap();
323        let ip2:IpAddr = "::1".parse().unwrap();
324        let ip3:IpAddr = "ff02::1".parse().unwrap();
325        let ip4:IpAddr = "ff02::2".parse().unwrap();
326
327        let hosts0: Vec<&'static str> = vec!["localhost", "localdomain", "domain.local"];
328        let hosts1: Vec<&'static str> = vec!["debian-laptop", "test123.domain.tld"];
329        let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
330        let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
331        let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
332
333
334        let ip_list = 
335            vec![
336                (ip0, hosts0),
337                (ip1, hosts1), 
338                (ip2, hosts2), 
339                (ip3, hosts3),
340                (ip4, hosts4)
341            ];
342
343        let test =
344        "127.0.0.1	localhost localdomain domain.local
345        127.0.1.1	debian-laptop test123.domain.tld
346        
347        # The following lines are desirable for IPv6 capable hosts
348        #
349        #
350        ::1     localhost ip6-localhost ip6-loopback
351        ff02::1 ip6-allnodes
352        ff02::2 ip6-allrouters
353        ".to_string();
354
355        let p = HostConfig::parse_host_file_internal(test);
356        assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
357
358        let p = p.unwrap();
359
360        for (ip, host) in ip_list
361        {
362            let res = p.hostnames.get(&ip);
363            assert_eq!(res.is_some(), true);
364
365            let res = res.unwrap();
366
367            assert_eq!(res.hostnames, host);
368        }
369    }
370
371}