icann_rdap_cli/dirs/
fcbs.rs

1use std::{
2    fs::{self, File},
3    io::{BufRead, BufReader},
4    path::PathBuf,
5};
6
7use icann_rdap_client::iana::{BootstrapStore, RegistryHasNotExpired};
8use icann_rdap_common::{
9    httpdata::HttpData,
10    iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType},
11};
12use tracing::debug;
13
14use super::bootstrap_cache_path;
15
16pub struct FileCacheBootstrapStore;
17
18impl BootstrapStore for FileCacheBootstrapStore {
19    fn has_bootstrap_registry(
20        &self,
21        reg_type: &IanaRegistryType,
22    ) -> Result<bool, icann_rdap_client::RdapClientError> {
23        let path = bootstrap_cache_path().join(reg_type.file_name());
24        if path.exists() {
25            let fc_reg = fetch_file_cache_bootstrap(path, |s| debug!("Checking for {s}"))?;
26            return Ok(Some(fc_reg).registry_has_not_expired());
27        }
28        Ok(false)
29    }
30
31    fn put_bootstrap_registry(
32        &self,
33        reg_type: &IanaRegistryType,
34        registry: IanaRegistry,
35        http_data: HttpData,
36    ) -> Result<(), icann_rdap_client::RdapClientError> {
37        let path = bootstrap_cache_path().join(reg_type.file_name());
38        let data = serde_json::to_string_pretty(&registry)?;
39        let cache_contents = http_data.to_lines(&data)?;
40        fs::write(path, cache_contents)?;
41        Ok(())
42    }
43
44    fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
45        let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapDns.file_name());
46        let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
47        Ok(iana.get_dns_bootstrap_urls(ldh)?)
48    }
49
50    fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
51        let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapAsn.file_name());
52        let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
53        Ok(iana.get_asn_bootstrap_urls(asn)?)
54    }
55
56    fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
57        let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv4.file_name());
58        let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
59        Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
60    }
61
62    fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
63        let path = bootstrap_cache_path().join(IanaRegistryType::RdapBootstrapIpv6.file_name());
64        let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
65        Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
66    }
67
68    fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, icann_rdap_client::RdapClientError> {
69        let path = bootstrap_cache_path().join(IanaRegistryType::RdapObjectTags.file_name());
70        let (iana, _http_data) = fetch_file_cache_bootstrap(path, |s| debug!("Reading {s}"))?;
71        Ok(iana.get_tag_bootstrap_urls(tag)?)
72    }
73}
74
75pub fn fetch_file_cache_bootstrap<F>(
76    path: PathBuf,
77    callback: F,
78) -> Result<(IanaRegistry, HttpData), std::io::Error>
79where
80    F: FnOnce(String),
81{
82    let input = File::open(&path)?;
83    let buf = BufReader::new(input);
84    let mut lines = Vec::new();
85    for line in buf.lines() {
86        lines.push(line?);
87    }
88    let cache_data = HttpData::from_lines(&lines)?;
89    callback(path.display().to_string());
90    let iana: IanaRegistry = serde_json::from_str(&cache_data.1.join(""))?;
91    Ok((iana, cache_data.0))
92}
93
94#[cfg(test)]
95#[allow(non_snake_case)]
96mod test {
97    use icann_rdap_client::{
98        iana::{BootstrapStore, PreferredUrl},
99        rdap::QueryType,
100    };
101    use icann_rdap_common::{
102        httpdata::HttpData,
103        iana::{IanaRegistry, IanaRegistryType},
104    };
105    use serial_test::serial;
106    use test_dir::{DirBuilder, FileType, TestDir};
107
108    use crate::dirs::{self, fcbs::FileCacheBootstrapStore};
109
110    fn test_dir() -> TestDir {
111        let test_dir = TestDir::temp()
112            .create("cache", FileType::Dir)
113            .create("config", FileType::Dir);
114        std::env::set_var("XDG_CACHE_HOME", test_dir.path("cache"));
115        std::env::set_var("XDG_CONFIG_HOME", test_dir.path("config"));
116        dirs::init().expect("unable to init directories");
117        test_dir
118    }
119
120    #[test]
121    #[serial]
122    fn GIVEN_fcbootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
123        // GIVEN
124        let _test_dir = test_dir();
125        let bs = FileCacheBootstrapStore;
126        let bootstrap = r#"
127            {
128                "version": "1.0",
129                "publication": "2024-01-07T10:11:12Z",
130                "description": "Some text",
131                "services": [
132                  [
133                    ["net", "com"],
134                    [
135                      "https://registry.example.com/myrdap/"
136                    ]
137                  ],
138                  [
139                    ["org", "mytld"],
140                    [
141                      "https://example.org/"
142                    ]
143                  ]
144                ]
145            }
146        "#;
147        let iana =
148            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
149        bs.put_bootstrap_registry(
150            &IanaRegistryType::RdapBootstrapDns,
151            iana,
152            HttpData::example().build(),
153        )
154        .expect("put iana registry");
155
156        // WHEN
157        let actual = bs
158            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
159            .expect("get bootstrap url")
160            .preferred_url()
161            .expect("preferred url");
162
163        // THEN
164        assert_eq!(actual, "https://example.org/")
165    }
166
167    #[test]
168    #[serial]
169    fn GIVEN_fcbootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
170        // GIVEN
171        let _test_dir = test_dir();
172        let bs = FileCacheBootstrapStore;
173        let bootstrap = r#"
174            {
175                "version": "1.0",
176                "publication": "2024-01-07T10:11:12Z",
177                "description": "RDAP Bootstrap file for example registries.",
178                "services": [
179                  [
180                    ["64496-64496"],
181                    [
182                      "https://rir3.example.com/myrdap/"
183                    ]
184                  ],
185                  [
186                    ["64497-64510", "65536-65551"],
187                    [
188                      "https://example.org/"
189                    ]
190                  ],
191                  [
192                    ["64512-65534"],
193                    [
194                      "http://example.net/rdaprir2/",
195                      "https://example.net/rdaprir2/"
196                    ]
197                  ]
198                ]
199            }
200        "#;
201        let iana =
202            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
203        bs.put_bootstrap_registry(
204            &IanaRegistryType::RdapBootstrapAsn,
205            iana,
206            HttpData::example().build(),
207        )
208        .expect("put iana registry");
209
210        // WHEN
211        let actual = bs
212            .get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
213            .expect("get bootstrap url")
214            .preferred_url()
215            .expect("preferred url");
216
217        // THEN
218        assert_eq!(actual, "https://example.net/rdaprir2/");
219    }
220
221    #[test]
222    #[serial]
223    fn GIVEN_fcbootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
224        // GIVEN
225        let _test_dir = test_dir();
226        let bs = FileCacheBootstrapStore;
227        let bootstrap = r#"
228            {
229                "version": "1.0",
230                "publication": "2024-01-07T10:11:12Z",
231                "description": "RDAP Bootstrap file for example registries.",
232                "services": [
233                  [
234                    ["198.51.100.0/24", "192.0.0.0/8"],
235                    [
236                      "https://rir1.example.com/myrdap/"
237                    ]
238                  ],
239                  [
240                    ["203.0.113.0/24", "192.0.2.0/24"],
241                    [
242                      "https://example.org/"
243                    ]
244                  ],
245                  [
246                    ["203.0.113.0/28"],
247                    [
248                      "https://example.net/rdaprir2/",
249                      "http://example.net/rdaprir2/"
250                    ]
251                  ]
252                ]
253            }
254        "#;
255        let iana =
256            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
257        bs.put_bootstrap_registry(
258            &IanaRegistryType::RdapBootstrapIpv4,
259            iana,
260            HttpData::example().build(),
261        )
262        .expect("put iana registry");
263
264        // WHEN
265        let actual = bs
266            .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
267            .expect("get bootstrap url")
268            .preferred_url()
269            .expect("preferred url");
270
271        // THEN
272        assert_eq!(actual, "https://rir1.example.com/myrdap/");
273    }
274
275    #[test]
276    #[serial]
277    fn GIVEN_fcbootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
278        // GIVEN
279        let _test_dir = test_dir();
280        let bs = FileCacheBootstrapStore;
281        let bootstrap = r#"
282            {
283                "version": "1.0",
284                "publication": "2024-01-07T10:11:12Z",
285                "description": "RDAP Bootstrap file for example registries.",
286                "services": [
287                  [
288                    ["2001:db8::/34"],
289                    [
290                      "https://rir2.example.com/myrdap/"
291                    ]
292                  ],
293                  [
294                    ["2001:db8:4000::/36", "2001:db8:ffff::/48"],
295                    [
296                      "https://example.org/"
297                    ]
298                  ],
299                  [
300                    ["2001:db8:1000::/36"],
301                    [
302                      "https://example.net/rdaprir2/",
303                      "http://example.net/rdaprir2/"
304                    ]
305                  ]
306                ]
307            }
308        "#;
309        let iana =
310            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
311        bs.put_bootstrap_registry(
312            &IanaRegistryType::RdapBootstrapIpv6,
313            iana,
314            HttpData::example().build(),
315        )
316        .expect("put iana registry");
317
318        // WHEN
319        let actual = bs
320            .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
321            .expect("get bootstrap url")
322            .preferred_url()
323            .expect("preferred url");
324
325        // THEN
326        assert_eq!(actual, "https://rir2.example.com/myrdap/");
327    }
328
329    #[test]
330    #[serial]
331    fn GIVEN_fcbootstrap_with_tag_THEN_get_entity_handle_query_urls_THEN_correct_url() {
332        // GIVEN
333        let _test_dir = test_dir();
334        let bs = FileCacheBootstrapStore;
335        let bootstrap = r#"
336            {
337              "version": "1.0",
338              "publication": "YYYY-MM-DDTHH:MM:SSZ",
339              "description": "RDAP bootstrap file for service provider object tags",
340              "services": [
341                [
342                  ["contact@example.com"],
343                  ["YYYY"],
344                  [
345                    "https://example.com/rdap/"
346                  ]
347                ],
348                [
349                  ["contact@example.org"],
350                  ["ZZ54"],
351                  [
352                    "http://rdap.example.org/"
353                  ]
354                ],
355                [
356                  ["contact@example.net"],
357                  ["1754"],
358                  [
359                    "https://example.net/rdap/",
360                    "http://example.net/rdap/"
361                  ]
362                ]
363              ]
364             }
365        "#;
366        let iana =
367            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
368        bs.put_bootstrap_registry(
369            &IanaRegistryType::RdapObjectTags,
370            iana,
371            HttpData::example().build(),
372        )
373        .expect("put iana registry");
374
375        // WHEN
376        let actual = bs
377            .get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
378            .expect("get bootstrap url")
379            .preferred_url()
380            .expect("preferred url");
381
382        // THEN
383        assert_eq!(actual, "https://example.com/rdap/");
384    }
385}