Skip to main content

netget/server/epp/
wire.rs

1//! RFC 5734 framing, RFC 5730 commands as structured JSON, and the responses NetGet writes for
2//! the domain (RFC 5731), host (RFC 5732) and contact (RFC 5733) mappings.
3use super::xml::{el, escape, Node, CONTACT, DOMAIN, EPP, HOST};
4use anyhow::{bail, ensure, Context, Result};
5use serde_json::{json, Map, Value};
6use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
7
8/// The largest frame either side accepts, header included.
9pub const MAX_FRAME: usize = 256 * 1024;
10
11pub async fn read_frame<R: AsyncRead + Unpin>(r: &mut R) -> Result<Option<Vec<u8>>> {
12    let mut len = [0u8; 4];
13    match r.read_exact(&mut len).await {
14        Ok(_) => {}
15        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
16        Err(e) => return Err(e.into()),
17    }
18    let total = u32::from_be_bytes(len) as usize;
19    ensure!(
20        total > 4,
21        "a frame length of {total} is shorter than its header"
22    );
23    ensure!(
24        total <= MAX_FRAME,
25        "a frame of {total} bytes exceeds {MAX_FRAME}"
26    );
27    let mut body = vec![0u8; total - 4];
28    r.read_exact(&mut body).await?;
29    Ok(Some(body))
30}
31
32pub async fn write_frame<W: AsyncWrite + Unpin>(w: &mut W, xml: &str) -> Result<()> {
33    ensure!(
34        xml.len() + 4 <= MAX_FRAME,
35        "a response of {} bytes exceeds {MAX_FRAME}",
36        xml.len()
37    );
38    let mut out = Vec::with_capacity(xml.len() + 4);
39    out.extend_from_slice(&((xml.len() + 4) as u32).to_be_bytes());
40    out.extend_from_slice(xml.as_bytes());
41    w.write_all(&out).await?;
42    w.flush().await?;
43    Ok(())
44}
45
46/// RFC 5730 §3 result codes with their standard messages.
47pub const RESULT_CODES: &[(u16, &str)] = &[
48    (1000, "Command completed successfully"),
49    (1001, "Command completed successfully; action pending"),
50    (1300, "Command completed successfully; no messages"),
51    (1301, "Command completed successfully; ack to dequeue"),
52    (1500, "Command completed successfully; ending session"),
53    (2000, "Unknown command"),
54    (2001, "Command syntax error"),
55    (2002, "Command use error"),
56    (2003, "Required parameter missing"),
57    (2004, "Parameter value range error"),
58    (2005, "Parameter value syntax error"),
59    (2100, "Unimplemented protocol version"),
60    (2101, "Unimplemented command"),
61    (2102, "Unimplemented option"),
62    (2103, "Unimplemented extension"),
63    (2104, "Billing failure"),
64    (2105, "Object is not eligible for renewal"),
65    (2106, "Object is not eligible for transfer"),
66    (2200, "Authentication error"),
67    (2201, "Authorization error"),
68    (2202, "Invalid authorization information"),
69    (2300, "Object pending transfer"),
70    (2301, "Object not pending transfer"),
71    (2302, "Object exists"),
72    (2303, "Object does not exist"),
73    (2304, "Object status prohibits operation"),
74    (2305, "Object association prohibits operation"),
75    (2306, "Parameter value policy error"),
76    (2307, "Unimplemented object service"),
77    (2308, "Data management policy violation"),
78    (2400, "Command failed"),
79    (2500, "Command failed; server closing connection"),
80    (2501, "Authentication error; server closing connection"),
81    (2502, "Session limit exceeded; server closing connection"),
82];
83
84pub fn message(code: u16) -> &'static str {
85    RESULT_CODES
86        .iter()
87        .find(|(c, _)| *c == code)
88        .map(|(_, m)| *m)
89        .unwrap_or("Command failed")
90}
91
92/// A refusal decided while reading the command.
93#[derive(Debug)]
94pub struct Refusal {
95    pub code: u16,
96    pub reason: String,
97}
98fn refuse(code: u16, reason: impl Into<String>) -> Refusal {
99    Refusal {
100        code,
101        reason: reason.into(),
102    }
103}
104
105#[derive(Debug)]
106pub enum Command {
107    Hello,
108    Login {
109        client_id: String,
110        password: String,
111        version: String,
112        obj_uris: Vec<String>,
113    },
114    Logout,
115    Poll {
116        op: String,
117        msg_id: Option<String>,
118    },
119    /// An object command for the handler: (command, object, fields).
120    Object(String, String, Value),
121}
122
123pub struct Frame {
124    pub command: Command,
125    pub cl_trid: Option<String>,
126}
127
128pub fn object_of(ns: &str) -> Option<&'static str> {
129    match ns {
130        DOMAIN => Some("domain"),
131        HOST => Some("host"),
132        CONTACT => Some("contact"),
133        _ => None,
134    }
135}
136
137pub fn namespace_of(object: &str) -> &'static str {
138    match object {
139        "domain" => DOMAIN,
140        "host" => HOST,
141        _ => CONTACT,
142    }
143}
144
145fn required(n: &Node, name: &str) -> Result<String, Refusal> {
146    n.text_of(name)
147        .filter(|t| !t.is_empty())
148        .ok_or_else(|| refuse(2003, format!("{name} is required")))
149}
150
151fn auth_info(n: &Node) -> Option<String> {
152    n.child("authInfo").and_then(|a| a.text_of("pw"))
153}
154
155fn period(n: &Node) -> Value {
156    n.child("period")
157        .map(|p| json!({"value": p.text().parse::<u32>().ok(), "unit": p.attr("unit").unwrap_or("y")}))
158        .unwrap_or(Value::Null)
159}
160
161fn statuses(n: &Node) -> Vec<String> {
162    n.all("status")
163        .filter_map(|s| s.attr("s").map(str::to_owned))
164        .collect()
165}
166
167fn domain_change(n: Option<&Node>) -> Value {
168    let Some(n) = n else { return Value::Null };
169    json!({
170        "ns": n.child("ns").map(|ns| ns.texts_of("hostObj")).unwrap_or_default(),
171        "contacts": n.all("contact").map(|c| json!({"type": c.attr("type").unwrap_or(""), "id": c.text()})).collect::<Vec<_>>(),
172        "status": statuses(n),
173    })
174}
175
176fn addresses(n: &Node) -> Vec<Value> {
177    n.all("addr")
178        .map(|a| json!({"ip": a.attr("ip").unwrap_or("v4"), "addr": a.text()}))
179        .collect()
180}
181
182/// The fields of an object command, as the handler sees them.
183fn fields(command: &str, object: &str, n: &Node) -> Result<Value, Refusal> {
184    let key = if object == "contact" { "id" } else { "name" };
185    Ok(match (command, object) {
186        ("check", _) => {
187            let names = n.texts_of(key);
188            if names.is_empty() {
189                return Err(refuse(2003, format!("check names at least one {key}")));
190            }
191            if names.len() > 64 {
192                return Err(refuse(2306, "at most 64 objects per check"));
193            }
194            json!({"names": names})
195        }
196        ("info", _) => json!({key: required(n, key)?, "auth_info": auth_info(n)}),
197        ("delete", _) => json!({key: required(n, key)?}),
198        ("create", "domain") => json!({
199            "name": required(n, "name")?,
200            "period": period(n),
201            "ns": n.child("ns").map(|ns| ns.texts_of("hostObj")).unwrap_or_default(),
202            "registrant": n.text_of("registrant"),
203            "contacts": n.all("contact").map(|c| json!({"type": c.attr("type").unwrap_or(""), "id": c.text()})).collect::<Vec<_>>(),
204            "auth_info": auth_info(n),
205        }),
206        ("create", "host") => json!({"name": required(n, "name")?, "addrs": addresses(n)}),
207        ("create", "contact") => {
208            let postal = n.child("postalInfo");
209            let addr = postal.and_then(|p| p.child("addr"));
210            json!({
211                "id": required(n, "id")?,
212                "name": postal.and_then(|p| p.text_of("name")),
213                "org": postal.and_then(|p| p.text_of("org")),
214                "street": addr.map(|a| a.texts_of("street")).unwrap_or_default(),
215                "city": addr.and_then(|a| a.text_of("city")),
216                "sp": addr.and_then(|a| a.text_of("sp")),
217                "pc": addr.and_then(|a| a.text_of("pc")),
218                "cc": addr.and_then(|a| a.text_of("cc")),
219                "voice": n.text_of("voice"),
220                "fax": n.text_of("fax"),
221                "email": n.text_of("email"),
222                "auth_info": auth_info(n),
223            })
224        }
225        ("renew", "domain") => json!({
226            "name": required(n, "name")?,
227            "cur_exp_date": required(n, "curExpDate")?,
228            "period": period(n),
229        }),
230        ("transfer", "domain") | ("transfer", "contact") => {
231            json!({key: required(n, key)?, "period": period(n), "auth_info": auth_info(n)})
232        }
233        ("update", "domain") => json!({
234            "name": required(n, "name")?,
235            "add": domain_change(n.child("add")),
236            "rem": domain_change(n.child("rem")),
237            "chg": n.child("chg").map(|c| json!({"registrant": c.text_of("registrant"), "auth_info": auth_info(c)})),
238        }),
239        ("update", "host") => json!({
240            "name": required(n, "name")?,
241            "add": n.child("add").map(|a| json!({"addrs": addresses(a), "status": statuses(a)})),
242            "rem": n.child("rem").map(|a| json!({"addrs": addresses(a), "status": statuses(a)})),
243            "new_name": n.child("chg").and_then(|c| c.text_of("name")),
244        }),
245        ("update", "contact") => {
246            json!({"id": required(n, "id")?, "change": n.child("chg").map(|c| json!({"email": c.text_of("email"), "voice": c.text_of("voice")}))})
247        }
248        (c, o) => {
249            return Err(refuse(
250                2101,
251                format!("{c} is not implemented for {o} objects"),
252            ))
253        }
254    })
255}
256
257/// Read a frame's command.
258pub fn command(root: &Node) -> Result<Frame, Refusal> {
259    if root.ns != EPP || root.name != "epp" {
260        return Err(refuse(
261            2001,
262            "the root element is not epp in urn:ietf:params:xml:ns:epp-1.0",
263        ));
264    }
265    let inner = root
266        .first()
267        .ok_or_else(|| refuse(2001, "an empty epp element"))?;
268    if inner.ns != EPP {
269        return Err(refuse(2001, "an element outside the EPP namespace"));
270    }
271    match inner.name.as_str() {
272        "hello" => {
273            return Ok(Frame {
274                command: Command::Hello,
275                cl_trid: None,
276            })
277        }
278        "command" => {}
279        other => return Err(refuse(2001, format!("{other} is not a command"))),
280    }
281    let cl_trid = inner.text_of("clTRID").filter(|t| !t.is_empty());
282    if cl_trid
283        .as_ref()
284        .is_some_and(|t| t.len() < 3 || t.len() > 64)
285    {
286        return Err(refuse(2005, "clTRID is 3 to 64 characters"));
287    }
288    let verb = inner
289        .children
290        .iter()
291        .find(|c| c.name != "clTRID" && c.name != "extension")
292        .ok_or_else(|| refuse(2001, "a command names no operation"))?;
293    if verb.ns != EPP {
294        return Err(refuse(2001, "the operation is not in the EPP namespace"));
295    }
296    let command = match verb.name.as_str() {
297        "login" => Command::Login {
298            client_id: required(verb, "clID")?,
299            password: required(verb, "pw")?,
300            version: verb
301                .child("options")
302                .and_then(|o| o.text_of("version"))
303                .unwrap_or_default(),
304            obj_uris: verb
305                .child("svcs")
306                .map(|s| s.texts_of("objURI"))
307                .unwrap_or_default(),
308        },
309        "logout" => Command::Logout,
310        "poll" => Command::Poll {
311            op: verb.attr("op").unwrap_or("req").to_owned(),
312            msg_id: verb.attr("msgID").map(str::to_owned),
313        },
314        name @ ("check" | "info" | "create" | "delete" | "renew" | "transfer" | "update") => {
315            let obj = verb
316                .first()
317                .ok_or_else(|| refuse(2001, format!("{name} names no object")))?;
318            let object = object_of(&obj.ns)
319                .ok_or_else(|| refuse(2307, format!("{} objects are not served", obj.ns)))?;
320            if obj.name != name {
321                return Err(refuse(
322                    2001,
323                    format!("{name} holds {}:{}", object, obj.name),
324                ));
325            }
326            let mut f = fields(name, object, obj)?;
327            if name == "transfer" {
328                let op = verb.attr("op").unwrap_or("");
329                if !matches!(op, "request" | "query" | "approve" | "reject" | "cancel") {
330                    return Err(refuse(
331                        2005,
332                        "transfer op is request, query, approve, reject or cancel",
333                    ));
334                }
335                f["op"] = json!(op);
336            }
337            Command::Object(name.to_owned(), object.to_owned(), f)
338        }
339        other => return Err(refuse(2000, format!("{other} is not an EPP command"))),
340    };
341    Ok(Frame { command, cl_trid })
342}
343
344pub fn greeting(server_id: &str, now: &str, objects: &[&str]) -> String {
345    let uris: String = objects
346        .iter()
347        .map(|o| el("objURI", namespace_of(o)))
348        .collect();
349    format!(
350        r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><epp xmlns="{EPP}"><greeting>{}{}<svcMenu><version>1.0</version><lang>en</lang>{uris}</svcMenu><dcp><access><all/></access><statement><purpose><admin/><prov/></purpose><recipient><ours/><public/></recipient><retention><stated/></retention></statement></dcp></greeting></epp>"#,
351        el("svID", server_id),
352        el("svDate", now)
353    )
354}
355
356pub fn response(
357    code: u16,
358    msg: &str,
359    reason: Option<&str>,
360    res_data: &str,
361    cl_trid: Option<&str>,
362    sv_trid: &str,
363) -> String {
364    let reason = reason
365        .filter(|r| !r.is_empty())
366        .map(|r| {
367            format!(
368                "<extValue><value><text/></value>{}</extValue>",
369                el("reason", r)
370            )
371        })
372        .unwrap_or_default();
373    let res_data = if res_data.is_empty() {
374        String::new()
375    } else {
376        format!("<resData>{res_data}</resData>")
377    };
378    let cl = cl_trid.map(|c| el("clTRID", c)).unwrap_or_default();
379    format!(
380        r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?><epp xmlns="{EPP}"><response><result code="{code}">{}{reason}</result>{res_data}<trID>{cl}{}</trID></response></epp>"#,
381        el("msg", msg),
382        el("svTRID", sv_trid)
383    )
384}
385
386fn s<'a>(v: &'a Value, k: &str) -> Option<&'a str> {
387    v.get(k).and_then(Value::as_str).filter(|x| !x.is_empty())
388}
389fn opt(tag: &str, v: &Value, k: &str) -> String {
390    s(v, k).map(|x| el(tag, x)).unwrap_or_default()
391}
392fn list(v: &Value, k: &str) -> Vec<String> {
393    v.get(k)
394        .and_then(Value::as_array)
395        .map(|a| {
396            a.iter()
397                .filter_map(|x| x.as_str().map(str::to_owned))
398                .collect()
399        })
400        .unwrap_or_default()
401}
402
403/// The resData a handler action describes, for the command it answers.
404pub fn res_data(action: &Value, command: &str, object: &str) -> Result<String> {
405    let p = object;
406    let ns = namespace_of(object);
407    let key = if object == "contact" { "id" } else { "name" };
408    let status = |v: &Value| -> String {
409        let st = list(v, "status");
410        let st = if st.is_empty() {
411            vec!["ok".to_owned()]
412        } else {
413            st
414        };
415        st.iter()
416            .map(|x| format!(r#"<{p}:status s="{}"/>"#, escape(x)))
417            .collect()
418    };
419    Ok(match action["type"].as_str().unwrap_or_default() {
420        "epp_check_result" => {
421            ensure!(command == "check", "epp_check_result answers check");
422            let mut cds = String::new();
423            for r in action["results"]
424                .as_array()
425                .context("results is an array")?
426            {
427                let name = s(r, "name").context("each result names its object")?;
428                let avail = r["available"]
429                    .as_bool()
430                    .context("each result says available")?;
431                let reason = if avail {
432                    String::new()
433                } else {
434                    format!(
435                        "<{p}:reason>{}</{p}:reason>",
436                        escape(s(r, "reason").unwrap_or("In use"))
437                    )
438                };
439                cds.push_str(&format!(
440                    r#"<{p}:cd><{p}:{key} avail="{}">{}</{p}:{key}>{reason}</{p}:cd>"#,
441                    if avail { 1 } else { 0 },
442                    escape(name)
443                ));
444            }
445            format!(r#"<{p}:chkData xmlns:{p}="{ns}">{cds}</{p}:chkData>"#)
446        }
447        "epp_info" => {
448            ensure!(command == "info", "epp_info answers info");
449            let o = &action["info"];
450            let id = s(o, key).context("the object names itself")?;
451            let mut body = format!("<{p}:{key}>{}</{p}:{key}>", escape(id));
452            body.push_str(&format!(
453                "<{p}:roid>{}</{p}:roid>",
454                escape(s(o, "roid").unwrap_or("1-NETGET"))
455            ));
456            body.push_str(&status(o));
457            match object {
458                "domain" => {
459                    body.push_str(&opt(&format!("{p}:registrant"), o, "registrant"));
460                    for c in o["contacts"].as_array().into_iter().flatten() {
461                        body.push_str(&format!(
462                            r#"<{p}:contact type="{}">{}</{p}:contact>"#,
463                            escape(s(c, "type").unwrap_or("admin")),
464                            escape(s(c, "id").unwrap_or_default())
465                        ));
466                    }
467                    let ns_list = list(o, "ns");
468                    if !ns_list.is_empty() {
469                        body.push_str(&format!(
470                            "<{p}:ns>{}</{p}:ns>",
471                            ns_list
472                                .iter()
473                                .map(|h| el(&format!("{p}:hostObj"), h))
474                                .collect::<String>()
475                        ));
476                    }
477                }
478                "host" => {
479                    for a in o["addrs"].as_array().into_iter().flatten() {
480                        body.push_str(&format!(
481                            r#"<{p}:addr ip="{}">{}</{p}:addr>"#,
482                            escape(s(a, "ip").unwrap_or("v4")),
483                            escape(s(a, "addr").unwrap_or_default())
484                        ));
485                    }
486                }
487                _ => {
488                    let streets: String = list(o, "street")
489                        .iter()
490                        .map(|x| el(&format!("{p}:street"), x))
491                        .collect();
492                    body.push_str(&format!(
493                        r#"<{p}:postalInfo type="loc">{}{}<{p}:addr>{streets}{}{}{}{}</{p}:addr></{p}:postalInfo>{}{}"#,
494                        opt(&format!("{p}:name"), o, "name"),
495                        opt(&format!("{p}:org"), o, "org"),
496                        opt(&format!("{p}:city"), o, "city"),
497                        opt(&format!("{p}:sp"), o, "sp"),
498                        opt(&format!("{p}:pc"), o, "pc"),
499                        opt(&format!("{p}:cc"), o, "cc"),
500                        opt(&format!("{p}:voice"), o, "voice"),
501                        opt(&format!("{p}:email"), o, "email"),
502                    ));
503                }
504            }
505            body.push_str(&format!(
506                "<{p}:clID>{}</{p}:clID>",
507                escape(s(o, "cl_id").unwrap_or("netget"))
508            ));
509            body.push_str(&opt(&format!("{p}:crID"), o, "cr_id"));
510            body.push_str(&opt(&format!("{p}:crDate"), o, "cr_date"));
511            body.push_str(&opt(&format!("{p}:upDate"), o, "up_date"));
512            if object == "domain" {
513                body.push_str(&opt(&format!("{p}:exDate"), o, "ex_date"));
514            }
515            if let Some(pw) = s(o, "auth_info") {
516                body.push_str(&format!(
517                    "<{p}:authInfo>{}</{p}:authInfo>",
518                    el(&format!("{p}:pw"), pw)
519                ));
520            }
521            format!(r#"<{p}:infData xmlns:{p}="{ns}">{body}</{p}:infData>"#)
522        }
523        "epp_created" => {
524            ensure!(command == "create", "epp_created answers create");
525            let id = s(action, key).context("epp_created names the object")?;
526            format!(
527                r#"<{p}:creData xmlns:{p}="{ns}"><{p}:{key}>{}</{p}:{key}>{}{}</{p}:creData>"#,
528                escape(id),
529                opt(&format!("{p}:crDate"), action, "cr_date"),
530                if object == "domain" {
531                    opt(&format!("{p}:exDate"), action, "ex_date")
532                } else {
533                    String::new()
534                }
535            )
536        }
537        "epp_renewed" => {
538            ensure!(command == "renew", "epp_renewed answers renew");
539            format!(
540                r#"<{p}:renData xmlns:{p}="{ns}"><{p}:name>{}</{p}:name>{}</{p}:renData>"#,
541                escape(s(action, "name").context("epp_renewed names the domain")?),
542                opt(&format!("{p}:exDate"), action, "ex_date")
543            )
544        }
545        "epp_transfer_status" => {
546            ensure!(
547                command == "transfer",
548                "epp_transfer_status answers transfer"
549            );
550            format!(
551                r#"<{p}:trnData xmlns:{p}="{ns}"><{p}:{key}>{}</{p}:{key}>{}{}{}{}{}{}</{p}:trnData>"#,
552                escape(s(action, key).context("epp_transfer_status names the object")?),
553                opt(&format!("{p}:trStatus"), action, "tr_status"),
554                opt(&format!("{p}:reID"), action, "re_id"),
555                opt(&format!("{p}:reDate"), action, "re_date"),
556                opt(&format!("{p}:acID"), action, "ac_id"),
557                opt(&format!("{p}:acDate"), action, "ac_date"),
558                if object == "domain" {
559                    opt(&format!("{p}:exDate"), action, "ex_date")
560                } else {
561                    String::new()
562                }
563            )
564        }
565        "epp_result" => String::new(),
566        other => bail!("{other} is not an EPP answer"),
567    })
568}
569
570/// A response frame read back by a client: code, message, reason, transaction ids and the
571/// resData flattened per object.
572pub fn read_response(root: &Node) -> Result<Value> {
573    ensure!(root.ns == EPP && root.name == "epp", "not an EPP document");
574    let inner = root.first().context("an empty epp element")?;
575    if inner.name == "greeting" {
576        let menu = inner.child("svcMenu");
577        return Ok(json!({
578            "greeting": true,
579            "sv_id": inner.text_of("svID"),
580            "sv_date": inner.text_of("svDate"),
581            "versions": menu.map(|m| m.texts_of("version")).unwrap_or_default(),
582            "langs": menu.map(|m| m.texts_of("lang")).unwrap_or_default(),
583            "obj_uris": menu.map(|m| m.texts_of("objURI")).unwrap_or_default(),
584            "ext_uris": menu.and_then(|m| m.child("svcExtension")).map(|e| e.texts_of("extURI")).unwrap_or_default(),
585        }));
586    }
587    ensure!(
588        inner.name == "response",
589        "neither a greeting nor a response"
590    );
591    let result = inner
592        .child("result")
593        .context("a response without a result")?;
594    let code: u16 = result
595        .attr("code")
596        .and_then(|c| c.parse().ok())
597        .context("a result without a code")?;
598    let reason = result
599        .child("extValue")
600        .and_then(|x| x.text_of("reason"))
601        .or_else(|| result.text_of("reason"));
602    let tr = inner.child("trID");
603    let mut data = Map::new();
604    if let Some(rd) = inner.child("resData").and_then(Node::first) {
605        data.insert("kind".into(), json!(rd.name));
606        data.insert("object".into(), json!(object_of(&rd.ns)));
607        match rd.name.as_str() {
608            "chkData" => {
609                let results: Vec<Value> = rd
610                    .all("cd")
611                    .map(|cd| {
612                        let n = cd.child("name").or_else(|| cd.child("id"));
613                        json!({
614                            "name": n.map(Node::text),
615                            "available": n.and_then(|n| n.attr("avail")).is_some_and(|a| a == "1" || a == "true"),
616                            "reason": cd.text_of("reason"),
617                        })
618                    })
619                    .collect();
620                data.insert("results".into(), json!(results));
621            }
622            _ => {
623                for c in &rd.children {
624                    let value = match c.name.as_str() {
625                        "status" => json!(c.attr("s")),
626                        "contact" => json!({"type": c.attr("type"), "id": c.text()}),
627                        "addr" if c.children.is_empty() => {
628                            json!({"ip": c.attr("ip"), "addr": c.text()})
629                        }
630                        "ns" => json!(c.texts_of("hostObj")),
631                        "authInfo" => json!(c.text_of("pw")),
632                        "postalInfo" => {
633                            let addr = c.child("addr");
634                            json!({"name": c.text_of("name"), "org": c.text_of("org"), "street": addr.map(|a| a.texts_of("street")), "city": addr.and_then(|a| a.text_of("city")), "cc": addr.and_then(|a| a.text_of("cc"))})
635                        }
636                        _ => json!(c.text()),
637                    };
638                    match c.name.as_str() {
639                        "status" | "contact" | "addr" => {
640                            let key = if c.name == "status" {
641                                "status"
642                            } else if c.name == "contact" {
643                                "contacts"
644                            } else {
645                                "addrs"
646                            };
647                            data.entry(key.to_owned())
648                                .or_insert_with(|| json!([]))
649                                .as_array_mut()
650                                .map(|a| a.push(value));
651                        }
652                        name => {
653                            data.insert(name.to_owned(), value);
654                        }
655                    }
656                }
657            }
658        }
659    }
660    Ok(json!({
661        "code": code,
662        "message": result.text_of("msg"),
663        "reason": reason,
664        "cl_trid": tr.and_then(|t| t.text_of("clTRID")),
665        "sv_trid": tr.and_then(|t| t.text_of("svTRID")),
666        "data": data,
667    }))
668}