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::a_sync::QType;
32use crate::a_sync::common::HOST_CFG_PATH;
33use crate::{error::*, writer_error};
34use crate::tokenizer::*;
35
36
37/// An /etc/hosts file parser
38
39
40#[derive(Clone, Debug)]
41pub struct HostnameEntry
42{
43    ip: IpAddr,
44    hostnames: Vec<String>,
45}
46
47impl Eq for HostnameEntry {}
48
49impl PartialEq for HostnameEntry 
50{
51    fn eq(&self, other: &HostnameEntry) -> bool 
52    {
53        return self.ip == other.ip;
54    }
55}
56
57impl Borrow<IpAddr> for HostnameEntry
58{
59    fn borrow(&self) -> &IpAddr 
60    {
61        return &self.ip;    
62    }
63}
64
65impl Hash for HostnameEntry 
66{
67    fn hash<H: Hasher>(&self, state: &mut H) 
68    {
69        self.ip.hash(state);
70    }
71}
72
73
74impl HostnameEntry
75{
76    /// Returns the IP address
77    pub 
78    fn get_ip(&self) -> &IpAddr
79    {
80        return &self.ip;
81    }
82
83    /// Returns the slice with the hostnames
84    pub 
85    fn get_hostnames(&self) -> &[String]
86    {
87        return self.hostnames.as_slice();
88    }
89
90    /// Returns the iterator of the hostnames
91    pub 
92    fn get_hostnames_iter(&self) -> std::slice::Iter<'_, String>
93    {
94        return self.hostnames.iter();
95    }
96}
97
98#[derive(Clone, Debug)]
99pub struct HostConfig
100{
101    hostnames: HashSet<HostnameEntry>
102}
103
104impl Default for HostConfig
105{
106    fn default() -> Self 
107    {
108        return
109            Self 
110            { 
111                hostnames: Default::default(), 
112            };
113    }
114}
115
116impl HostConfig
117{
118    pub 
119    fn is_empty(&self) -> bool
120    {
121        return self.hostnames.is_empty();
122    }
123
124    pub 
125    fn search_by_ip(&self, ip: &IpAddr) -> Option<&HostnameEntry>
126    {
127        return self.hostnames.get(ip);
128    }
129
130    // Expensive operation. Walks through the list in On(Ologn) or On^2
131    pub 
132    fn search_by_fqdn(&self, qtype: &QType, name: &str) -> Option<&HostnameEntry>
133    {
134        for host in self.hostnames.iter()
135        {
136            for fqdn in host.hostnames.iter()
137            {
138                // check that fqdn match and IP type is same as requested
139                if name == fqdn.as_str() && qtype.ipaddr_match(&host.ip)
140                {
141                    return Some(host);
142                }
143            }
144        }
145
146        return None;
147    }
148
149    pub 
150    fn parse_host_file_internal(file_content: String, f: &mut Writer) -> CDnsResult<Self>
151    {
152        let mut tk = ConfTokenizer::from_str(&file_content)?;
153        let mut he_list: HashSet<HostnameEntry> = HashSet::new();
154
155        loop
156        {
157            let field_ip = tk.read_next()?;
158    
159            if field_ip.is_none() == true
160            {
161                // reached EOF
162                break;
163            }
164
165            //println!("ip: {}", field_ip.as_ref().unwrap());
166    
167            let ip: IpAddr = 
168                match field_ip.unwrap().parse()
169                {
170                    Ok(r) => r,
171                    Err(_e) => 
172                    {
173                        // skip
174                        tk.skip_upto_eol();
175                        continue;
176                    }
177                };
178    
179            let hostnames = tk.read_upto_eol()?;
180
181            if hostnames.len() > 0
182            {
183                let he = HostnameEntry{ ip: ip, hostnames: hostnames };
184                he_list.insert(he);
185            }
186            else
187            {
188                writer_error!(f, "in file: '{}' IP is not defined with domain name: '{}'\n", HOST_CFG_PATH, ip);
189            }
190        }
191
192        if he_list.len() == 0
193        {
194            writer_error!(f, "file: '{}' file is empty or damaged!\n", HOST_CFG_PATH);
195        }
196
197        return Ok(
198            Self
199            {
200                hostnames: he_list
201            }
202        );
203    }    
204}
205
206
207#[test]
208fn test_parse_host_file_0()
209{
210    let hosts1: Vec<&'static str> = vec!["debian-laptop"];
211    let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
212    let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
213    let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
214
215    let ip1: IpAddr = "127.0.1.1".parse().unwrap();
216    let ip2: IpAddr = "::1".parse().unwrap();
217    let ip3: IpAddr = "ff02::1".parse().unwrap();
218    let ip4: IpAddr = "ff02::2".parse().unwrap();
219
220    let ip_list = 
221        vec![
222            (ip1, hosts1), 
223            (ip2, hosts2), 
224            (ip3, hosts3),
225            (ip4, hosts4)
226        ];
227
228    let test =
229    "127.0. 0.1	localhost
230    127.0.1.1	debian-laptop
231    
232    # The following lines are desirable for IPv6 capable hosts
233    ::1     localhost ip6-localhost ip6-loopback
234    ff02::1 ip6-allnodes
235    ff02::2 ip6-allrouters".to_string();
236
237    let mut writer = Writer::new();
238
239    let p = HostConfig::parse_host_file_internal(test, &mut writer);
240    assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
241
242    let p = p.unwrap();
243
244    for (ip, host) in ip_list
245    {
246        let res = p.hostnames.get(&ip);
247        assert_eq!(res.is_some(), true);
248
249        let res = res.unwrap();
250
251        assert_eq!(res.hostnames, host);
252    }
253
254    return;
255}
256
257#[test]
258fn test_parse_host_file()
259{
260    let ip0:IpAddr = "127.0.0.1".parse().unwrap();
261    let ip1:IpAddr = "127.0.1.1".parse().unwrap();
262    let ip2:IpAddr = "::1".parse().unwrap();
263    let ip3:IpAddr = "ff02::1".parse().unwrap();
264    let ip4:IpAddr = "ff02::2".parse().unwrap();
265
266    let hosts0: Vec<&'static str> = vec!["localhost"];
267    let hosts1: Vec<&'static str> = vec!["debian-laptop"];
268    let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
269    let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
270    let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
271
272    let ip_list = 
273        vec![
274            (ip0, hosts0),
275            (ip1, hosts1), 
276            (ip2, hosts2), 
277            (ip3, hosts3),
278            (ip4, hosts4)
279        ];
280
281    let test =
282    "127.0.0.1	localhost
283    127.0.1.1	debian-laptop
284    
285    # The following lines are desirable for IPv6 capable hosts
286    ::1     localhost ip6-localhost ip6-loopback
287    ff02::1 ip6-allnodes
288    ff02::2 ip6-allrouters".to_string();
289
290    let mut writer = Writer::new();
291
292    let p = HostConfig::parse_host_file_internal(test, &mut writer);
293    assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
294
295    let p = p.unwrap();
296
297    for (ip, host) in ip_list
298    {
299        let res = p.hostnames.get(&ip);
300        assert_eq!(res.is_some(), true);
301
302        let res = res.unwrap();
303
304        assert_eq!(res.hostnames, host);
305    }
306}
307
308#[test]
309fn test_parse_host_file_2()
310{
311    let ip0:IpAddr = "127.0.0.1".parse().unwrap();
312    let ip1:IpAddr = "127.0.1.1".parse().unwrap();
313    let ip2:IpAddr = "::1".parse().unwrap();
314    let ip3:IpAddr = "ff02::1".parse().unwrap();
315    let ip4:IpAddr = "ff02::2".parse().unwrap();
316
317    let hosts0: Vec<&'static str> = vec!["localhost", "localdomain", "domain.local"];
318    let hosts1: Vec<&'static str> = vec!["debian-laptop", "test123.domain.tld"];
319    let hosts2: Vec<&'static str> = vec!["localhost", "ip6-localhost", "ip6-loopback"];
320    let hosts3: Vec<&'static str> = vec!["ip6-allnodes"];
321    let hosts4: Vec<&'static str> = vec!["ip6-allrouters"];
322
323
324    let ip_list = 
325        vec![
326            (ip0, hosts0),
327            (ip1, hosts1), 
328            (ip2, hosts2), 
329            (ip3, hosts3),
330            (ip4, hosts4)
331        ];
332
333    let test =
334    "127.0.0.1	localhost localdomain domain.local
335    127.0.1.1	debian-laptop test123.domain.tld
336    
337    # The following lines are desirable for IPv6 capable hosts
338    #
339    #
340    ::1     localhost ip6-localhost ip6-loopback
341    ff02::1 ip6-allnodes
342    ff02::2 ip6-allrouters
343    ".to_string();
344
345    let mut writer = Writer::new();
346
347    let p = HostConfig::parse_host_file_internal(test, &mut writer);
348    assert_eq!(p.is_ok(), true, "{}", p.err().unwrap());
349
350    let p = p.unwrap();
351
352    for (ip, host) in ip_list
353    {
354        let res = p.hostnames.get(&ip);
355        assert_eq!(res.is_some(), true);
356
357        let res = res.unwrap();
358
359        assert_eq!(res.hostnames, host);
360    }
361}
362