1use 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
8pub 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
46pub 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#[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 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
182fn 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
257pub 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
403pub 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
570pub 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}