diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index e98f68ea38..659f1aa377 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -4674,6 +4674,22 @@ declare namespace Deno { * and the promise returned will be rejected with an AbortError. */ signal?: AbortSignal; + /** */ + ttl?: boolean; + } + + export interface RecordWithTtl { + /** */ + data: + | string + | CaaRecord + | MxRecord + | NaptrRecord + | SoaRecord + | SrvRecord + | string[]; + /** */ + ttl: number; } /** If {@linkcode Deno.resolveDns} is called with `"CAA"` record type @@ -4748,6 +4764,37 @@ declare namespace Deno { target: string; } + /** + * Performs DNS resolution against the given query, returning resolved + * records with ttl values. + * + * Fails in the cases such as: + * + * - the query is in invalid format. + * - the options have an invalid parameter. For example `nameServer.port` is + * beyond the range of 16-bit unsigned integer. + * - the request timed out. + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A", { ttl: true }); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 53 }, + * ttl: true, + * }); + * ``` + * + * Requires `allow-net` permission. + * + * @tags allow-net + * @category Network + */ + export function resolveDns( + query: string, + recordType: RecordType, + options?: { ttl: true } & ResolveDnsOptions, + ): Promise; + /** * Performs DNS resolution against the given query, returning resolved * records. @@ -4994,6 +5041,7 @@ declare namespace Deno { | SoaRecord[] | SrvRecord[] | string[][] + | RecordWithTtl[] >; /** diff --git a/ext/net/ops.rs b/ext/net/ops.rs index 768dd33135..ba6c256fe7 100644 --- a/ext/net/ops.rs +++ b/ext/net/ops.rs @@ -561,7 +561,7 @@ where #[derive(Serialize, Eq, PartialEq, Debug)] #[serde(untagged)] -pub enum DnsReturnRecord { +pub enum DnsRecordData { A(String), Aaaa(String), Aname(String), @@ -603,6 +603,20 @@ pub enum DnsReturnRecord { Txt(Vec), } +#[derive(Serialize, Eq, PartialEq, Debug)] +#[serde()] +pub struct DnsRecordWithTtl { + pub data: DnsRecordData, + pub ttl: u32, +} + +#[derive(Serialize, Eq, PartialEq, Debug)] +#[serde(untagged)] +pub enum DnsReturnRecord { + WithoutTtl(DnsRecordData), + WithTtl(DnsRecordWithTtl), +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveAddrArgs { @@ -616,6 +630,7 @@ pub struct ResolveAddrArgs { #[serde(rename_all = "camelCase")] pub struct ResolveDnsOption { name_server: Option, + ttl: Option, } fn default_port() -> u16 { @@ -662,6 +677,8 @@ where system_conf::read_system_conf()? }; + let ttl = options.as_ref().and_then(|o| o.ttl).unwrap_or(false); + { let mut s = state.borrow_mut(); let perm = s.borrow_mut::(); @@ -720,8 +737,20 @@ where } _ => NetError::Dns(e), })? + .records() .iter() - .filter_map(|rdata| rdata_to_return_record(record_type)(rdata).transpose()) + .filter_map(|rec| { + let data = format_rdata(record_type)(rec.data()).unwrap(); + if ttl { + Some(DnsReturnRecord::WithTtl(DnsRecordWithTtl { + data: data?, + ttl: rec.ttl(), + })) + } else { + Some(DnsReturnRecord::WithoutTtl(data?)) + } + }) + .map(Ok) .collect::, NetError>>() } @@ -765,22 +794,22 @@ pub fn op_set_keepalive_inner( resource.set_keepalive(keepalive).map_err(NetError::Map) } -fn rdata_to_return_record( +fn format_rdata( ty: RecordType, -) -> impl Fn(&RData) -> Result, NetError> { +) -> impl Fn(&RData) -> Result, NetError> { use RecordType::*; - move |r: &RData| -> Result, NetError> { + move |r: &RData| -> Result, NetError> { let record = match ty { - A => r.as_a().map(ToString::to_string).map(DnsReturnRecord::A), + A => r.as_a().map(ToString::to_string).map(DnsRecordData::A), AAAA => r .as_aaaa() .map(ToString::to_string) - .map(DnsReturnRecord::Aaaa), + .map(DnsRecordData::Aaaa), ANAME => r .as_aname() .map(ToString::to_string) - .map(DnsReturnRecord::Aname), - CAA => r.as_caa().map(|caa| DnsReturnRecord::Caa { + .map(DnsRecordData::Aname), + CAA => r.as_caa().map(|caa| DnsRecordData::Caa { critical: caa.issuer_critical(), tag: caa.tag().to_string(), value: match caa.value() { @@ -807,12 +836,12 @@ fn rdata_to_return_record( CNAME => r .as_cname() .map(ToString::to_string) - .map(DnsReturnRecord::Cname), - MX => r.as_mx().map(|mx| DnsReturnRecord::Mx { + .map(DnsRecordData::Cname), + MX => r.as_mx().map(|mx| DnsRecordData::Mx { preference: mx.preference(), exchange: mx.exchange().to_string(), }), - NAPTR => r.as_naptr().map(|naptr| DnsReturnRecord::Naptr { + NAPTR => r.as_naptr().map(|naptr| DnsRecordData::Naptr { order: naptr.order(), preference: naptr.preference(), flags: String::from_utf8(naptr.flags().to_vec()).unwrap(), @@ -820,12 +849,9 @@ fn rdata_to_return_record( regexp: String::from_utf8(naptr.regexp().to_vec()).unwrap(), replacement: naptr.replacement().to_string(), }), - NS => r.as_ns().map(ToString::to_string).map(DnsReturnRecord::Ns), - PTR => r - .as_ptr() - .map(ToString::to_string) - .map(DnsReturnRecord::Ptr), - SOA => r.as_soa().map(|soa| DnsReturnRecord::Soa { + NS => r.as_ns().map(ToString::to_string).map(DnsRecordData::Ns), + PTR => r.as_ptr().map(ToString::to_string).map(DnsRecordData::Ptr), + SOA => r.as_soa().map(|soa| DnsRecordData::Soa { mname: soa.mname().to_string(), rname: soa.rname().to_string(), serial: soa.serial(), @@ -834,7 +860,7 @@ fn rdata_to_return_record( expire: soa.expire(), minimum: soa.minimum(), }), - SRV => r.as_srv().map(|srv| DnsReturnRecord::Srv { + SRV => r.as_srv().map(|srv| DnsRecordData::Srv { priority: srv.priority(), weight: srv.weight(), port: srv.port(), @@ -848,7 +874,7 @@ fn rdata_to_return_record( bytes.iter().map(|&b| b as char).collect::() }) .collect(); - DnsReturnRecord::Txt(texts) + DnsRecordData::Txt(texts) }), _ => return Err(NetError::UnsupportedRecordType), }; @@ -891,37 +917,37 @@ mod tests { #[test] fn rdata_to_return_record_a() { - let func = rdata_to_return_record(RecordType::A); + let func = format_rdata(RecordType::A); let rdata = RData::A(A(Ipv4Addr::new(127, 0, 0, 1))); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::A("127.0.0.1".to_string())) + Some(DnsRecordData::A("127.0.0.1".to_string())) ); } #[test] fn rdata_to_return_record_aaaa() { - let func = rdata_to_return_record(RecordType::AAAA); + let func = format_rdata(RecordType::AAAA); let rdata = RData::AAAA(AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Aaaa("::1".to_string())) + Some(DnsRecordData::Aaaa("::1".to_string())) ); } #[test] fn rdata_to_return_record_aname() { - let func = rdata_to_return_record(RecordType::ANAME); + let func = format_rdata(RecordType::ANAME); let rdata = RData::ANAME(ANAME(Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Aname("".to_string())) + Some(DnsRecordData::Aname("".to_string())) ); } #[test] fn rdata_to_return_record_caa() { - let func = rdata_to_return_record(RecordType::CAA); + let func = format_rdata(RecordType::CAA); let rdata = RData::CAA(CAA::new_issue( false, Some(Name::parse("example.com", None).unwrap()), @@ -929,7 +955,7 @@ mod tests { )); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Caa { + Some(DnsRecordData::Caa { critical: false, tag: "issue".to_string(), value: "example.com; account=123456".to_string(), @@ -939,21 +965,21 @@ mod tests { #[test] fn rdata_to_return_record_cname() { - let func = rdata_to_return_record(RecordType::CNAME); + let func = format_rdata(RecordType::CNAME); let rdata = RData::CNAME(CNAME(Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Cname("".to_string())) + Some(DnsRecordData::Cname("".to_string())) ); } #[test] fn rdata_to_return_record_mx() { - let func = rdata_to_return_record(RecordType::MX); + let func = format_rdata(RecordType::MX); let rdata = RData::MX(MX::new(10, Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Mx { + Some(DnsRecordData::Mx { preference: 10, exchange: "".to_string() }) @@ -962,7 +988,7 @@ mod tests { #[test] fn rdata_to_return_record_naptr() { - let func = rdata_to_return_record(RecordType::NAPTR); + let func = format_rdata(RecordType::NAPTR); let rdata = RData::NAPTR(NAPTR::new( 1, 2, @@ -973,7 +999,7 @@ mod tests { )); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Naptr { + Some(DnsRecordData::Naptr { order: 1, preference: 2, flags: "".to_string(), @@ -986,27 +1012,27 @@ mod tests { #[test] fn rdata_to_return_record_ns() { - let func = rdata_to_return_record(RecordType::NS); + let func = format_rdata(RecordType::NS); let rdata = RData::NS(NS(Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Ns("".to_string())) + Some(DnsRecordData::Ns("".to_string())) ); } #[test] fn rdata_to_return_record_ptr() { - let func = rdata_to_return_record(RecordType::PTR); + let func = format_rdata(RecordType::PTR); let rdata = RData::PTR(PTR(Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Ptr("".to_string())) + Some(DnsRecordData::Ptr("".to_string())) ); } #[test] fn rdata_to_return_record_soa() { - let func = rdata_to_return_record(RecordType::SOA); + let func = format_rdata(RecordType::SOA); let rdata = RData::SOA(SOA::new( Name::new(), Name::new(), @@ -1018,7 +1044,7 @@ mod tests { )); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Soa { + Some(DnsRecordData::Soa { mname: "".to_string(), rname: "".to_string(), serial: 0, @@ -1032,11 +1058,11 @@ mod tests { #[test] fn rdata_to_return_record_srv() { - let func = rdata_to_return_record(RecordType::SRV); + let func = format_rdata(RecordType::SRV); let rdata = RData::SRV(SRV::new(1, 2, 3, Name::new())); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Srv { + Some(DnsRecordData::Srv { priority: 1, weight: 2, port: 3, @@ -1047,7 +1073,7 @@ mod tests { #[test] fn rdata_to_return_record_txt() { - let func = rdata_to_return_record(RecordType::TXT); + let func = format_rdata(RecordType::TXT); let rdata = RData::TXT(TXT::from_bytes(vec![ "foo".as_bytes(), "bar".as_bytes(), @@ -1056,7 +1082,7 @@ mod tests { ])); assert_eq!( func(&rdata).unwrap(), - Some(DnsReturnRecord::Txt(vec![ + Some(DnsRecordData::Txt(vec![ "foo".to_string(), "bar".to_string(), "£".to_string(), diff --git a/ext/node/polyfills/internal_binding/cares_wrap.ts b/ext/node/polyfills/internal_binding/cares_wrap.ts index 6a864e5855..41f239d00c 100644 --- a/ext/node/polyfills/internal_binding/cares_wrap.ts +++ b/ext/node/polyfills/internal_binding/cares_wrap.ts @@ -186,20 +186,22 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery { this.#tries = tries; } - async #query(query: string, recordType: Deno.RecordType) { - // TODO(@bartlomieju): TTL logic. - + async #query(query: string, recordType: Deno.RecordType, ttl?: boolean) { let code: number; let ret: Awaited>; + const resolveOptions: Deno.ResolveDnsOptions = { + ...ttl !== undefined ? { ttl } : {}, + }; + if (this.#servers.length) { for (const [ipAddr, port] of this.#servers) { - const resolveOptions = { + Object.assign(resolveOptions, { nameServer: { ipAddr, port, }, - }; + }); ({ code, ret } = await this.#resolve( query, @@ -212,7 +214,7 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery { } } } else { - ({ code, ret } = await this.#resolve(query, recordType)); + ({ code, ret } = await this.#resolve(query, recordType, resolveOptions)); } return { code: code!, ret: ret! }; @@ -351,18 +353,34 @@ export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery { } queryA(req: QueryReqWrap, name: string): number { - this.#query(name, "A").then(({ code, ret }) => { - req.oncomplete(code, ret); + this.#query(name, "A", req.ttl).then(({ code, ret }) => { + let recordsWithTtl; + if (req.ttl) { + recordsWithTtl = (ret as Deno.RecordWithTtl[]).map((val) => ({ + address: val?.data, + ttl: val?.ttl, + })); + } + + req.oncomplete(code, recordsWithTtl ?? ret); }); return 0; } queryAaaa(req: QueryReqWrap, name: string): number { - this.#query(name, "AAAA").then(({ code, ret }) => { - const records = (ret as string[]).map((record) => compressIPv6(record)); + this.#query(name, "AAAA", req.ttl).then(({ code, ret }) => { + let recordsWithTtl; + if (req.ttl) { + recordsWithTtl = (ret as Deno.RecordWithTtl[]).map((val) => ({ + address: compressIPv6(val?.data as string), + ttl: val?.ttl, + })); + } else { + ret = (ret as string[]).map((record) => compressIPv6(record)); + } - req.oncomplete(code, records); + req.oncomplete(code, recordsWithTtl ?? ret); }); return 0; diff --git a/tests/unit/dns_test.ts b/tests/unit/dns_test.ts new file mode 100644 index 0000000000..98367dc9c2 --- /dev/null +++ b/tests/unit/dns_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +import { resolve4, resolve6 } from "node:dns/promises"; +import { assertEquals } from "@std/assert/equals"; + +Deno.test({ + name: "Dns resolving for ttl values, A and AAAA records", + async fn() { + const ARecord = "34.120.54.55"; + const AAAARecord = "2600:1901::6d85::"; + + const ARes1 = await Deno.resolveDns("deno.com", "A", { ttl: true }); + const ARes2 = await resolve4("deno.com", { ttl: true }); + + assertEquals(ARes1[0].data, ARecord); + assertEquals(ARes2[0].address, ARecord); + + const AAAARes1 = await Deno.resolveDns("deno.com", "AAAA", { ttl: true }); + const AAAARes2 = await resolve6("deno.com", { ttl: true }); + + assertEquals(AAAARes1[0].data, AAAARecord); + assertEquals(AAAARes2[0].address, AAAARecord); + }, +});