1use chrono::Utc;
4use serde_json::Value;
5
6use rdap_types::{
7 asn::AsnResponse,
8 common::{RdapEntity, RdapEvent, RdapLink, RdapRemark, RdapRole, RdapStatus, ResponseMeta},
9 domain::{DomainResponse, RegistrarSummary},
10 entity::EntityResponse,
11 error::{RdapError, Result},
12 ip::{IpResponse, IpVersion},
13 nameserver::{NameserverIpAddresses, NameserverResponse},
14};
15
16#[derive(Debug, Clone, Default)]
18pub struct Normalizer;
19
20impl Normalizer {
21 pub fn new() -> Self {
22 Self
23 }
24
25 pub fn domain(
26 &self,
27 query: &str,
28 raw: Value,
29 source: &str,
30 cached: bool,
31 ) -> Result<DomainResponse> {
32 let meta = make_meta(source, cached);
33 let obj = require_object(&raw)?;
34
35 let entities = parse_entities(obj.get("entities"));
36 let events = parse_events(obj.get("events"));
37
38 let nameservers = obj
39 .get("nameservers")
40 .and_then(|v| v.as_array())
41 .map(|arr| {
42 arr.iter()
43 .filter_map(|ns| {
44 ns.get("ldhName")
45 .or_else(|| ns.get("unicodeName"))
46 .and_then(|v| v.as_str())
47 .map(str::to_lowercase)
48 })
49 .collect::<Vec<_>>()
50 })
51 .unwrap_or_default();
52
53 let registrar = extract_registrar(&entities);
54
55 Ok(DomainResponse {
56 query: query.to_string(),
57 ldh_name: string_field(obj, "ldhName"),
58 unicode_name: string_field(obj, "unicodeName"),
59 handle: string_field(obj, "handle"),
60 status: parse_status(obj.get("status")),
61 nameservers,
62 registrar,
63 entities,
64 events,
65 links: parse_links(obj.get("links")),
66 remarks: parse_remarks(obj.get("remarks")),
67 meta,
68 })
69 }
70
71 pub fn ip(&self, query: &str, raw: Value, source: &str, cached: bool) -> Result<IpResponse> {
72 let meta = make_meta(source, cached);
73 let obj = require_object(&raw)?;
74
75 let ip_version = obj
76 .get("ipVersion")
77 .and_then(|v| v.as_str())
78 .map(|s| match s {
79 "v4" => IpVersion::V4,
80 _ => IpVersion::V6,
81 });
82
83 Ok(IpResponse {
84 query: query.to_string(),
85 handle: string_field(obj, "handle"),
86 start_address: string_field(obj, "startAddress"),
87 end_address: string_field(obj, "endAddress"),
88 ip_version,
89 name: string_field(obj, "name"),
90 allocation_type: string_field(obj, "type"),
91 country: string_field(obj, "country"),
92 parent_handle: string_field(obj, "parentHandle"),
93 status: parse_status(obj.get("status")),
94 entities: parse_entities(obj.get("entities")),
95 events: parse_events(obj.get("events")),
96 links: parse_links(obj.get("links")),
97 remarks: parse_remarks(obj.get("remarks")),
98 meta,
99 })
100 }
101
102 pub fn asn(&self, query: u32, raw: Value, source: &str, cached: bool) -> Result<AsnResponse> {
103 let meta = make_meta(source, cached);
104 let obj = require_object(&raw)?;
105
106 Ok(AsnResponse {
107 query,
108 handle: string_field(obj, "handle"),
109 start_autnum: obj
110 .get("startAutnum")
111 .and_then(|v| v.as_u64())
112 .map(|n| n as u32),
113 end_autnum: obj
114 .get("endAutnum")
115 .and_then(|v| v.as_u64())
116 .map(|n| n as u32),
117 name: string_field(obj, "name"),
118 autnum_type: string_field(obj, "type"),
119 country: string_field(obj, "country"),
120 status: parse_status(obj.get("status")),
121 entities: parse_entities(obj.get("entities")),
122 events: parse_events(obj.get("events")),
123 links: parse_links(obj.get("links")),
124 remarks: parse_remarks(obj.get("remarks")),
125 meta,
126 })
127 }
128
129 pub fn nameserver(
130 &self,
131 query: &str,
132 raw: Value,
133 source: &str,
134 cached: bool,
135 ) -> Result<NameserverResponse> {
136 let meta = make_meta(source, cached);
137 let obj = require_object(&raw)?;
138
139 let ip_addresses = {
140 let ip_obj = obj.get("ipAddresses").and_then(|v| v.as_object());
141 NameserverIpAddresses {
142 v4: ip_obj
143 .and_then(|o| o.get("v4"))
144 .and_then(|v| v.as_array())
145 .map(|arr| {
146 arr.iter()
147 .filter_map(|v| v.as_str().map(str::to_string))
148 .collect()
149 })
150 .unwrap_or_default(),
151 v6: ip_obj
152 .and_then(|o| o.get("v6"))
153 .and_then(|v| v.as_array())
154 .map(|arr| {
155 arr.iter()
156 .filter_map(|v| v.as_str().map(str::to_string))
157 .collect()
158 })
159 .unwrap_or_default(),
160 }
161 };
162
163 Ok(NameserverResponse {
164 query: query.to_string(),
165 handle: string_field(obj, "handle"),
166 ldh_name: string_field(obj, "ldhName"),
167 unicode_name: string_field(obj, "unicodeName"),
168 ip_addresses,
169 status: parse_status(obj.get("status")),
170 entities: parse_entities(obj.get("entities")),
171 events: parse_events(obj.get("events")),
172 links: parse_links(obj.get("links")),
173 remarks: parse_remarks(obj.get("remarks")),
174 meta,
175 })
176 }
177
178 pub fn entity(
179 &self,
180 query: &str,
181 raw: Value,
182 source: &str,
183 cached: bool,
184 ) -> Result<EntityResponse> {
185 let meta = make_meta(source, cached);
186 let obj = require_object(&raw)?;
187
188 let roles = obj
189 .get("roles")
190 .and_then(|v| v.as_array())
191 .map(|arr| {
192 arr.iter()
193 .filter_map(|v| serde_json::from_value::<RdapRole>(v.clone()).ok())
194 .collect()
195 })
196 .unwrap_or_default();
197
198 Ok(EntityResponse {
199 query: query.to_string(),
200 handle: string_field(obj, "handle"),
201 vcard_array: obj.get("vcardArray").cloned(),
202 roles,
203 status: parse_status(obj.get("status")),
204 entities: parse_entities(obj.get("entities")),
205 events: parse_events(obj.get("events")),
206 links: parse_links(obj.get("links")),
207 remarks: parse_remarks(obj.get("remarks")),
208 meta,
209 })
210 }
211}
212
213fn make_meta(source: &str, cached: bool) -> ResponseMeta {
216 ResponseMeta {
217 source: source.to_string(),
218 queried_at: Utc::now().to_rfc3339(),
219 cached,
220 }
221}
222
223fn require_object(value: &Value) -> Result<&serde_json::Map<String, Value>> {
224 value.as_object().ok_or_else(|| RdapError::ParseError {
225 reason: "Expected a JSON object at the response root".to_string(),
226 })
227}
228
229fn string_field(obj: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
230 obj.get(key).and_then(|v| v.as_str()).map(str::to_string)
231}
232
233fn parse_status(value: Option<&Value>) -> Vec<RdapStatus> {
234 value
235 .and_then(|v| v.as_array())
236 .map(|arr| {
237 arr.iter()
238 .filter_map(|v| serde_json::from_value::<RdapStatus>(v.clone()).ok())
239 .collect()
240 })
241 .unwrap_or_default()
242}
243
244fn parse_events(value: Option<&Value>) -> Vec<RdapEvent> {
245 value
246 .and_then(|v| v.as_array())
247 .map(|arr| {
248 arr.iter()
249 .filter_map(|v| serde_json::from_value::<RdapEvent>(v.clone()).ok())
250 .collect()
251 })
252 .unwrap_or_default()
253}
254
255fn parse_links(value: Option<&Value>) -> Vec<RdapLink> {
256 value
257 .and_then(|v| v.as_array())
258 .map(|arr| {
259 arr.iter()
260 .filter_map(|v| serde_json::from_value::<RdapLink>(v.clone()).ok())
261 .collect()
262 })
263 .unwrap_or_default()
264}
265
266fn parse_remarks(value: Option<&Value>) -> Vec<RdapRemark> {
267 value
268 .and_then(|v| v.as_array())
269 .map(|arr| {
270 arr.iter()
271 .filter_map(|v| serde_json::from_value::<RdapRemark>(v.clone()).ok())
272 .collect()
273 })
274 .unwrap_or_default()
275}
276
277fn parse_entities(value: Option<&Value>) -> Vec<RdapEntity> {
278 value
279 .and_then(|v| v.as_array())
280 .map(|arr| {
281 arr.iter()
282 .filter_map(|v| serde_json::from_value::<RdapEntity>(v.clone()).ok())
283 .collect()
284 })
285 .unwrap_or_default()
286}
287
288fn extract_registrar(entities: &[RdapEntity]) -> Option<RegistrarSummary> {
289 let registrar_entity = entities
290 .iter()
291 .find(|e| e.roles.iter().any(|r| matches!(r, RdapRole::Registrar)))?;
292
293 let name = registrar_entity
294 .vcard_array
295 .as_ref()
296 .and_then(extract_vcard_name);
297
298 let url = registrar_entity
299 .links
300 .iter()
301 .find(|l| l.rel.as_deref() == Some("self"))
302 .map(|l| l.href.clone());
303
304 Some(RegistrarSummary {
305 name,
306 handle: registrar_entity.handle.clone(),
307 url,
308 abuse_email: None,
309 abuse_phone: None,
310 })
311}
312
313fn extract_vcard_name(vcard: &Value) -> Option<String> {
314 let outer = vcard.as_array()?;
315 let props = outer.get(1)?.as_array()?;
316
317 for prop in props {
318 let arr = prop.as_array()?;
319 if arr.first()?.as_str()? == "fn" {
320 return arr.get(3).and_then(|v| v.as_str()).map(str::to_string);
321 }
322 }
323 None
324}
325
326#[cfg(test)]
329mod tests {
330 use super::*;
331 use serde_json::json;
332
333 fn norm() -> Normalizer {
334 Normalizer::new()
335 }
336
337 #[test]
338 fn domain_basic_fields() {
339 let raw = json!({
340 "ldhName": "EXAMPLE.COM",
341 "unicodeName": "example.com",
342 "handle": "DOMAIN-HANDLE-1",
343 "status": ["active"],
344 "nameservers": [
345 { "ldhName": "NS1.EXAMPLE.COM" },
346 { "ldhName": "NS2.EXAMPLE.COM" }
347 ]
348 });
349 let res = norm()
350 .domain("example.com", raw, "https://rdap.example/", false)
351 .unwrap();
352
353 assert_eq!(res.query, "example.com");
354 assert_eq!(res.ldh_name.as_deref(), Some("EXAMPLE.COM"));
355 assert!(res.is_active());
356 assert_eq!(res.nameservers, vec!["ns1.example.com", "ns2.example.com"]);
357 }
358
359 #[test]
360 fn ip_basic_v4_fields() {
361 let raw = json!({
362 "handle": "NET-192-0-2-0-1",
363 "startAddress": "192.0.2.0",
364 "ipVersion": "v4",
365 "country": "US"
366 });
367 let res = norm()
368 .ip("192.0.2.0/24", raw, "https://rdap.arin.net/", false)
369 .unwrap();
370
371 assert_eq!(res.country.as_deref(), Some("US"));
372 assert_eq!(res.ip_version, Some(IpVersion::V4));
373 }
374
375 #[test]
376 fn asn_basic_fields() {
377 let raw = json!({
378 "handle": "AS15169",
379 "startAutnum": 15169,
380 "name": "GOOGLE",
381 "country": "US"
382 });
383 let res = norm()
384 .asn(15169, raw, "https://rdap.arin.net/", false)
385 .unwrap();
386
387 assert_eq!(res.query, 15169);
388 assert_eq!(res.name.as_deref(), Some("GOOGLE"));
389 }
390
391 #[test]
392 fn nameserver_basic_fields() {
393 let raw = json!({
394 "ldhName": "NS1.EXAMPLE.COM",
395 "ipAddresses": {
396 "v4": ["192.0.2.1"],
397 "v6": ["2001:db8::1"]
398 }
399 });
400 let res = norm()
401 .nameserver("ns1.example.com", raw, "s", false)
402 .unwrap();
403
404 assert_eq!(res.ip_addresses.v4, vec!["192.0.2.1"]);
405 assert_eq!(res.ip_addresses.v6, vec!["2001:db8::1"]);
406 }
407
408 #[test]
409 fn domain_non_object_json_returns_error() {
410 let res = norm().domain("example.com", json!([1, 2, 3]), "s", false);
411 assert!(res.is_err());
412 }
413}