Skip to main content

nodejs/stdlib/
dns.rs

1//! Node `dns` module.
2//!
3//! `dns.lookup`/`dns.promises.lookup` use the platform resolver (`getaddrinfo`,
4//! via `std::net::ToSocketAddrs`) exactly like Node — they honour `/etc/hosts`.
5//! Every `resolve*`/`reverse` method is a real DNS query issued through
6//! `hickory_resolver`'s blocking `Resolver`. Because Node runs its DNS callbacks
7//! asynchronously, each query runs on a dedicated `std::thread`; when it finishes
8//! it posts an `IoTask` back to the main thread which builds the JS record values
9//! and fires the Node-style `(err, records)` callback (or settles the Promise).
10//! `incr_handle`/`decr_handle` bracket the async op so the event loop stays alive.
11//!
12//! A per-thread override list (`setServers`) and the default result order are
13//! module state; `dns.Resolver` instances carry their own `@@servers` list on the
14//! instance object so instance methods query independent nameservers.
15
16use super::arg_str;
17use crate::host::{with_host, JsObj};
18use fusevm::Value;
19use hickory_resolver::config::{NameServerConfigGroup, ResolverConfig, ResolverOpts};
20use hickory_resolver::error::{ResolveError, ResolveErrorKind};
21use hickory_resolver::proto::op::ResponseCode;
22use hickory_resolver::proto::rr::rdata::caa::{Property as CaaProperty, Value as CaaValue};
23use hickory_resolver::proto::rr::{Name, RData, RecordType};
24use hickory_resolver::Resolver;
25use indexmap::IndexMap;
26use std::cell::RefCell;
27use std::net::{IpAddr, ToSocketAddrs};
28
29pub const METHODS: &[&str] = &[
30    // getaddrinfo-based (platform resolver, honours /etc/hosts)
31    "lookup",
32    "lookupService",
33    // real DNS queries (callback form)
34    "resolve",
35    "resolve4",
36    "resolve6",
37    "resolveMx",
38    "resolveTxt",
39    "resolveCname",
40    "resolveNs",
41    "resolvePtr",
42    "resolveSrv",
43    "resolveSoa",
44    "resolveNaptr",
45    "resolveCaa",
46    "resolveTlsa",
47    "resolveAny",
48    "reverse",
49    // configuration
50    "getServers",
51    "setServers",
52    "getDefaultResultOrder",
53    "setDefaultResultOrder",
54    // `dns.promises.*` members, reachable through the `promises` nested object.
55    "promiseLookup",
56    "promiseLookupService",
57    "promiseResolve",
58    "promiseResolve4",
59    "promiseResolve6",
60    "promiseResolveMx",
61    "promiseResolveTxt",
62    "promiseResolveCname",
63    "promiseResolveNs",
64    "promiseResolvePtr",
65    "promiseResolveSrv",
66    "promiseResolveSoa",
67    "promiseResolveNaptr",
68    "promiseResolveCaa",
69    "promiseResolveTlsa",
70    "promiseResolveAny",
71    "promiseReverse",
72];
73
74/// Method surface of a `dns.Resolver` instance (wired by the parent via
75/// `instance_has_method("Resolver", …)`).
76pub const RESOLVER_METHODS: &[&str] = &[
77    "getServers",
78    "setServers",
79    "resolve",
80    "resolve4",
81    "resolve6",
82    "resolveMx",
83    "resolveTxt",
84    "resolveCname",
85    "resolveNs",
86    "resolvePtr",
87    "resolveSrv",
88    "resolveSoa",
89    "resolveNaptr",
90    "resolveCaa",
91    "resolveTlsa",
92    "resolveAny",
93    "reverse",
94    "cancel",
95    "setLocalAddress",
96];
97
98pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
99    Some(match method {
100        "lookup" => lookup(args),
101        "lookupService" => lookup_service(args),
102        "resolve" => resolve_cb(args),
103        "resolve4" => cb_query(Query::A, args),
104        "resolve6" => cb_query(Query::Aaaa, args),
105        "resolveMx" => cb_query(Query::Mx, args),
106        "resolveTxt" => cb_query(Query::Txt, args),
107        "resolveCname" => cb_query(Query::Cname, args),
108        "resolveNs" => cb_query(Query::Ns, args),
109        "resolvePtr" => cb_query(Query::Ptr, args),
110        "resolveSrv" => cb_query(Query::Srv, args),
111        "resolveSoa" => cb_query(Query::Soa, args),
112        "resolveNaptr" => cb_query(Query::Naptr, args),
113        "resolveCaa" => cb_query(Query::Caa, args),
114        "resolveTlsa" => cb_query(Query::Tlsa, args),
115        "resolveAny" => cb_query(Query::Any, args),
116        "reverse" => cb_query(Query::Reverse, args),
117        "getServers" => get_servers(),
118        "setServers" => set_servers(args),
119        "getDefaultResultOrder" => Ok(get_default_result_order()),
120        "setDefaultResultOrder" => set_default_result_order(args),
121        "promiseLookup" => promise_lookup(args),
122        "promiseLookupService" => promise_lookup_service(args),
123        "promiseResolve" => resolve_promise(args),
124        "promiseResolve4" => promise_query(Query::A, args),
125        "promiseResolve6" => promise_query(Query::Aaaa, args),
126        "promiseResolveMx" => promise_query(Query::Mx, args),
127        "promiseResolveTxt" => promise_query(Query::Txt, args),
128        "promiseResolveCname" => promise_query(Query::Cname, args),
129        "promiseResolveNs" => promise_query(Query::Ns, args),
130        "promiseResolvePtr" => promise_query(Query::Ptr, args),
131        "promiseResolveSrv" => promise_query(Query::Srv, args),
132        "promiseResolveSoa" => promise_query(Query::Soa, args),
133        "promiseResolveNaptr" => promise_query(Query::Naptr, args),
134        "promiseResolveCaa" => promise_query(Query::Caa, args),
135        "promiseResolveTlsa" => promise_query(Query::Tlsa, args),
136        "promiseResolveAny" => promise_query(Query::Any, args),
137        "promiseReverse" => promise_query(Query::Reverse, args),
138        _ => return None,
139    })
140}
141
142// ── module state (per-thread; node-js runs JS on one thread) ─────────────────
143
144struct DnsState {
145    /// `setServers` override; `None` = use the system resolver configuration.
146    servers: Option<Vec<String>>,
147    /// `getDefaultResultOrder` value (`"verbatim"` | `"ipv4first"`).
148    order: String,
149}
150
151thread_local! {
152    static STATE: RefCell<DnsState> =
153        RefCell::new(DnsState { servers: None, order: "verbatim".into() });
154}
155
156fn module_servers() -> Option<Vec<String>> {
157    STATE.with(|s| s.borrow().servers.clone())
158}
159
160// ── non-function exports ─────────────────────────────────────────────────────
161
162/// `dns.promises` nested object plus the numeric flags and string error codes.
163pub fn constant(name: &str) -> Option<Value> {
164    match name {
165        "promises" => Some(promises_object()),
166        "ADDRCONFIG" => Some(Value::Float(1024.0)),
167        "V4MAPPED" => Some(Value::Float(2048.0)),
168        "ALL" => Some(Value::Float(256.0)),
169        _ => code_constant(name).map(|c| with_host(|h| h.new_str(c))),
170    }
171}
172
173/// String error-code constants (`dns.NODATA === 'ENODATA'`, …).
174fn code_constant(name: &str) -> Option<&'static str> {
175    Some(match name {
176        "NODATA" => "ENODATA",
177        "FORMERR" => "EFORMERR",
178        "SERVFAIL" => "ESERVFAIL",
179        "NOTFOUND" => "ENOTFOUND",
180        "NOTIMP" => "ENOTIMP",
181        "REFUSED" => "EREFUSED",
182        "BADQUERY" => "EBADQUERY",
183        "BADNAME" => "EBADNAME",
184        "BADFAMILY" => "EBADFAMILY",
185        "BADRESP" => "EBADRESP",
186        "CONNREFUSED" => "ECONNREFUSED",
187        "TIMEOUT" => "ETIMEOUT",
188        "EOF" => "EOF",
189        "FILE" => "EFILE",
190        "NOMEM" => "ENOMEM",
191        "DESTRUCTION" => "EDESTRUCTION",
192        "BADSTR" => "EBADSTR",
193        "BADFLAGS" => "EBADFLAGS",
194        "NONAME" => "ENONAME",
195        "BADHINTS" => "EBADHINTS",
196        "NOTINITIALIZED" => "ENOTINITIALIZED",
197        "LOADIPHLPAPI" => "ELOADIPHLPAPI",
198        "ADDRGETNETWORKPARAMS" => "EADDRGETNETWORKPARAMS",
199        "CANCELLED" => "ECANCELLED",
200        _ => return None,
201    })
202}
203
204/// `dns.promises` member name → the `dns.*` builtin it routes to.
205const PROMISE_MEMBERS: &[(&str, &str)] = &[
206    ("lookup", "promiseLookup"),
207    ("lookupService", "promiseLookupService"),
208    ("resolve", "promiseResolve"),
209    ("resolve4", "promiseResolve4"),
210    ("resolve6", "promiseResolve6"),
211    ("resolveMx", "promiseResolveMx"),
212    ("resolveTxt", "promiseResolveTxt"),
213    ("resolveCname", "promiseResolveCname"),
214    ("resolveNs", "promiseResolveNs"),
215    ("resolvePtr", "promiseResolvePtr"),
216    ("resolveSrv", "promiseResolveSrv"),
217    ("resolveSoa", "promiseResolveSoa"),
218    ("resolveNaptr", "promiseResolveNaptr"),
219    ("resolveCaa", "promiseResolveCaa"),
220    ("resolveTlsa", "promiseResolveTlsa"),
221    ("resolveAny", "promiseResolveAny"),
222    ("reverse", "promiseReverse"),
223    ("getServers", "getServers"),
224    ("setServers", "setServers"),
225    ("getDefaultResultOrder", "getDefaultResultOrder"),
226    ("setDefaultResultOrder", "setDefaultResultOrder"),
227];
228
229/// Build the `dns.promises` namespace object: each member is a `Builtin`
230/// reference routed back through `call`.
231fn promises_object() -> Value {
232    with_host(|h| {
233        let mut m = IndexMap::new();
234        for (key, target) in PROMISE_MEMBERS {
235            let b = h.alloc(JsObj::Builtin(format!("dns.{target}")));
236            m.insert((*key).into(), b);
237        }
238        h.new_object(m)
239    })
240}
241
242// ── dns.lookup (getaddrinfo) ─────────────────────────────────────────────────
243
244/// `dns.lookup(hostname[, options|family], cb)` → `cb(null, address, family)`.
245fn lookup(args: &[Value]) -> Result<Value, String> {
246    let hostname = arg_str(args, 0);
247    let want = opt_family(args);
248    let Some(cb) = args.last().cloned() else {
249        return Ok(Value::Undef);
250    };
251    match pick_addr(&hostname, want) {
252        Some(ip) => {
253            let family = if ip.is_ipv4() { 4.0 } else { 6.0 };
254            let (err, address) = with_host(|h| (h.null(), h.new_str(ip.to_string())));
255            crate::host::invoke(&cb, vec![err, address, Value::Float(family)], None)?;
256        }
257        None => {
258            let err = dns_error("ENOTFOUND", "getaddrinfo", &hostname);
259            crate::host::invoke(&cb, vec![err], None)?;
260        }
261    }
262    Ok(Value::Undef)
263}
264
265/// `dns.promises.lookup(hostname)` → Promise of `{ address, family }`.
266fn promise_lookup(args: &[Value]) -> Result<Value, String> {
267    let hostname = arg_str(args, 0);
268    let want = opt_family(args);
269    let p = with_host(|h| h.new_promise());
270    let id = with_host(|h| h.promise_id(&p)).unwrap_or(0);
271    match pick_addr(&hostname, want) {
272        Some(ip) => {
273            let family = if ip.is_ipv4() { 4.0 } else { 6.0 };
274            let obj = with_host(|h| {
275                let mut m = IndexMap::new();
276                m.insert("address".into(), h.new_str(ip.to_string()));
277                m.insert("family".into(), Value::Float(family));
278                h.new_object(m)
279            });
280            crate::host::resolve_promise_val(id, obj);
281        }
282        None => {
283            crate::host::reject_promise_val(id, dns_error("ENOTFOUND", "getaddrinfo", &hostname))
284        }
285    }
286    Ok(p)
287}
288
289/// Resolve `hostname` through `getaddrinfo`, filtered by requested family.
290fn pick_addr(hostname: &str, want: Option<u8>) -> Option<IpAddr> {
291    (hostname, 0u16)
292        .to_socket_addrs()
293        .ok()?
294        .map(|sa| sa.ip())
295        .find(|ip| match want {
296            Some(4) => ip.is_ipv4(),
297            Some(6) => ip.is_ipv6(),
298            _ => true,
299        })
300}
301
302/// The optional middle argument of `lookup`: numeric family or `{ family }`.
303fn opt_family(args: &[Value]) -> Option<u8> {
304    if args.len() < 3 {
305        return None;
306    }
307    with_host(|h| match h.get(&args[1]) {
308        Some(JsObj::Object(p)) => p.get("family").map(|f| h.to_number(f) as u8),
309        _ => {
310            let n = h.to_number(&args[1]);
311            (n.is_finite() && n > 0.0).then_some(n as u8)
312        }
313    })
314}
315
316// ── lookupService (real reverse + service name) ──────────────────────────────
317
318/// `dns.lookupService(address, port, cb)` → `cb(null, hostname, service)`.
319fn lookup_service(args: &[Value]) -> Result<Value, String> {
320    let addr = arg_str(args, 0);
321    let port = super::arg_num(args, 1);
322    let Some(cb) = args.last().cloned() else {
323        return Ok(Value::Undef);
324    };
325    let servers = module_servers();
326    with_host(|h| h.incr_handle());
327    let tx = with_host(|h| h.io_sender());
328    std::thread::spawn(move || {
329        let result = run_lookup_service(&addr, port, servers.as_deref());
330        let label = addr;
331        let _ = tx.send(Box::new(move || {
332            with_host(|h| h.decr_handle());
333            let call_args = match result {
334                Ok((host, service)) => {
335                    let (n, hv, sv) =
336                        with_host(|h| (h.null(), h.new_str(host), h.new_str(service)));
337                    vec![n, hv, sv]
338                }
339                Err(code) => vec![dns_error(&code, "getnameinfo", &label)],
340            };
341            if let Err(e) = crate::host::invoke(&cb, call_args, None) {
342                eprintln!("{e}");
343            }
344            Ok(())
345        }));
346    });
347    Ok(Value::Undef)
348}
349
350/// `dns.promises.lookupService(address, port)` → Promise of `{ hostname, service }`.
351fn promise_lookup_service(args: &[Value]) -> Result<Value, String> {
352    let addr = arg_str(args, 0);
353    let port = super::arg_num(args, 1);
354    let servers = module_servers();
355    let p = with_host(|h| h.new_promise());
356    let id = with_host(|h| h.promise_id(&p)).unwrap_or(0);
357    with_host(|h| h.incr_handle());
358    let tx = with_host(|h| h.io_sender());
359    std::thread::spawn(move || {
360        let result = run_lookup_service(&addr, port, servers.as_deref());
361        let label = addr;
362        let _ = tx.send(Box::new(move || {
363            with_host(|h| h.decr_handle());
364            match result {
365                Ok((host, service)) => {
366                    let obj = with_host(|h| {
367                        let mut m = IndexMap::new();
368                        m.insert("hostname".into(), h.new_str(host));
369                        m.insert("service".into(), h.new_str(service));
370                        h.new_object(m)
371                    });
372                    crate::host::resolve_promise_val(id, obj);
373                }
374                Err(code) => {
375                    crate::host::reject_promise_val(id, dns_error(&code, "getnameinfo", &label))
376                }
377            }
378            Ok(())
379        }));
380    });
381    Ok(p)
382}
383
384fn run_lookup_service(
385    addr: &str,
386    port: f64,
387    servers: Option<&[String]>,
388) -> Result<(String, String), String> {
389    let ip: IpAddr = addr.parse().map_err(|_| "EINVAL".to_string())?;
390    let resolver = build_resolver(servers)?;
391    let lookup = resolver
392        .reverse_lookup(ip)
393        .map_err(|e| err_code(&e).to_string())?;
394    let host = lookup
395        .iter()
396        .next()
397        .map(|n| name_str(n))
398        .ok_or_else(|| "ENOTFOUND".to_string())?;
399    Ok((host, port_service(port as u16)))
400}
401
402/// Well-known IANA port → service names (`getservbyport` parity for common
403/// ports); unknown ports fall back to the numeric string.
404fn port_service(port: u16) -> String {
405    let name = match port {
406        20 => "ftp-data",
407        21 => "ftp",
408        22 => "ssh",
409        23 => "telnet",
410        25 => "smtp",
411        53 => "domain",
412        67 => "bootps",
413        68 => "bootpc",
414        69 => "tftp",
415        79 => "finger",
416        80 => "http",
417        110 => "pop3",
418        111 => "sunrpc",
419        119 => "nntp",
420        123 => "ntp",
421        143 => "imap",
422        161 => "snmp",
423        194 => "irc",
424        389 => "ldap",
425        443 => "https",
426        445 => "microsoft-ds",
427        465 => "submissions",
428        514 => "shell",
429        515 => "printer",
430        587 => "submission",
431        631 => "ipp",
432        636 => "ldaps",
433        993 => "imaps",
434        995 => "pop3s",
435        1194 => "openvpn",
436        1433 => "ms-sql-s",
437        3306 => "mysql",
438        3389 => "ms-wbt-server",
439        5432 => "postgresql",
440        5060 => "sip",
441        5061 => "sips",
442        6379 => "redis",
443        8080 => "http-alt",
444        8443 => "https-alt",
445        _ => return port.to_string(),
446    };
447    name.to_string()
448}
449
450// ── generic resolve dispatch ─────────────────────────────────────────────────
451
452#[derive(Clone, Copy)]
453enum Query {
454    A,
455    Aaaa,
456    Mx,
457    Txt,
458    Cname,
459    Ns,
460    Ptr,
461    Srv,
462    Soa,
463    Naptr,
464    Caa,
465    Tlsa,
466    Any,
467    Reverse,
468}
469
470impl Query {
471    /// The `syscall` string Node reports on the error object.
472    fn syscall(self) -> &'static str {
473        match self {
474            Query::A => "queryA",
475            Query::Aaaa => "queryAaaa",
476            Query::Mx => "queryMx",
477            Query::Txt => "queryTxt",
478            Query::Cname => "queryCname",
479            Query::Ns => "queryNs",
480            Query::Ptr => "queryPtr",
481            Query::Srv => "querySrv",
482            Query::Soa => "querySoa",
483            Query::Naptr => "queryNaptr",
484            Query::Caa => "queryCaa",
485            Query::Tlsa => "queryTlsa",
486            Query::Any => "queryAny",
487            Query::Reverse => "getHostByAddr",
488        }
489    }
490}
491
492fn query_of(rrtype: &str) -> Option<Query> {
493    Some(match rrtype {
494        "A" => Query::A,
495        "AAAA" => Query::Aaaa,
496        "MX" => Query::Mx,
497        "TXT" => Query::Txt,
498        "CNAME" => Query::Cname,
499        "NS" => Query::Ns,
500        "PTR" => Query::Ptr,
501        "SRV" => Query::Srv,
502        "SOA" => Query::Soa,
503        "NAPTR" => Query::Naptr,
504        "CAA" => Query::Caa,
505        "TLSA" => Query::Tlsa,
506        "ANY" => Query::Any,
507        _ => return None,
508    })
509}
510
511fn cb_query(q: Query, args: &[Value]) -> Result<Value, String> {
512    let name = arg_str(args, 0);
513    let Some(cb) = args.last().cloned() else {
514        return Ok(Value::Undef);
515    };
516    start_query(q, name, module_servers(), cb);
517    Ok(Value::Undef)
518}
519
520fn promise_query(q: Query, args: &[Value]) -> Result<Value, String> {
521    let name = arg_str(args, 0);
522    Ok(start_promise_query(q, name, module_servers()))
523}
524
525/// `dns.resolve(hostname[, rrtype], cb)`.
526fn resolve_cb(args: &[Value]) -> Result<Value, String> {
527    let name = arg_str(args, 0);
528    let Some(cb) = args.last().cloned() else {
529        return Ok(Value::Undef);
530    };
531    let rrtype = if args.len() > 2 {
532        arg_str(args, 1)
533    } else {
534        "A".into()
535    };
536    let q = query_of(&rrtype).unwrap_or(Query::A);
537    start_query(q, name, module_servers(), cb);
538    Ok(Value::Undef)
539}
540
541/// `dns.promises.resolve(hostname[, rrtype])`.
542fn resolve_promise(args: &[Value]) -> Result<Value, String> {
543    let name = arg_str(args, 0);
544    let rrtype = if args.len() > 1 {
545        arg_str(args, 1)
546    } else {
547        "A".into()
548    };
549    let q = query_of(&rrtype).unwrap_or(Query::A);
550    Ok(start_promise_query(q, name, module_servers()))
551}
552
553// ── async drivers (spawn thread, post IoTask back) ───────────────────────────
554
555fn start_query(q: Query, name: String, servers: Option<Vec<String>>, cb: Value) {
556    with_host(|h| h.incr_handle());
557    let tx = with_host(|h| h.io_sender());
558    std::thread::spawn(move || {
559        let res = run_query(q, &name, servers.as_deref());
560        let label = name;
561        let _ = tx.send(Box::new(move || {
562            with_host(|h| h.decr_handle());
563            let call_args = match res {
564                Ok(data) => {
565                    let val = dns_to_value(data);
566                    let n = with_host(|h| h.null());
567                    vec![n, val]
568                }
569                Err(code) => vec![dns_error(&code, q.syscall(), &label)],
570            };
571            if let Err(e) = crate::host::invoke(&cb, call_args, None) {
572                eprintln!("{e}");
573            }
574            Ok(())
575        }));
576    });
577}
578
579fn start_promise_query(q: Query, name: String, servers: Option<Vec<String>>) -> Value {
580    let p = with_host(|h| h.new_promise());
581    let id = with_host(|h| h.promise_id(&p)).unwrap_or(0);
582    with_host(|h| h.incr_handle());
583    let tx = with_host(|h| h.io_sender());
584    std::thread::spawn(move || {
585        let res = run_query(q, &name, servers.as_deref());
586        let label = name;
587        let _ = tx.send(Box::new(move || {
588            with_host(|h| h.decr_handle());
589            match res {
590                Ok(data) => crate::host::resolve_promise_val(id, dns_to_value(data)),
591                Err(code) => {
592                    crate::host::reject_promise_val(id, dns_error(&code, q.syscall(), &label))
593                }
594            }
595            Ok(())
596        }));
597    });
598    p
599}
600
601/// Callback-vs-Promise dispatch used by `Resolver` instance methods.
602fn run_or_promise(
603    q: Query,
604    name: String,
605    servers: Option<Vec<String>>,
606    cb: Option<Value>,
607    promises: bool,
608) -> Value {
609    if promises {
610        start_promise_query(q, name, servers)
611    } else if let Some(cb) = cb {
612        start_query(q, name, servers, cb);
613        Value::Undef
614    } else {
615        Value::Undef
616    }
617}
618
619// ── the blocking query itself (runs on a worker thread) ──────────────────────
620
621/// Plain-data (Send) record set produced by a worker thread; converted to JS
622/// values on the main thread by `dns_to_value`.
623enum DnsResult {
624    Strings(Vec<String>),
625    Mx(Vec<(u16, String)>),
626    Txt(Vec<Vec<String>>),
627    Srv(Vec<SrvRec>),
628    Soa(SoaRec),
629    Naptr(Vec<NaptrRec>),
630    Caa(Vec<CaaRec>),
631    Tlsa(Vec<TlsaRec>),
632    Any(Vec<AnyRec>),
633}
634
635struct SoaRec {
636    nsname: String,
637    hostmaster: String,
638    serial: u32,
639    refresh: i32,
640    retry: i32,
641    expire: i32,
642    minttl: u32,
643}
644
645struct SrvRec {
646    priority: u16,
647    weight: u16,
648    port: u16,
649    name: String,
650}
651
652struct NaptrRec {
653    flags: String,
654    service: String,
655    regexp: String,
656    replacement: String,
657    order: u16,
658    preference: u16,
659}
660
661struct CaaRec {
662    critical: u16,
663    tag: String,
664    value: String,
665}
666
667struct TlsaRec {
668    cert_usage: u8,
669    selector: u8,
670    matching: u8,
671    data: Vec<u8>,
672}
673
674enum AnyRec {
675    A(String, u32),
676    Aaaa(String, u32),
677    Cname(String),
678    Mx(u16, String),
679    Ns(String),
680    Ptr(String),
681    Txt(Vec<String>),
682    Srv(SrvRec),
683    Soa(SoaRec),
684    Naptr(NaptrRec),
685    Caa(CaaRec),
686}
687
688fn run_query(q: Query, name: &str, servers: Option<&[String]>) -> Result<DnsResult, String> {
689    let r = build_resolver(servers)?;
690    let ec = |e: ResolveError| err_code(&e).to_string();
691    match q {
692        Query::A => {
693            let l = r.ipv4_lookup(name).map_err(ec)?;
694            Ok(DnsResult::Strings(
695                l.iter().map(|a| a.to_string()).collect(),
696            ))
697        }
698        Query::Aaaa => {
699            let l = r.ipv6_lookup(name).map_err(ec)?;
700            Ok(DnsResult::Strings(
701                l.iter().map(|a| a.to_string()).collect(),
702            ))
703        }
704        Query::Mx => {
705            let l = r.mx_lookup(name).map_err(ec)?;
706            Ok(DnsResult::Mx(
707                l.iter()
708                    .map(|m| (m.preference(), name_str(m.exchange())))
709                    .collect(),
710            ))
711        }
712        Query::Txt => {
713            let l = r.txt_lookup(name).map_err(ec)?;
714            let entries = l
715                .iter()
716                .map(|t| {
717                    t.txt_data()
718                        .iter()
719                        .map(|b| String::from_utf8_lossy(b).into_owned())
720                        .collect()
721                })
722                .collect();
723            Ok(DnsResult::Txt(entries))
724        }
725        Query::Cname => {
726            let l = r.lookup(name, RecordType::CNAME).map_err(ec)?;
727            let names = l
728                .iter()
729                .filter_map(|d| match d {
730                    RData::CNAME(c) => Some(name_str(c)),
731                    _ => None,
732                })
733                .collect();
734            Ok(DnsResult::Strings(names))
735        }
736        Query::Ns => {
737            let l = r.ns_lookup(name).map_err(ec)?;
738            Ok(DnsResult::Strings(l.iter().map(|n| name_str(n)).collect()))
739        }
740        Query::Ptr => {
741            let l = r.lookup(name, RecordType::PTR).map_err(ec)?;
742            let names = l
743                .iter()
744                .filter_map(|d| match d {
745                    RData::PTR(p) => Some(name_str(p)),
746                    _ => None,
747                })
748                .collect();
749            Ok(DnsResult::Strings(names))
750        }
751        Query::Srv => {
752            let l = r.srv_lookup(name).map_err(ec)?;
753            Ok(DnsResult::Srv(l.iter().map(srv_rec).collect()))
754        }
755        Query::Soa => {
756            let l = r.soa_lookup(name).map_err(ec)?;
757            let soa = l.iter().next().ok_or_else(|| "ENODATA".to_string())?;
758            Ok(DnsResult::Soa(soa_rec(soa)))
759        }
760        Query::Naptr => {
761            let l = r.lookup(name, RecordType::NAPTR).map_err(ec)?;
762            let recs = l
763                .iter()
764                .filter_map(|d| match d {
765                    RData::NAPTR(n) => Some(naptr_rec(n)),
766                    _ => None,
767                })
768                .collect();
769            Ok(DnsResult::Naptr(recs))
770        }
771        Query::Caa => {
772            let l = r.lookup(name, RecordType::CAA).map_err(ec)?;
773            let recs = l
774                .iter()
775                .filter_map(|d| match d {
776                    RData::CAA(c) => Some(caa_rec(c)),
777                    _ => None,
778                })
779                .collect();
780            Ok(DnsResult::Caa(recs))
781        }
782        Query::Tlsa => {
783            let l = r.tlsa_lookup(name).map_err(ec)?;
784            let recs = l
785                .iter()
786                .map(|t| TlsaRec {
787                    cert_usage: t.cert_usage().into(),
788                    selector: t.selector().into(),
789                    matching: t.matching().into(),
790                    data: t.cert_data().to_vec(),
791                })
792                .collect();
793            Ok(DnsResult::Tlsa(recs))
794        }
795        Query::Any => {
796            let l = r.lookup(name, RecordType::ANY).map_err(ec)?;
797            let recs = l.records().iter().filter_map(any_rec).collect();
798            Ok(DnsResult::Any(recs))
799        }
800        Query::Reverse => {
801            let ip: IpAddr = name.parse().map_err(|_| "EINVAL".to_string())?;
802            let l = r.reverse_lookup(ip).map_err(ec)?;
803            Ok(DnsResult::Strings(l.iter().map(|n| name_str(n)).collect()))
804        }
805    }
806}
807
808// ── record → plain-data extractors (worker thread) ───────────────────────────
809
810fn name_str(n: &Name) -> String {
811    n.to_ascii().trim_end_matches('.').to_string()
812}
813
814fn srv_rec(s: &hickory_resolver::proto::rr::rdata::SRV) -> SrvRec {
815    SrvRec {
816        priority: s.priority(),
817        weight: s.weight(),
818        port: s.port(),
819        name: name_str(s.target()),
820    }
821}
822
823fn soa_rec(s: &hickory_resolver::proto::rr::rdata::SOA) -> SoaRec {
824    SoaRec {
825        nsname: name_str(s.mname()),
826        hostmaster: name_str(s.rname()),
827        serial: s.serial(),
828        refresh: s.refresh(),
829        retry: s.retry(),
830        expire: s.expire(),
831        minttl: s.minimum(),
832    }
833}
834
835fn naptr_rec(n: &hickory_resolver::proto::rr::rdata::NAPTR) -> NaptrRec {
836    NaptrRec {
837        flags: String::from_utf8_lossy(n.flags()).into_owned(),
838        service: String::from_utf8_lossy(n.services()).into_owned(),
839        regexp: String::from_utf8_lossy(n.regexp()).into_owned(),
840        replacement: name_str(n.replacement()),
841        order: n.order(),
842        preference: n.preference(),
843    }
844}
845
846fn caa_rec(c: &hickory_resolver::proto::rr::rdata::CAA) -> CaaRec {
847    CaaRec {
848        critical: if c.issuer_critical() { 128 } else { 0 },
849        tag: caa_tag(c.tag()),
850        value: caa_value(c.value()),
851    }
852}
853
854fn caa_tag(p: &CaaProperty) -> String {
855    match p {
856        CaaProperty::Issue => "issue".into(),
857        CaaProperty::IssueWild => "issuewild".into(),
858        CaaProperty::Iodef => "iodef".into(),
859        CaaProperty::Unknown(s) => s.clone(),
860    }
861}
862
863fn caa_value(v: &CaaValue) -> String {
864    match v {
865        CaaValue::Issuer(name, kvs) => {
866            let mut s = name.as_ref().map(name_str).unwrap_or_default();
867            for kv in kvs {
868                s.push_str("; ");
869                s.push_str(kv.key());
870                s.push('=');
871                s.push_str(kv.value());
872            }
873            s
874        }
875        CaaValue::Url(u) => u.to_string(),
876        CaaValue::Unknown(b) => String::from_utf8_lossy(b).into_owned(),
877    }
878}
879
880/// Map one record from an `ANY` response to its `AnyRec`, skipping unmodelled
881/// record types.
882fn any_rec(rec: &hickory_resolver::proto::rr::Record) -> Option<AnyRec> {
883    let ttl = rec.ttl();
884    Some(match rec.data()? {
885        RData::A(a) => AnyRec::A(a.to_string(), ttl),
886        RData::AAAA(a) => AnyRec::Aaaa(a.to_string(), ttl),
887        RData::CNAME(c) => AnyRec::Cname(name_str(c)),
888        RData::MX(m) => AnyRec::Mx(m.preference(), name_str(m.exchange())),
889        RData::NS(n) => AnyRec::Ns(name_str(n)),
890        RData::PTR(p) => AnyRec::Ptr(name_str(p)),
891        RData::TXT(t) => AnyRec::Txt(
892            t.txt_data()
893                .iter()
894                .map(|b| String::from_utf8_lossy(b).into_owned())
895                .collect(),
896        ),
897        RData::SRV(s) => AnyRec::Srv(srv_rec(s)),
898        RData::SOA(s) => AnyRec::Soa(soa_rec(s)),
899        RData::NAPTR(n) => AnyRec::Naptr(naptr_rec(n)),
900        RData::CAA(c) => AnyRec::Caa(caa_rec(c)),
901        _ => return None,
902    })
903}
904
905// ── DnsResult → JS Value (main thread) ───────────────────────────────────────
906
907fn dns_to_value(res: DnsResult) -> Value {
908    match res {
909        DnsResult::Strings(v) => with_host(|h| {
910            let items: Vec<Value> = v.into_iter().map(|s| h.new_str(s)).collect();
911            h.new_array(items)
912        }),
913        DnsResult::Mx(v) => with_host(|h| {
914            let items: Vec<Value> = v
915                .into_iter()
916                .map(|(pri, ex)| {
917                    let mut m = IndexMap::new();
918                    m.insert("exchange".into(), h.new_str(ex));
919                    m.insert("priority".into(), Value::Float(pri as f64));
920                    m.insert("type".into(), h.new_str("MX"));
921                    h.new_object(m)
922                })
923                .collect();
924            h.new_array(items)
925        }),
926        DnsResult::Txt(v) => with_host(|h| {
927            let items: Vec<Value> = v
928                .into_iter()
929                .map(|chunks| {
930                    let inner: Vec<Value> = chunks.into_iter().map(|s| h.new_str(s)).collect();
931                    h.new_array(inner)
932                })
933                .collect();
934            h.new_array(items)
935        }),
936        DnsResult::Srv(v) => with_host(|h| {
937            let items: Vec<Value> = v.iter().map(|s| srv_obj(h, s)).collect();
938            h.new_array(items)
939        }),
940        DnsResult::Soa(s) => with_host(|h| soa_obj(h, &s)),
941        DnsResult::Naptr(v) => with_host(|h| {
942            let items: Vec<Value> = v.iter().map(|n| naptr_obj(h, n)).collect();
943            h.new_array(items)
944        }),
945        DnsResult::Caa(v) => with_host(|h| {
946            let items: Vec<Value> = v.iter().map(|c| caa_obj(h, c)).collect();
947            h.new_array(items)
948        }),
949        DnsResult::Tlsa(v) => {
950            // Buffer values must be built outside the object's `with_host`.
951            let mut items = Vec::with_capacity(v.len());
952            for t in v {
953                let data = super::buffer::from_bytes(&t.data);
954                let obj = with_host(|h| {
955                    let mut m = IndexMap::new();
956                    m.insert("certUsage".into(), Value::Float(t.cert_usage as f64));
957                    m.insert("selector".into(), Value::Float(t.selector as f64));
958                    m.insert("match".into(), Value::Float(t.matching as f64));
959                    m.insert("data".into(), data);
960                    h.new_object(m)
961                });
962                items.push(obj);
963            }
964            with_host(|h| h.new_array(items))
965        }
966        DnsResult::Any(v) => with_host(|h| {
967            let items: Vec<Value> = v.into_iter().map(|rec| any_obj(h, rec)).collect();
968            h.new_array(items)
969        }),
970    }
971}
972
973fn srv_obj(h: &mut crate::host::JsHost, s: &SrvRec) -> Value {
974    let mut m = IndexMap::new();
975    m.insert("name".into(), h.new_str(s.name.clone()));
976    m.insert("port".into(), Value::Float(s.port as f64));
977    m.insert("priority".into(), Value::Float(s.priority as f64));
978    m.insert("weight".into(), Value::Float(s.weight as f64));
979    m.insert("type".into(), h.new_str("SRV"));
980    h.new_object(m)
981}
982
983fn soa_obj(h: &mut crate::host::JsHost, s: &SoaRec) -> Value {
984    let mut m = IndexMap::new();
985    m.insert("nsname".into(), h.new_str(s.nsname.clone()));
986    m.insert("hostmaster".into(), h.new_str(s.hostmaster.clone()));
987    m.insert("serial".into(), Value::Float(s.serial as f64));
988    m.insert("refresh".into(), Value::Float(s.refresh as f64));
989    m.insert("retry".into(), Value::Float(s.retry as f64));
990    m.insert("expire".into(), Value::Float(s.expire as f64));
991    m.insert("minttl".into(), Value::Float(s.minttl as f64));
992    h.new_object(m)
993}
994
995fn naptr_obj(h: &mut crate::host::JsHost, n: &NaptrRec) -> Value {
996    let mut m = IndexMap::new();
997    m.insert("flags".into(), h.new_str(n.flags.clone()));
998    m.insert("service".into(), h.new_str(n.service.clone()));
999    m.insert("regexp".into(), h.new_str(n.regexp.clone()));
1000    m.insert("replacement".into(), h.new_str(n.replacement.clone()));
1001    m.insert("order".into(), Value::Float(n.order as f64));
1002    m.insert("preference".into(), Value::Float(n.preference as f64));
1003    h.new_object(m)
1004}
1005
1006fn caa_obj(h: &mut crate::host::JsHost, c: &CaaRec) -> Value {
1007    let mut m = IndexMap::new();
1008    m.insert("critical".into(), Value::Float(c.critical as f64));
1009    m.insert("type".into(), h.new_str("CAA"));
1010    m.insert(c.tag.clone(), h.new_str(c.value.clone()));
1011    h.new_object(m)
1012}
1013
1014/// Build one `resolveAny` record object (`{ type, … }`) per Node's shapes.
1015fn any_obj(h: &mut crate::host::JsHost, rec: AnyRec) -> Value {
1016    let mut m = IndexMap::new();
1017    match rec {
1018        AnyRec::A(addr, ttl) => {
1019            m.insert("type".into(), h.new_str("A"));
1020            m.insert("address".into(), h.new_str(addr));
1021            m.insert("ttl".into(), Value::Float(ttl as f64));
1022        }
1023        AnyRec::Aaaa(addr, ttl) => {
1024            m.insert("type".into(), h.new_str("AAAA"));
1025            m.insert("address".into(), h.new_str(addr));
1026            m.insert("ttl".into(), Value::Float(ttl as f64));
1027        }
1028        AnyRec::Cname(v) => {
1029            m.insert("type".into(), h.new_str("CNAME"));
1030            m.insert("value".into(), h.new_str(v));
1031        }
1032        AnyRec::Mx(pri, ex) => {
1033            m.insert("type".into(), h.new_str("MX"));
1034            m.insert("exchange".into(), h.new_str(ex));
1035            m.insert("priority".into(), Value::Float(pri as f64));
1036        }
1037        AnyRec::Ns(v) => {
1038            m.insert("type".into(), h.new_str("NS"));
1039            m.insert("value".into(), h.new_str(v));
1040        }
1041        AnyRec::Ptr(v) => {
1042            m.insert("type".into(), h.new_str("PTR"));
1043            m.insert("value".into(), h.new_str(v));
1044        }
1045        AnyRec::Txt(chunks) => {
1046            let entries: Vec<Value> = chunks.into_iter().map(|s| h.new_str(s)).collect();
1047            let arr = h.new_array(entries);
1048            m.insert("type".into(), h.new_str("TXT"));
1049            m.insert("entries".into(), arr);
1050        }
1051        AnyRec::Srv(s) => {
1052            m.insert("type".into(), h.new_str("SRV"));
1053            m.insert("priority".into(), Value::Float(s.priority as f64));
1054            m.insert("weight".into(), Value::Float(s.weight as f64));
1055            m.insert("port".into(), Value::Float(s.port as f64));
1056            m.insert("name".into(), h.new_str(s.name));
1057        }
1058        AnyRec::Soa(s) => return soa_with_type(h, &s),
1059        AnyRec::Naptr(n) => {
1060            m.insert("type".into(), h.new_str("NAPTR"));
1061            m.insert("flags".into(), h.new_str(n.flags));
1062            m.insert("service".into(), h.new_str(n.service));
1063            m.insert("regexp".into(), h.new_str(n.regexp));
1064            m.insert("replacement".into(), h.new_str(n.replacement));
1065            m.insert("order".into(), Value::Float(n.order as f64));
1066            m.insert("preference".into(), Value::Float(n.preference as f64));
1067        }
1068        AnyRec::Caa(c) => {
1069            m.insert("type".into(), h.new_str("CAA"));
1070            m.insert("critical".into(), Value::Float(c.critical as f64));
1071            m.insert(c.tag, h.new_str(c.value));
1072        }
1073    }
1074    h.new_object(m)
1075}
1076
1077fn soa_with_type(h: &mut crate::host::JsHost, s: &SoaRec) -> Value {
1078    let mut m = IndexMap::new();
1079    m.insert("type".into(), h.new_str("SOA"));
1080    m.insert("nsname".into(), h.new_str(s.nsname.clone()));
1081    m.insert("hostmaster".into(), h.new_str(s.hostmaster.clone()));
1082    m.insert("serial".into(), Value::Float(s.serial as f64));
1083    m.insert("refresh".into(), Value::Float(s.refresh as f64));
1084    m.insert("retry".into(), Value::Float(s.retry as f64));
1085    m.insert("expire".into(), Value::Float(s.expire as f64));
1086    m.insert("minttl".into(), Value::Float(s.minttl as f64));
1087    h.new_object(m)
1088}
1089
1090// ── resolver construction / error mapping ────────────────────────────────────
1091
1092/// Build a blocking `Resolver`. With an explicit server list, use those
1093/// nameservers (UDP/TCP, port 53); otherwise the system configuration.
1094fn build_resolver(servers: Option<&[String]>) -> Result<Resolver, String> {
1095    match servers {
1096        Some(list) if !list.is_empty() => {
1097            let ips: Vec<IpAddr> = list.iter().filter_map(|s| parse_ip(s)).collect();
1098            if ips.is_empty() {
1099                return Err("EBADFAMILY".into());
1100            }
1101            let group = NameServerConfigGroup::from_ips_clear(&ips, 53, true);
1102            let cfg = ResolverConfig::from_parts(None, vec![], group);
1103            Resolver::new(cfg, ResolverOpts::default()).map_err(|e| e.to_string())
1104        }
1105        _ => Resolver::from_system_conf()
1106            .or_else(|_| Resolver::new(ResolverConfig::default(), ResolverOpts::default()))
1107            .map_err(|e| e.to_string()),
1108    }
1109}
1110
1111/// Parse a bare IP or an `ip:port` / `[ipv6]:port` string to its `IpAddr`.
1112fn parse_ip(s: &str) -> Option<IpAddr> {
1113    s.parse::<IpAddr>()
1114        .ok()
1115        .or_else(|| s.parse::<std::net::SocketAddr>().ok().map(|sa| sa.ip()))
1116}
1117
1118/// The DNS system nameservers (deduplicated), as IP strings.
1119fn system_servers() -> Vec<String> {
1120    match hickory_resolver::system_conf::read_system_conf() {
1121        Ok((cfg, _)) => {
1122            let mut out: Vec<String> = Vec::new();
1123            for ns in cfg.name_servers() {
1124                let ip = ns.socket_addr.ip().to_string();
1125                if !out.contains(&ip) {
1126                    out.push(ip);
1127                }
1128            }
1129            out
1130        }
1131        Err(_) => Vec::new(),
1132    }
1133}
1134
1135/// Map a `hickory` resolve error to the Node/c-ares error code string.
1136fn err_code(e: &ResolveError) -> &'static str {
1137    match e.kind() {
1138        ResolveErrorKind::NoRecordsFound { response_code, .. } => {
1139            if *response_code == ResponseCode::NXDomain {
1140                "ENOTFOUND"
1141            } else {
1142                "ENODATA"
1143            }
1144        }
1145        ResolveErrorKind::NoConnections => "ESERVFAIL",
1146        ResolveErrorKind::Timeout => "ETIMEOUT",
1147        _ => "ESERVFAIL",
1148    }
1149}
1150
1151/// Build an `Error` carrying `code`/`syscall`/`hostname`, message
1152/// `"<syscall> <code> <hostname>"` (c-ares parity).
1153fn dns_error(code: &str, syscall: &str, host: &str) -> Value {
1154    let msg = format!("Error: {syscall} {code} {host}");
1155    with_host(|h| {
1156        let e = crate::builtins::synth_error(h, &msg);
1157        let cv = h.new_str(code.to_string());
1158        let sv = h.new_str(syscall.to_string());
1159        let hv = h.new_str(host.to_string());
1160        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
1161            p.insert("code".into(), cv);
1162            p.insert("syscall".into(), sv);
1163            p.insert("hostname".into(), hv);
1164        }
1165        e
1166    })
1167}
1168
1169// ── configuration methods ────────────────────────────────────────────────────
1170
1171fn get_servers() -> Result<Value, String> {
1172    let list = module_servers().unwrap_or_else(system_servers);
1173    Ok(with_host(|h| {
1174        let items: Vec<Value> = list.into_iter().map(|s| h.new_str(s)).collect();
1175        h.new_array(items)
1176    }))
1177}
1178
1179fn set_servers(args: &[Value]) -> Result<Value, String> {
1180    let list = servers_from_arg(args.first());
1181    STATE.with(|s| s.borrow_mut().servers = Some(list));
1182    Ok(Value::Undef)
1183}
1184
1185fn servers_from_arg(v: Option<&Value>) -> Vec<String> {
1186    with_host(|h| match v.and_then(|v| h.get(v)) {
1187        Some(JsObj::Array(items)) => items.iter().map(|x| h.str_of(x)).collect(),
1188        _ => Vec::new(),
1189    })
1190}
1191
1192fn get_default_result_order() -> Value {
1193    let order = STATE.with(|s| s.borrow().order.clone());
1194    with_host(|h| h.new_str(order))
1195}
1196
1197fn set_default_result_order(args: &[Value]) -> Result<Value, String> {
1198    let order = arg_str(args, 0);
1199    if order == "ipv4first" || order == "verbatim" || order == "ipv6first" {
1200        STATE.with(|s| s.borrow_mut().order = order);
1201    }
1202    Ok(Value::Undef)
1203}
1204
1205// ── dns.Resolver instance ────────────────────────────────────────────────────
1206
1207/// `new dns.Resolver()` → callback-style resolver instance.
1208pub fn construct_resolver(_args: &[Value]) -> Value {
1209    make_resolver_obj(false)
1210}
1211
1212/// `new dns.promises.Resolver()` → Promise-returning resolver instance.
1213pub fn construct_resolver_promises(_args: &[Value]) -> Value {
1214    make_resolver_obj(true)
1215}
1216
1217fn make_resolver_obj(promises: bool) -> Value {
1218    let empty = with_host(|h| h.new_array(Vec::new()));
1219    let tag = with_host(|h| h.new_str("Resolver"));
1220    with_host(|h| {
1221        let mut m = IndexMap::new();
1222        m.insert("@@native".into(), tag);
1223        m.insert("@@servers".into(), empty);
1224        if promises {
1225            m.insert("@@promises".into(), Value::Bool(true));
1226        }
1227        h.new_object(m)
1228    })
1229}
1230
1231/// Dispatch an instance method on a `dns.Resolver` receiver.
1232pub fn resolver_instance_call(
1233    recv: &Value,
1234    method: &str,
1235    args: Vec<Value>,
1236) -> Result<Value, String> {
1237    let promises = recv_promises(recv);
1238    let servers = recv_servers(recv);
1239
1240    match method {
1241        "getServers" => {
1242            let list = servers.clone().unwrap_or_else(system_servers);
1243            return Ok(with_host(|h| {
1244                let items: Vec<Value> = list.into_iter().map(|s| h.new_str(s)).collect();
1245                h.new_array(items)
1246            }));
1247        }
1248        "setServers" => {
1249            let list = servers_from_arg(args.first());
1250            set_recv_servers(recv, &list);
1251            return Ok(Value::Undef);
1252        }
1253        "cancel" | "setLocalAddress" => return Ok(Value::Undef),
1254        _ => {}
1255    }
1256
1257    if method == "resolve" {
1258        let name = super::arg_str(&args, 0);
1259        let has_rrtype = if promises {
1260            args.len() > 1
1261        } else {
1262            args.len() > 2
1263        };
1264        let rrtype = if has_rrtype {
1265            super::arg_str(&args, 1)
1266        } else {
1267            "A".to_string()
1268        };
1269        let q = query_of(&rrtype).unwrap_or(Query::A);
1270        let cb = if promises { None } else { args.last().cloned() };
1271        return Ok(run_or_promise(q, name, servers, cb, promises));
1272    }
1273
1274    if let Some(q) = query_for_method(method) {
1275        let name = super::arg_str(&args, 0);
1276        let cb = if promises { None } else { args.last().cloned() };
1277        return Ok(run_or_promise(q, name, servers, cb, promises));
1278    }
1279
1280    Err(format!("TypeError: resolver.{method} is not a function"))
1281}
1282
1283fn query_for_method(method: &str) -> Option<Query> {
1284    Some(match method {
1285        "resolve4" => Query::A,
1286        "resolve6" => Query::Aaaa,
1287        "resolveMx" => Query::Mx,
1288        "resolveTxt" => Query::Txt,
1289        "resolveCname" => Query::Cname,
1290        "resolveNs" => Query::Ns,
1291        "resolvePtr" => Query::Ptr,
1292        "resolveSrv" => Query::Srv,
1293        "resolveSoa" => Query::Soa,
1294        "resolveNaptr" => Query::Naptr,
1295        "resolveCaa" => Query::Caa,
1296        "resolveTlsa" => Query::Tlsa,
1297        "resolveAny" => Query::Any,
1298        "reverse" => Query::Reverse,
1299        _ => return None,
1300    })
1301}
1302
1303fn recv_promises(recv: &Value) -> bool {
1304    with_host(|h| {
1305        matches!(
1306            h.get(recv),
1307            Some(JsObj::Object(p)) if matches!(p.get("@@promises"), Some(Value::Bool(true)))
1308        )
1309    })
1310}
1311
1312fn recv_servers(recv: &Value) -> Option<Vec<String>> {
1313    with_host(|h| {
1314        let JsObj::Object(p) = h.get(recv)? else {
1315            return None;
1316        };
1317        let arr = p.get("@@servers")?;
1318        let JsObj::Array(items) = h.get(arr)? else {
1319            return None;
1320        };
1321        let list: Vec<String> = items.iter().map(|x| h.str_of(x)).collect();
1322        if list.is_empty() {
1323            None
1324        } else {
1325            Some(list)
1326        }
1327    })
1328}
1329
1330fn set_recv_servers(recv: &Value, list: &[String]) {
1331    let arr = with_host(|h| {
1332        let items: Vec<Value> = list.iter().map(|s| h.new_str(s.clone())).collect();
1333        h.new_array(items)
1334    });
1335    with_host(|h| {
1336        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1337            p.insert("@@servers".into(), arr);
1338        }
1339    });
1340}