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