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(®istry)?;
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(actual, "https://example.com/rdap/");
388 }
389}