icann_rdap_cli/dirs/
fcbs.rs

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