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