From 88872ff2690da95a61a3f0aa9a01a777660ab549 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 9 Dec 2024 16:13:22 -0500 Subject: [PATCH] implement node:dns module for nodejs_compat (#3183) --- compile_flags.txt | 1 + deps/rust/Cargo.lock | 27 +- deps/rust/cargo.bzl | 1 + deps/rust/crates/BUILD.bazel | 6 + deps/rust/crates/BUILD.thiserror-2.0.6.bazel | 114 ++++++ .../crates/BUILD.thiserror-impl-2.0.6.bazel | 49 +++ deps/rust/crates/defs.bzl | 22 ++ justfile | 4 + src/node/dns.ts | 141 ++++++-- src/node/dns/promises.ts | 157 +++++++++ src/node/internal/dns.d.ts | 68 +--- src/node/internal/internal_dns.ts | 246 +++++++++++++ src/node/internal/internal_dns_client.ts | 242 +++++++++++++ src/node/internal/internal_errors.ts | 22 ++ src/node/internal/internal_utils.ts | 171 ++++++++- src/node/util.ts | 51 +-- src/rust/dns/BUILD.bazel | 13 + src/rust/dns/lib.rs | 204 +++++++++++ src/workerd/api/node/BUILD.bazel | 2 + src/workerd/api/node/dns.c++ | 104 ++---- src/workerd/api/node/dns.h | 118 ++----- src/workerd/api/node/tests/dns-nodejs-test.js | 329 +++++++++++++++++- 22 files changed, 1776 insertions(+), 316 deletions(-) create mode 100644 deps/rust/crates/BUILD.thiserror-2.0.6.bazel create mode 100644 deps/rust/crates/BUILD.thiserror-impl-2.0.6.bazel create mode 100644 src/node/internal/internal_dns.ts create mode 100644 src/node/internal/internal_dns_client.ts create mode 100644 src/rust/dns/BUILD.bazel create mode 100644 src/rust/dns/lib.rs diff --git a/compile_flags.txt b/compile_flags.txt index 96f7d0b1d1d..ad0368fca5d 100644 --- a/compile_flags.txt +++ b/compile_flags.txt @@ -50,6 +50,7 @@ -isystembazel-bin/src/rust/cxx-integration/_virtual_includes/cxx-include/ -isystembazel-bin/src/rust/cxx-integration/_virtual_includes/cxx-integration@cxx -isystembazel-bin/src/rust/cxx-integration-test/_virtual_includes/cxx-integration-test@cxx +-isystembazel-bin/src/rust/dns/_virtual_includes/dns@cxx -D_FORTIFY_SOURCE=1 -D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES -D_LIBCPP_NO_ABI_TAG diff --git a/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index d830399314d..da38e1fab3e 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -285,6 +285,7 @@ dependencies = [ "serde", "serde_json", "syn 2.0.87", + "thiserror 2.0.6", "tokio", "tracing", ] @@ -450,7 +451,7 @@ dependencies = [ "memchr", "mime", "selectors", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -461,7 +462,7 @@ dependencies = [ "encoding_rs", "libc", "lol_html", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -876,7 +877,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +dependencies = [ + "thiserror-impl 2.0.6", ] [[package]] @@ -890,6 +900,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "tokio" version = "1.41.1" diff --git a/deps/rust/cargo.bzl b/deps/rust/cargo.bzl index e1bada151a1..caf8ca3e408 100644 --- a/deps/rust/cargo.bzl +++ b/deps/rust/cargo.bzl @@ -24,6 +24,7 @@ PACKAGES = { "serde_json": crate.spec(version = "1"), "serde": crate.spec(version = "1", features = ["derive"]), "syn": crate.spec(version = "2"), + "thiserror": crate.spec(version = "2"), # tokio is huge, let's enable only features when we actually need them. "tokio": crate.spec(version = "1", default_features = False, features = ["net", "rt", "rt-multi-thread", "time"]), "tracing": crate.spec(version = "0", default_features = False, features = ["std"]), diff --git a/deps/rust/crates/BUILD.bazel b/deps/rust/crates/BUILD.bazel index 07b45217a2d..1bdfddb456f 100644 --- a/deps/rust/crates/BUILD.bazel +++ b/deps/rust/crates/BUILD.bazel @@ -133,6 +133,12 @@ alias( tags = ["manual"], ) +alias( + name = "thiserror", + actual = "@crates_vendor__thiserror-2.0.6//:thiserror", + tags = ["manual"], +) + alias( name = "tokio", actual = "@crates_vendor__tokio-1.41.1//:tokio", diff --git a/deps/rust/crates/BUILD.thiserror-2.0.6.bazel b/deps/rust/crates/BUILD.thiserror-2.0.6.bazel new file mode 100644 index 00000000000..2450065e749 --- /dev/null +++ b/deps/rust/crates/BUILD.thiserror-2.0.6.bazel @@ -0,0 +1,114 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @//deps/rust:crates_vendor +############################################################################### + +load("@rules_rust//cargo:defs.bzl", "cargo_build_script") +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "thiserror", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "default", + "std", + ], + crate_root = "src/lib.rs", + edition = "2021", + proc_macro_deps = [ + "@crates_vendor__thiserror-impl-2.0.6//:thiserror_impl", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=thiserror", + "manual", + "noclippy", + "norustfmt", + ], + version = "2.0.6", + deps = [ + "@crates_vendor__thiserror-2.0.6//:build_script_build", + ], +) + +cargo_build_script( + name = "_bs", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + "**/*.rs", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "default", + "std", + ], + crate_name = "build_script_build", + crate_root = "build.rs", + data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + edition = "2021", + pkg_name = "thiserror", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=thiserror", + "manual", + "noclippy", + "norustfmt", + ], + version = "2.0.6", + visibility = ["//visibility:private"], +) + +alias( + name = "build_script_build", + actual = ":_bs", + tags = ["manual"], +) diff --git a/deps/rust/crates/BUILD.thiserror-impl-2.0.6.bazel b/deps/rust/crates/BUILD.thiserror-impl-2.0.6.bazel new file mode 100644 index 00000000000..1efc78cff3c --- /dev/null +++ b/deps/rust/crates/BUILD.thiserror-impl-2.0.6.bazel @@ -0,0 +1,49 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @//deps/rust:crates_vendor +############################################################################### + +load("@rules_rust//rust:defs.bzl", "rust_proc_macro") + +package(default_visibility = ["//visibility:public"]) + +rust_proc_macro( + name = "thiserror_impl", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_root = "src/lib.rs", + edition = "2021", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=thiserror-impl", + "manual", + "noclippy", + "norustfmt", + ], + version = "2.0.6", + deps = [ + "@crates_vendor__proc-macro2-1.0.89//:proc_macro2", + "@crates_vendor__quote-1.0.37//:quote", + "@crates_vendor__syn-2.0.87//:syn", + ], +) diff --git a/deps/rust/crates/defs.bzl b/deps/rust/crates/defs.bzl index 9119ad971c9..7ff610d5c70 100644 --- a/deps/rust/crates/defs.bzl +++ b/deps/rust/crates/defs.bzl @@ -312,6 +312,7 @@ _NORMAL_DEPENDENCIES = { "serde": Label("@crates_vendor__serde-1.0.215//:serde"), "serde_json": Label("@crates_vendor__serde_json-1.0.132//:serde_json"), "syn": Label("@crates_vendor__syn-2.0.87//:syn"), + "thiserror": Label("@crates_vendor__thiserror-2.0.6//:thiserror"), "tokio": Label("@crates_vendor__tokio-1.41.1//:tokio"), "tracing": Label("@crates_vendor__tracing-0.1.40//:tracing"), }, @@ -1386,6 +1387,16 @@ def crate_repositories(): build_file = Label("@workerd//deps/rust/crates:BUILD.thiserror-1.0.69.bazel"), ) + maybe( + http_archive, + name = "crates_vendor__thiserror-2.0.6", + sha256 = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47", + type = "tar.gz", + urls = ["https://static.crates.io/crates/thiserror/2.0.6/download"], + strip_prefix = "thiserror-2.0.6", + build_file = Label("@workerd//deps/rust/crates:BUILD.thiserror-2.0.6.bazel"), + ) + maybe( http_archive, name = "crates_vendor__thiserror-impl-1.0.69", @@ -1396,6 +1407,16 @@ def crate_repositories(): build_file = Label("@workerd//deps/rust/crates:BUILD.thiserror-impl-1.0.69.bazel"), ) + maybe( + http_archive, + name = "crates_vendor__thiserror-impl-2.0.6", + sha256 = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312", + type = "tar.gz", + urls = ["https://static.crates.io/crates/thiserror-impl/2.0.6/download"], + strip_prefix = "thiserror-impl-2.0.6", + build_file = Label("@workerd//deps/rust/crates:BUILD.thiserror-impl-2.0.6.bazel"), + ) + maybe( http_archive, name = "crates_vendor__tokio-1.41.1", @@ -1623,6 +1644,7 @@ def crate_repositories(): struct(repo = "crates_vendor__serde-1.0.215", is_dev_dep = False), struct(repo = "crates_vendor__serde_json-1.0.132", is_dev_dep = False), struct(repo = "crates_vendor__syn-2.0.87", is_dev_dep = False), + struct(repo = "crates_vendor__thiserror-2.0.6", is_dev_dep = False), struct(repo = "crates_vendor__tokio-1.41.1", is_dev_dep = False), struct(repo = "crates_vendor__tracing-0.1.40", is_dev_dep = False), ] diff --git a/justfile b/justfile index 8db8dd77ea4..5f929076682 100644 --- a/justfile +++ b/justfile @@ -60,3 +60,7 @@ rustfmt: # example: just bench mimetype bench path: bazel run //src/workerd/tests:bench-{{path}} + +# example: just clippy dns +clippy package="...": + bazel build //src/rust/{{package}} --config=lint diff --git a/src/node/dns.ts b/src/node/dns.ts index 52a495ac85c..181382fd870 100644 --- a/src/node/dns.ts +++ b/src/node/dns.ts @@ -1,30 +1,124 @@ -import { default as dnsUtil } from 'node-internal:dns'; +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + import * as errorCodes from 'node-internal:internal_dns_constants'; +import * as dns from 'node-internal:internal_dns'; +import { callbackify } from 'node-internal:internal_utils'; export * from 'node-internal:internal_dns_constants'; -export const getServers = dnsUtil.getServers.bind(dnsUtil); -export const lookup = dnsUtil.lookup.bind(dnsUtil); -export const lookupService = dnsUtil.lookupService.bind(dnsUtil); -export const resolve = dnsUtil.resolve.bind(dnsUtil); -export const resolve4 = dnsUtil.resolve4.bind(dnsUtil); -export const resolve6 = dnsUtil.resolve6.bind(dnsUtil); -export const resolveAny = dnsUtil.resolveAny.bind(dnsUtil); -export const resolveCname = dnsUtil.resolveCname.bind(dnsUtil); -export const resolveCaa = dnsUtil.resolveCaa.bind(dnsUtil); -export const resolveMx = dnsUtil.resolveMx.bind(dnsUtil); -export const resolveNaptr = dnsUtil.resolveNaptr.bind(dnsUtil); -export const resolveNs = dnsUtil.resolveNs.bind(dnsUtil); -export const resolvePtr = dnsUtil.resolvePtr.bind(dnsUtil); -export const resolveSoa = dnsUtil.resolveSoa.bind(dnsUtil); -export const resolveSrv = dnsUtil.resolveSrv.bind(dnsUtil); -export const resolveTxt = dnsUtil.resolveTxt.bind(dnsUtil); -export const reverse = dnsUtil.reverse.bind(dnsUtil); -export const setDefaultResultOrder = - dnsUtil.setDefaultResultOrder.bind(dnsUtil); -export const getDefaultResultOrder = - dnsUtil.getDefaultResultOrder.bind(dnsUtil); -export const setServers = dnsUtil.setServers.bind(dnsUtil); +export const reverse = callbackify(dns.reverse.bind(dns)); +export const resolveTxt = callbackify(dns.resolveTxt.bind(dns)); +export const resolveCaa = callbackify(dns.resolveCaa.bind(dns)); +export const resolveMx = callbackify(dns.resolveMx.bind(dns)); +export const resolveCname = callbackify(dns.resolveCname.bind(dns)); +export const resolveNs = callbackify(dns.resolveNs.bind(dns)); +export const resolvePtr = callbackify(dns.resolvePtr.bind(dns)); +export const resolveSrv = callbackify(dns.resolveSrv.bind(dns)); +export const resolveSoa = callbackify(dns.resolveSoa.bind(dns)); +export const resolveNaptr = callbackify(dns.resolveNaptr.bind(dns)); +export const resolve4 = callbackify(dns.resolve4.bind(dns)); +export const resolve6 = callbackify(dns.resolve6.bind(dns)); +export const getServers = callbackify(dns.getServers.bind(dns)); +export const setServers = callbackify(dns.setServers.bind(dns)); +export const getDefaultResultOrder = callbackify( + dns.getDefaultResultOrder.bind(dns) +); +export const setDefaultResultOrder = callbackify( + dns.setDefaultResultOrder.bind(dns) +); +export const lookup = callbackify(dns.lookup.bind(this)); +export const lookupService = callbackify(dns.lookupService.bind(this)); +export const resolve = callbackify(dns.resolve.bind(this)); +export const resolveAny = callbackify(dns.resolveAny.bind(this)); + +export class Resolver { + public cancel(): void { + // TODO(soon): Implement this. + throw new Error('Not implemented'); + } + + public setLocalAddress(): void { + // Does not apply to workerd implementation + throw new Error('Not implemented'); + } + + public getServers(callback: never): void { + getServers(callback); + } + + public resolve(callback: never): void { + resolve(callback); + } + + public resolve4( + input: string, + options?: { ttl?: boolean }, + callback?: never + ): void { + // @ts-expect-error TS2554 TODO(soon): Fix callbackify typescript types + resolve4(input, options, callback); + } + + public resolve6( + input: string, + options?: { ttl?: boolean }, + callback?: never + ): void { + // @ts-expect-error TS2554 TODO(soon): Fix callbackify typescript types + resolve6(input, options, callback); + } + + public resolveAny(_input: string, callback: never): void { + resolveAny(callback); + } + + public resolveCaa(name: string, callback: never): void { + resolveCaa(name, callback); + } + + public resolveCname(name: string, callback: never): void { + resolveCname(name, callback); + } + + public resolveMx(name: string, callback: never): void { + resolveMx(name, callback); + } + + public resolveNaptr(name: string, callback: never): void { + resolveNaptr(name, callback); + } + + public resolveNs(name: string, callback: never): void { + resolveNs(name, callback); + } + + public resolvePtr(name: string, callback: never): void { + resolvePtr(name, callback); + } + + public resolveSoa(name: string, callback: never): void { + resolveSoa(name, callback); + } + + public resolveSrv(name: string, callback: never): void { + resolveSrv(name, callback); + } + + public resolveTxt(name: string, callback: never): void { + resolveTxt(name, callback); + } + + public reverse(name: string, callback: never): void { + reverse(name, callback); + } + + public setServers(callback: never): void { + setServers(callback); + } +} export default { getServers, @@ -47,6 +141,7 @@ export default { setDefaultResultOrder, getDefaultResultOrder, setServers, + Resolver, ...errorCodes, }; diff --git a/src/node/dns/promises.ts b/src/node/dns/promises.ts index fad3d749fb1..8430b79f1e9 100644 --- a/src/node/dns/promises.ts +++ b/src/node/dns/promises.ts @@ -1,7 +1,164 @@ import * as errorCodes from 'node-internal:internal_dns_constants'; +import { + reverse, + resolveTxt, + resolveCaa, + resolveMx, + resolveCname, + resolveNs, + resolvePtr, + resolveSrv, + resolveSoa, + resolveNaptr, + resolve4, + resolve6, + getServers, + setServers, + getDefaultResultOrder, + setDefaultResultOrder, + lookup, + lookupService, + resolve, + resolveAny, +} from 'node-internal:internal_dns'; +import { + CAA, + MX, + NAPTR, + SOA, + SRV, + TTLResponse, +} from 'node:internal/internal_dns_client'; export * from 'node-internal:internal_dns_constants'; +export { + reverse, + resolveTxt, + resolveCaa, + resolveMx, + resolveCname, + resolveNs, + resolvePtr, + resolveSrv, + resolveSoa, + resolveNaptr, + resolve4, + resolve6, + getServers, + setServers, + getDefaultResultOrder, + setDefaultResultOrder, + lookup, + lookupService, + resolve, + resolveAny, +} from 'node-internal:internal_dns'; + +export class Resolver { + // eslint-disable-next-line @typescript-eslint/require-await + public async cancel(): Promise { + // TODO(soon): Implement this. + throw new Error('Not implemented'); + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async setLocalAddress(): Promise { + // Does not apply to workerd implementation + throw new Error('Not implemented'); + } + + public getServers(): Promise { + return getServers(); + } + + public resolve(): Promise { + return resolve(); + } + + public resolve4( + input: string, + options?: { ttl?: boolean } + ): Promise<(string | TTLResponse)[]> { + return resolve4(input, options); + } + + public resolve6( + input: string, + options?: { ttl?: boolean } + ): Promise<(string | TTLResponse)[]> { + return resolve6(input, options); + } + + public resolveAny(): Promise { + return resolveAny(); + } + + public resolveCaa(name: string): Promise { + return resolveCaa(name); + } + + public resolveCname(name: string): Promise { + return resolveCname(name); + } + + public resolveMx(name: string): Promise { + return resolveMx(name); + } + + public resolveNaptr(name: string): Promise { + return resolveNaptr(name); + } + + public resolveNs(name: string): Promise { + return resolveNs(name); + } + + public esolvePtr(name: string): Promise { + return resolvePtr(name); + } + + public resolveSoa(name: string): Promise { + return resolveSoa(name); + } + + public resolveSrv(name: string): Promise { + return resolveSrv(name); + } + + public resolveTxt(name: string): Promise { + return resolveTxt(name); + } + + public reverse(name: string): Promise { + return reverse(name); + } + + public setServers(): Promise { + return setServers(); + } +} export default { + reverse, + resolveTxt, + resolveCaa, + resolveMx, + resolveCname, + resolveNs, + resolvePtr, + resolveSrv, + resolveSoa, + resolveNaptr, + resolve4, + resolve6, + getServers, + setServers, + getDefaultResultOrder, + setDefaultResultOrder, + lookup, + lookupService, + resolve, + resolveAny, + Resolver, ...errorCodes, }; diff --git a/src/node/internal/dns.d.ts b/src/node/internal/dns.d.ts index c8dabd1a53b..4bd2b2df797 100644 --- a/src/node/internal/dns.d.ts +++ b/src/node/internal/dns.d.ts @@ -1,60 +1,14 @@ -export function getServers(): string[]; - -export type LookupOptions = { - family: number | string; - hints: number; - all: boolean; - order: string; - verbatim: boolean; +export function parseCaaRecord(record: string): { + critical: number; + field: 'issue' | 'iodef' | 'issuewild'; + value: string; }; -export function lookup( - hostname: string, - options: LookupOptions, - callback: unknown -): void; - -export function lookupService( - address: string, - port: number, - callback: unknown -): void; -export function resolve( - hostname: string, - rrtype: string, - callback: unknown -): void; - -export type Resolve4Options = { - ttl: boolean; +export function parseNaptrRecord(record: string): { + flags: string; + service: string; + regexp: string; + replacement: string; + order: number; + preference: number; }; -export function resolve4( - hostname: string, - options: Resolve4Options, - callback: unknown -): void; - -export type Resolve6Options = { - ttl: boolean; -}; -export function resolve6( - hostname: string, - options: Resolve6Options, - callback: unknown -): void; - -export function resolveAny(hostname: string, callback: unknown): void; -export function resolveCname(hostname: string, callback: unknown): void; -export function resolveCaa(hostname: string, callback: unknown): void; -export function resolveMx(hostname: string, callback: unknown): void; -export function resolveNaptr(hostname: string, callback: unknown): void; -export function resolveNs(hostname: string, callback: unknown): void; -export function resolvePtr(hostname: string, callback: unknown): void; -export function resolveSoa(hostname: string, callback: unknown): void; -export function resolveSrv(hostname: string, callback: unknown): void; -export function resolveTxt(hostname: string, callback: unknown): void; -export function reverse(ip: string, callback: unknown): void; - -export function setDefaultResultOrder(order: string): void; -export function getDefaultResultOrder(): string; -export function setServers(servers: string[]): string; diff --git a/src/node/internal/internal_dns.ts b/src/node/internal/internal_dns.ts new file mode 100644 index 00000000000..688d45512c7 --- /dev/null +++ b/src/node/internal/internal_dns.ts @@ -0,0 +1,246 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import { + sendDnsRequest, + validateAnswer, + normalizeMx, + normalizeCname, + normalizeCaa, + normalizePtr, + normalizeNaptr, + normalizeNs, + normalizeSoa, + normalizeTxt, + normalizeSrv, + type MX, + type CAA, + type NAPTR, + type SOA, + type SRV, + type TTLResponse, +} from 'node-internal:internal_dns_client'; +import { DnsError } from 'node-internal:internal_errors'; +import { validateString } from 'node-internal:validators'; +import * as errorCodes from 'node-internal:internal_dns_constants'; + +// eslint-disable-next-line @typescript-eslint/require-await +export async function getServers(): Promise { + return ['1.1.1.1', '2606:4700:4700::1111', '1.0.0.1', '2606:4700:4700::1001']; +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function lookup(): Promise { + // TODO(soon): Implement this. + throw new Error('Not implemented'); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function lookupService(): Promise { + // TODO(soon): Implement this. + throw new Error('Not implemented'); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function resolve(): Promise { + // TODO(soon): Implement this. + throw new Error('Not implemented'); +} + +export function resolve4( + name: string, + options?: { ttl?: boolean } +): Promise<(string | TTLResponse)[]> { + validateString(name, 'name'); + + // The following change is done to comply with Node.js behavior + const ttl = !!options?.ttl; + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'A').then((json) => { + validateAnswer(json.Answer, name, 'queryA'); + + return json.Answer.map((a) => + ttl ? { ttl: a.TTL, address: a.data } : a.data + ); + }); +} + +export function resolve6( + name: string, + options?: { ttl?: boolean } +): Promise<(string | TTLResponse)[]> { + validateString(name, 'name'); + + // The following change is done to comply with Node.js behavior + const ttl = !!options?.ttl; + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'AAAA').then((json) => { + validateAnswer(json.Answer, name, 'queryAaaa'); + + return json.Answer.map((a) => + ttl ? { ttl: a.TTL, address: a.data } : a.data + ); + }); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function resolveAny(): Promise { + // TODO(soon): Implement this + throw new Error('Not implemented'); +} + +export function resolveCname(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'CNAME').then((json) => { + validateAnswer(json.Answer, name, 'queryCname'); + + return json.Answer.map(normalizeCname); + }); +} + +export function resolveCaa(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'CAA').then((json) => { + validateAnswer(json.Answer, name, 'queryCaa'); + + return json.Answer.map(normalizeCaa); + }); +} + +export function resolveMx(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'MX').then((json) => { + validateAnswer(json.Answer, name, 'queryMx'); + + return json.Answer.map((answer) => normalizeMx(name, answer)); + }); +} + +export function resolveNaptr(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'NAPTR').then((json) => { + validateAnswer(json.Answer, name, 'queryNaptr'); + + return json.Answer.map(normalizeNaptr); + }); +} + +export function resolveNs(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'NS').then((json) => { + validateAnswer(json.Answer, name, 'queryNs'); + + return json.Answer.map(normalizeNs); + }); +} + +export function resolvePtr(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'PTR').then((json) => { + validateAnswer(json.Answer, name, 'queryPtr'); + + return json.Answer.map(normalizePtr); + }); +} + +export function resolveSoa(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'SOA').then((json) => { + validateAnswer(json.Answer, name, 'querySoa'); + + // This is highly unlikely, but let's assert length just to be safe. + if (json.Answer.length === 0) { + throw new DnsError(name, errorCodes.NOTFOUND, 'querySoa'); + } + + return normalizeSoa(json.Answer[0]!); + }); +} + +export function resolveSrv(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'SRV').then((json) => { + validateAnswer(json.Answer, name, 'querySrv'); + + return json.Answer.map(normalizeSrv); + }); +} + +export function resolveTxt(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'TXT').then((json) => { + validateAnswer(json.Answer, name, 'queryTxt'); + + return json.Answer.map(normalizeTxt); + }); +} + +export function reverse(name: string): Promise { + validateString(name, 'name'); + + // Validation errors needs to be sync. + // Return a promise rather than using async qualifier. + return sendDnsRequest(name, 'PTR').then((json) => { + validateAnswer(json.Answer, name, 'queryPtr'); + + return json.Answer.map(({ data }) => { + if (data.endsWith('.')) { + return data.slice(0, -1); + } + return data; + }); + }); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function setDefaultResultOrder(): Promise { + // Does not apply to workerd + throw new Error('Not implemented'); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function getDefaultResultOrder(): Promise { + // Does not apply to workerd + throw new Error('Not implemented'); +} + +// eslint-disable-next-line @typescript-eslint/require-await +export async function setServers(): Promise { + // This function does not apply to workerd model. + // Our implementation always use Cloudflare DNS and does not + // allow users to change the underlying DNS server. + throw new Error('Not implemented'); +} diff --git a/src/node/internal/internal_dns_client.ts b/src/node/internal/internal_dns_client.ts new file mode 100644 index 00000000000..f905708b961 --- /dev/null +++ b/src/node/internal/internal_dns_client.ts @@ -0,0 +1,242 @@ +import { default as dnsUtil } from 'node-internal:dns'; +import * as errorCodes from 'node-internal:internal_dns_constants'; +import { DnsError } from 'node-internal:internal_errors'; +import { validateString } from 'node-internal:validators'; + +export type TTLResponse = { + ttl: number; + address: string; +}; +export interface Answer { + // The record owner. + name: string; + // The type of DNS record. + // These are defined here: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 + type: number; + // The number of seconds the answer can be stored in cache before it is considered stale. + TTL: number; + // The value of the DNS record for the given name and type. The data will be in text for standardized record types and in hex for unknown types. + data: string; +} +export interface FailedResponse { + error: string; +} +export interface SuccessResponse { + // The Response Code of the DNS Query. + // These are defined here: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 + Status: number; + // If true, it means the truncated bit was set. + // This happens when the DNS answer is larger than a single UDP or TCP packet. + // TC will almost always be false with Cloudflare DNS over HTTPS because Cloudflare supports the maximum response size. + TC: boolean; + // If true, it means the Recursive Desired bit was set. + RD: boolean; + // If true, it means the Recursion Available bit was set. + RA: boolean; + // If true, it means that every record in the answer was verified with DNSSEC. + AD: boolean; + // If true, the client asked to disable DNSSEC validation. + CD: boolean; + Question: { + // The record name requested. + name: string; + // The type of DNS record requested. + // These are defined here: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 + type: number; + }[]; + Answer?: Answer[]; + Authority: Answer[]; + Additional?: Answer; +} + +export async function sendDnsRequest( + name: string, + type: string +): Promise { + // We are using cloudflare-dns.com and not 1.1.1.1 because of certificate issues. + // TODO(soon): Replace this when KJ certificate issues are resolved. + const server = new URL('https://cloudflare-dns.com/dns-query'); + server.searchParams.set('name', name); + server.searchParams.set('type', type); + + // syscall needs to be in format of `queryTxt` + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const syscall = `query${type.at(0)?.toUpperCase()}${type.slice(1)}`; + + let json: SuccessResponse | FailedResponse; + try { + const response = await fetch(server, { + headers: { + Accept: 'application/dns-json', + }, + method: 'GET', + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + json = await response.json(); + } catch { + throw new DnsError(name, errorCodes.BADQUERY, syscall); + } + + if ('error' in json) { + throw new DnsError(name, errorCodes.BADRESP, syscall); + } + + if (!json.Question.at(0)) { + // Some APIs depend on Question being existent. + throw new DnsError(name, errorCodes.BADRESP, syscall); + } + + if (json.Answer?.at(0)?.name === '') { + throw new DnsError(name, errorCodes.NOTFOUND, syscall); + } + + return json; +} + +export function validateAnswer( + answer: unknown, + name: string, + query: string +): asserts answer is Answer[] { + if (answer == null) { + throw new DnsError(name, errorCodes.NOTFOUND, query); + } +} + +export type MX = { + exchange: string; + priority: number; +}; +export function normalizeMx(name: string, answer: Answer): MX { + const [priority, exchange]: string[] = answer.data.split(' '); + if (priority == null || exchange == null) { + throw new DnsError(name, errorCodes.BADRESP, 'queryMx'); + } + + // Cloudflare API returns "data": "10 smtp.google.com." hence + // we need to parse it. Let's play it safe. + if (exchange.endsWith('.')) { + return { + exchange: exchange.slice(0, -1), + priority: parseInt(priority, 10), + }; + } + + return { + exchange, + priority: parseInt(priority, 10), + }; +} + +export function normalizeCname({ data }: Answer): string { + // Cloudflare DNS returns "nodejs.org." whereas + // Node.js returns "nodejs.org" as a CNAME data. + if (data.endsWith('.')) { + return data.slice(0, -1); + } + return data; +} + +export type CAA = { + critical: number; + issue?: string; + iodef?: string; + issuewild?: string; +}; +export function normalizeCaa({ data }: Answer): CAA { + // CAA API returns "hex", so we need to convert it to UTF-8 + const record = dnsUtil.parseCaaRecord(data); + const obj: CAA = { critical: record.critical }; + obj[record.field] = record.value; + return obj; +} + +export type NAPTR = { + flags: string; + service: string; + regexp: string; + replacement: string; + order: number; + preference: number; +}; +export function normalizeNaptr({ data }: Answer): NAPTR { + // Cloudflare DNS appends "." at the end whereas Node.js doesn't. + return dnsUtil.parseNaptrRecord(data); +} + +export function normalizePtr({ data }: Answer): string { + if (data.endsWith('.')) { + return data.slice(0, -1); + } + return data; +} + +export function normalizeNs({ data }: Answer): string { + if (data.endsWith('.')) { + return data.slice(0, -1); + } + return data; +} + +export type SOA = { + nsname: string; + hostmaster: string; + serial: number; + refresh: number; + retry: number; + expire: number; + minttl: number; +}; +export function normalizeSoa({ data }: Answer): SOA { + // Cloudflare DNS returns ""meera.ns.cloudflare.com. dns.cloudflare.com. 2357999196 10000 2400 604800 1800"" + const [nsname, hostmaster, serial, refresh, retry, expire, minttl] = + data.split(' '); + + validateString(nsname, 'nsname'); + validateString(hostmaster, 'hostmaster'); + validateString(serial, 'serial'); + validateString(refresh, 'refresh'); + validateString(retry, 'retry'); + validateString(expire, 'expire'); + validateString(minttl, 'minttl'); + + return { + nsname, + hostmaster, + serial: parseInt(serial, 10), + refresh: parseInt(refresh, 10), + retry: parseInt(retry, 10), + expire: parseInt(expire, 10), + minttl: parseInt(minttl, 10), + }; +} + +export type SRV = { + name: string; + port: number; + priority: number; + weight: number; +}; +export function normalizeSrv({ data }: Answer): SRV { + // Cloudflare DNS returns "5 0 80 calendar.google.com" + const [priority, weight, port, name] = data.split(' '); + validateString(priority, 'priority'); + validateString(weight, 'weight'); + validateString(port, 'port'); + validateString(name, 'name'); + return { + priority: parseInt(priority, 10), + weight: parseInt(weight, 10), + port: parseInt(port, 10), + name, + }; +} + +export function normalizeTxt({ data }: Answer): string[] { + // Each entry has quotation marks as a prefix and suffix. + // Node.js APIs doesn't have them. + if (data.startsWith('"') && data.endsWith('"')) { + return [data.slice(1, -1)]; + } + return [data]; +} diff --git a/src/node/internal/internal_errors.ts b/src/node/internal/internal_errors.ts index c6cc970bc52..3320ac35993 100644 --- a/src/node/internal/internal_errors.ts +++ b/src/node/internal/internal_errors.ts @@ -587,6 +587,28 @@ export class ERR_INVALID_URI extends NodeError { } } +// Example: +// +// Error: queryTxt ENOTFOUND google.com2 +// at QueryReqWrap.onresolve [as oncomplete] (node:internal/dns/callback_resolver:45:19) +// at QueryReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) { +// errno: undefined, +// code: 'ENOTFOUND', +// syscall: 'queryTxt', +// hostname: 'google.com2' +// } +export class DnsError extends NodeError { + errno = undefined; + + constructor( + public hostname: string, + code: string, + public syscall: string + ) { + super(code, `${syscall} ${code} ${hostname}`); + } +} + export function aggregateTwoErrors(innerError: any, outerError: any) { if (innerError && outerError && innerError !== outerError) { if (Array.isArray(outerError.errors)) { diff --git a/src/node/internal/internal_utils.ts b/src/node/internal/internal_utils.ts index b02e3731891..7290f4f5b34 100644 --- a/src/node/internal/internal_utils.ts +++ b/src/node/internal/internal_utils.ts @@ -26,9 +26,10 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -/* eslint-disable */ import { default as bufferUtil } from 'node-internal:buffer'; import type { Encoding } from 'node-internal:buffer'; +import { validateFunction } from 'node-internal:validators'; +import { ERR_FALSY_VALUE_REJECTION } from 'node-internal:internal_errors'; const { UTF8, UTF16LE, HEX, ASCII, BASE64, BASE64URL, LATIN1 } = bufferUtil; @@ -44,7 +45,8 @@ export function normalizeEncoding(enc?: string): Encoding | undefined { return slowCases(enc); } -export function slowCases(enc: string): Encoding | undefined { +export function slowCases(enc: unknown): Encoding | undefined { + // @ts-expect-error TS18046 TS complains about unknown can not have length. switch (enc.length) { case 4: if (enc === 'UTF8') return UTF8; @@ -111,7 +113,7 @@ export function slowCases(enc: string): Encoding | undefined { return undefined; } -export function spliceOne(list: (string | undefined)[], index: number) { +export function spliceOne(list: (string | undefined)[], index: number): void { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; list.pop(); } @@ -200,3 +202,166 @@ export function getOwnNonIndexProperties( } return result; } + +function callbackifyOnRejected( + reason: unknown, + cb: (error?: unknown) => void +): void { + if (!reason) { + cb(new ERR_FALSY_VALUE_REJECTION(String(reason))); + return; + } + cb(reason); +} + +// Types are taken from @types/node package +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/dccb0e78c4d3265ae06985789156451bd73312c0/types/node/util.d.ts#L1054 +export function callbackify( + fn: () => Promise +): (callback: (err: Error) => void) => void; +export function callbackify( + fn: () => Promise +): (callback: (err: Error, result: TResult) => void) => void; +export function callbackify( + fn: (arg1: T1) => Promise +): (arg1: T1, callback: (err: Error) => void) => void; +export function callbackify( + fn: (arg1: T1) => Promise +): (arg1: T1, callback: (err: Error, result: TResult) => void) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise +): (arg1: T1, arg2: T2, callback: (err: Error) => void) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise +): ( + arg1: T1, + arg2: T2, + callback: (err: Error | null, result: TResult) => void +) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise +): (arg1: T1, arg2: T2, arg3: T3, callback: (err: Error) => void) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + callback: (err: Error | null, result: TResult) => void +) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + callback: (err: Error) => void +) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + callback: (err: Error | null, result: TResult) => void +) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + callback: (err: Error) => void +) => void; +export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + callback: (err: Error | null, result: TResult) => void +) => void; +export function callbackify( + fn: ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6 + ) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6, + callback: (err: Error) => void +) => void; +export function callbackify( + fn: ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6 + ) => Promise +): ( + arg1: T1, + arg2: T2, + arg3: T3, + arg4: T4, + arg5: T5, + arg6: T6, + callback: (err: Error | null, result: TResult) => void +) => void; +export function callbackify Promise>( + original: T +): T extends (...args: infer TArgs) => Promise + ? (...params: [...TArgs, (err: Error, ret: TReturn) => unknown]) => void + : never { + validateFunction(original, 'original'); + + function callbackified( + this: unknown, + ...args: [...unknown[], (err: unknown, ret: unknown) => void] + ): void { + const maybeCb = args.pop(); + validateFunction(maybeCb, 'last argument'); + const cb = maybeCb.bind(this); + Reflect.apply(original, this, args).then( + (ret: unknown) => { + queueMicrotask(() => cb(null, ret)); + }, + (rej: unknown) => { + queueMicrotask(() => { + callbackifyOnRejected(rej, cb); + }); + } + ); + } + + const descriptors = Object.getOwnPropertyDescriptors(original); + if (typeof descriptors.length?.value === 'number') { + descriptors.length.value++; + } + if (typeof descriptors.name?.value === 'string') { + descriptors.name.value += 'Callbackified'; + } + const propertiesValues = Object.values(descriptors); + for (let i = 0; i < propertiesValues.length; i++) { + Object.setPrototypeOf(propertiesValues[i], null); + } + Object.defineProperties(callbackified, descriptors); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return callbackified; +} diff --git a/src/node/util.ts b/src/node/util.ts index ca12e08f2c5..58e7d54bc45 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -19,10 +19,7 @@ import { debuglog } from 'node-internal:debuglog'; export const debug = debuglog; export { debuglog }; -import { - ERR_FALSY_VALUE_REJECTION, - ERR_INVALID_ARG_TYPE, -} from 'node-internal:internal_errors'; +import { ERR_INVALID_ARG_TYPE } from 'node-internal:internal_errors'; import { inspect, @@ -30,54 +27,14 @@ import { formatWithOptions, stripVTControlCharacters, } from 'node-internal:internal_inspect'; -export { inspect, format, formatWithOptions, stripVTControlCharacters }; +import { callbackify } from 'node-internal:internal_utils'; +export { inspect, format, formatWithOptions, stripVTControlCharacters }; +export { callbackify } from 'node-internal:internal_utils'; export const types = internalTypes; export const { MIMEParams, MIMEType } = utilImpl; -const callbackifyOnRejected = (reason: unknown, cb: Function) => { - if (!reason) { - reason = new ERR_FALSY_VALUE_REJECTION(`${reason}`); - } - return cb(reason); -}; - -export function callbackify Promise>( - original: T -): T extends (...args: infer TArgs) => Promise - ? (...params: [...TArgs, (err: Error, ret: TReturn) => any]) => void - : never { - validateFunction(original, 'original'); - - function callbackified( - this: unknown, - ...args: [...unknown[], (err: unknown, ret: unknown) => void] - ): any { - const maybeCb = args.pop(); - validateFunction(maybeCb, 'last argument'); - const cb = (maybeCb as Function).bind(this); - Reflect.apply(original, this, args).then( - (ret: any) => queueMicrotask(() => cb(null, ret)), - (rej: any) => queueMicrotask(() => callbackifyOnRejected(rej, cb)) - ); - } - - const descriptors = Object.getOwnPropertyDescriptors(original); - if (typeof descriptors['length']!.value === 'number') { - descriptors['length']!.value++; - } - if (typeof descriptors['name']!.value === 'string') { - descriptors['name']!.value += 'Callbackified'; - } - const propertiesValues = Object.values(descriptors); - for (let i = 0; i < propertiesValues.length; i++) { - Object.setPrototypeOf(propertiesValues[i], null); - } - Object.defineProperties(callbackified, descriptors); - return callbackified as any; -} - const kCustomPromisifiedSymbol = Symbol.for('nodejs.util.promisify.custom'); const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs'); diff --git a/src/rust/dns/BUILD.bazel b/src/rust/dns/BUILD.bazel new file mode 100644 index 00000000000..0489a4814a6 --- /dev/null +++ b/src/rust/dns/BUILD.bazel @@ -0,0 +1,13 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +wd_rust_crate( + name = "dns", + cxx_bridge_deps = [], + cxx_bridge_src = "lib.rs", + test_deps = [], + visibility = ["//visibility:public"], + deps = [ + "//src/rust/cxx-integration", + "@crates_vendor//:thiserror", + ], +) diff --git a/src/rust/dns/lib.rs b/src/rust/dns/lib.rs new file mode 100644 index 00000000000..ff343181455 --- /dev/null +++ b/src/rust/dns/lib.rs @@ -0,0 +1,204 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DnsParserError { + #[error("Invalid hex string: {0}")] + InvalidHexString(String), + #[error("ParseInt error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), + #[error("Invalid DNS response: {0}")] + InvalidDnsResponse(String), + #[error("unknown dns parser error")] + Unknown, +} + +#[cxx::bridge(namespace = "workerd::rust::dns")] +mod ffi { + /// CAA record representation + struct CaaRecord { + critical: u8, + field: String, + value: String, + } + /// NAPTR record representation + struct NaptrRecord { + flags: String, + service: String, + regexp: String, + replacement: String, + order: u32, + preference: u32, + } + extern "Rust" { + fn parse_caa_record(record: &str) -> Result; + fn parse_naptr_record(record: &str) -> Result; + } +} + +/// Given a vector of strings, converts each slice to UTF-8 from HEX. +/// +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn decode_hex(input: &[&str]) -> Result, DnsParserError> { + let mut v = Vec::with_capacity(input.len()); + + for slice in input { + let num = u16::from_str_radix(slice, 16)?; + let ch = String::from_utf16(&[num]) + .map_err(|_| DnsParserError::InvalidHexString("Invalid UTF-16 sequence".to_owned()))?; + v.push(ch); + } + + Ok(v) +} + +/// Parses an unknown RR format returned from Cloudflare DNS. +/// Specification is available at +/// `` +/// +/// The format of the record is as follows: +/// \# +/// \\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67 +/// | | | | +/// | | | - Starting point of the actual data +/// | | - Length of the field. +/// | - Number representation of "`is_critical`" +/// - Length of the data +/// +/// Note: Field can be "issuewild", "issue" or "iodef". +/// +/// ``` +/// let record = parse_caa_record("\\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67"); +/// assert_eq!(record.critical, false); +/// assert_eq!(record.field, "issue") +/// assert_eq!(record.value, "pki.goog") +/// ``` +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn parse_caa_record(record: &str) -> Result { + // Let's remove "\\#" and the length of data from the beginning of the record + let data = record.split_ascii_whitespace().collect::>()[2..].to_vec(); + let critical = data[0].parse::()?; + let prefix_length = data[1].parse::()?; + + let field = decode_hex(&data[2..prefix_length + 2])?.join(""); + let value = decode_hex(&data[(prefix_length + 2)..])?.join(""); + + // Field can be "issuewild", "issue" or "iodef" + if field != "issuewild" && field != "issue" && field != "iodef" { + return Err(DnsParserError::InvalidDnsResponse(format!( + "Received unknown field '{field}'" + ))); + } + + Ok(ffi::CaaRecord { + critical, + field, + value, + }) +} + +/// Parses an unknown RR format returned from Cloudflare DNS. +/// Specification is available at +/// `` +/// +/// The format of the record is as follows: +/// \# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00 +/// |--| |--| | | | |--------------------------| | |--------------| | |--------------------------------| +/// | | | | | | | | | - Replacement +/// | | | | | | | | - Length of first part of the replacement +/// | | | | | | | - Regexp +/// | | | | | | - Regexp length +/// | | | | | - Service +/// | | | | - Length of service +/// | | | - Flag +/// | | - Length of flags +/// | - Preference +/// - Order +/// +/// ``` +/// let record = parse_naptr_record("\\# 37 15 b3 08 ae 01 73 0a 6d 79 2d 73 65 72 76 69 63 65 06 72 65 67 65 78 70 0b 72 65 70 6c 61 63 65 6d 65 6e 74 00"); +/// assert_eq!(record.flags, "s"); +/// assert_eq!(record.service, "my-service"); +/// assert_eq!(record.regexp, "regexp"); +/// assert_eq!(record.replacement, "replacement"); +/// assert_eq!(record.order, 5555); +/// assert_eq!(record.preference, 2222); +/// ``` +/// +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn parse_naptr_record(record: &str) -> Result { + let data = record.split_ascii_whitespace().collect::>()[1..].to_vec(); + + let order_str = data[1..3].to_vec(); + let order = u32::from_str_radix(&order_str.join(""), 16)?; + let preference_str = data[3..5].to_vec(); + let preference = u32::from_str_radix(&preference_str.join(""), 16)?; + + let flag_length = usize::from_str_radix(data[5], 16)?; + let flag_offset = 6; + let flags = decode_hex(&data[flag_offset..flag_length + flag_offset])?.join(""); + + let service_length = usize::from_str_radix(data[flag_offset + flag_length], 16)?; + let service_offset = flag_offset + flag_length + 1; + let service = decode_hex(&data[service_offset..service_length + service_offset])?.join(""); + + let regexp_length = usize::from_str_radix(data[service_offset + service_length], 16)?; + let regexp_offset = service_offset + service_length + 1; + let regexp = decode_hex(&data[regexp_offset..regexp_length + regexp_offset])?.join(""); + + let replacement = parse_replacement(&data[regexp_offset + regexp_length..])?; + + Ok(ffi::NaptrRecord { + flags, + service, + regexp, + replacement, + order, + preference, + }) +} + +/// Replacement values needs to be parsed accordingly. +/// It has a similar characteristic to CAA and NAPTR records whereas +/// first character contains the length of the input, and the second character +/// is the starting index of the substring. We need to continue parsing until there +/// are no input left, and later join them using "." +/// +/// It is important that the returning value doesn't end with dot (".") character. +/// +/// # Errors +/// `DnsParserError::InvalidHexString` +/// `DnsParserError::ParseIntError` +pub fn parse_replacement(input: &[&str]) -> Result { + if input.is_empty() { + return Ok(String::new()); + } + + let mut output: Vec = vec![]; + let mut length_index = 0; + let mut offset_index = 1; + + // Iterate through each character to parse different frames. + // Each frame starts with the length of the remaining frame. + while length_index < input.len() { + let length = usize::from_str_radix(input[length_index], 16)?; + let subset = input[offset_index..length + offset_index].to_vec(); + let decoded = decode_hex(&subset)?.join(""); + + // We omit the trailing "." from replacements. + // Cloudflare DNS returns "_sip._udp.sip2sip.info." whereas Node.js removes trailing dot + if !decoded.is_empty() { + output.push(decoded); + } + + length_index += subset.len() + 1; + offset_index = length_index + 1; + } + + Ok(output.join(".")) +} diff --git a/src/workerd/api/node/BUILD.bazel b/src/workerd/api/node/BUILD.bazel index 7b2c0869962..93e7ce90211 100644 --- a/src/workerd/api/node/BUILD.bazel +++ b/src/workerd/api/node/BUILD.bazel @@ -20,6 +20,8 @@ wd_cc_library( "zlib-util.h", ], implementation_deps = [ + "//src/rust/cxx-integration", + "//src/rust/dns", "//src/workerd/io", "@capnp-cpp//src/kj/compat:kj-gzip", ], diff --git a/src/workerd/api/node/dns.c++ b/src/workerd/api/node/dns.c++ index 51eb1aa1a0a..05acd472fbc 100644 --- a/src/workerd/api/node/dns.c++ +++ b/src/workerd/api/node/dns.c++ @@ -3,92 +3,28 @@ // https://opensource.org/licenses/Apache-2.0 #include "dns.h" -namespace workerd::api::node { -kj::Array DnsUtil::getServers(jsg::Lock& js) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::lookup(jsg::Lock& js, - kj::String hostname, - jsg::Optional options, - LookupCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::lookupService( - jsg::Lock& js, kj::String address, kj::uint port, LookupServiceCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolve( - jsg::Lock& js, kj::String hostname, kj::String rrtype, ResolveCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolve4( - jsg::Lock& js, kj::String hostname, Resolve4Options options, Resolve6Callback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolve6( - jsg::Lock& js, kj::String hostname, Resolve6Options options, Resolve6Callback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveAny(jsg::Lock& js, kj::String hostname, ResolveAnyCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveCname(jsg::Lock& js, kj::String hostname, ResolveCnameCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveCaa(jsg::Lock& js, kj::String hostname, ResolveCaaCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveMx(jsg::Lock& js, kj::String hostname, ResolveMxCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveNaptr(jsg::Lock& js, kj::String hostname, ResolveNaptrCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveNs(jsg::Lock& js, kj::String hostname, ResolveNsCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} +#include +#include +#include -void DnsUtil::resolvePtr(jsg::Lock& js, kj::String hostname, ResolvePtrCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveSoa(jsg::Lock& js, kj::String hostname, ResolveSoaCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveSrv(jsg::Lock& js, kj::String hostname, ResolveSrvCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::resolveTxt(jsg::Lock& js, kj::String hostname, ResolveTxtCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::reverse(jsg::Lock& js, kj::String ip, ReverseCallback callback) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -void DnsUtil::setDefaultResultOrder(jsg::Lock& js, kj::String order) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} - -kj::StringPtr DnsUtil::getDefaultResultOrder(jsg::Lock& js) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); -} +namespace workerd::api::node { -void DnsUtil::setServers(kj::Array servers) { - JSG_FAIL_REQUIRE(Error, "Not implemented"_kj); +DnsUtil::CaaRecord DnsUtil::parseCaaRecord(kj::String record) { + auto parsed = rust::dns::parse_caa_record(::rust::Str(record.begin(), record.size())); + return CaaRecord{ + .critical = parsed.critical, .field = kj::str(parsed.field), .value = kj::str(parsed.value)}; +} + +DnsUtil::NaptrRecord DnsUtil::parseNaptrRecord(kj::String record) { + auto parsed = rust::dns::parse_naptr_record(::rust::Str(record.begin(), record.size())); + return NaptrRecord{ + .flags = kj::str(parsed.flags), + .service = kj::str(parsed.service), + .regexp = kj::str(parsed.regexp), + .replacement = kj::str(parsed.replacement), + .order = parsed.order, + .preference = parsed.preference, + }; } } // namespace workerd::api::node diff --git a/src/workerd/api/node/dns.h b/src/workerd/api/node/dns.h index 5f9139f1c1b..33207254e65 100644 --- a/src/workerd/api/node/dns.h +++ b/src/workerd/api/node/dns.h @@ -7,6 +7,8 @@ #include +#include + namespace workerd::api::node { class DnsUtil final: public jsg::Object { @@ -14,114 +16,36 @@ class DnsUtil final: public jsg::Object { DnsUtil() = default; DnsUtil(jsg::Lock&, const jsg::Url&) {} - kj::Array getServers(jsg::Lock& js); - - struct LookupOptions { - kj::OneOf family = static_cast(0); - kj::uint hints; - bool all = false; - jsg::Optional order; - bool verbatim = true; - - JSG_STRUCT(family, hints, all, order, verbatim); - }; - using LookupCallback = jsg::Function; - void lookup(jsg::Lock& js, - kj::String hostname, - jsg::Optional options, - LookupCallback callback); - - using LookupServiceCallback = jsg::Function; - void lookupService( - jsg::Lock& js, kj::String address, kj::uint port, LookupServiceCallback callback); + // TODO: Remove this once we can expose Rust structs + struct CaaRecord { + uint8_t critical; + kj::String field; + kj::String value; - using ResolveCallback = jsg::Function; - void resolve(jsg::Lock& js, kj::String hostname, kj::String rrtype, ResolveCallback callback); - - struct Resolve4Options { - bool ttl; - - JSG_STRUCT(ttl); + JSG_STRUCT(critical, field, value); }; - using Resolve4Callback = jsg::Function; - void resolve4( - jsg::Lock& js, kj::String hostname, Resolve4Options options, Resolve4Callback callback); - struct Resolve6Options { - bool ttl; + struct NaptrRecord { + kj::String flags; + kj::String service; + kj::String regexp; + kj::String replacement; + uint32_t order; + uint32_t preference; - JSG_STRUCT(ttl); + JSG_STRUCT(flags, service, regexp, replacement, order, preference); }; - using Resolve6Callback = jsg::Function; - void resolve6( - jsg::Lock& js, kj::String hostname, Resolve6Options options, Resolve6Callback callback); - - using ResolveAnyCallback = jsg::Function; - void resolveAny(jsg::Lock& js, kj::String hostname, ResolveAnyCallback callback); - - using ResolveCnameCallback = jsg::Function; - void resolveCname(jsg::Lock& js, kj::String hostname, ResolveCnameCallback callback); - - using ResolveCaaCallback = jsg::Function; - void resolveCaa(jsg::Lock& js, kj::String hostname, ResolveCaaCallback callback); - using ResolveMxCallback = jsg::Function; - void resolveMx(jsg::Lock& js, kj::String hostname, ResolveMxCallback callback); - - using ResolveNaptrCallback = jsg::Function; - void resolveNaptr(jsg::Lock& js, kj::String hostname, ResolveNaptrCallback callback); - - using ResolveNsCallback = jsg::Function; - void resolveNs(jsg::Lock& js, kj::String hostname, ResolveNsCallback callback); - - using ResolvePtrCallback = jsg::Function; - void resolvePtr(jsg::Lock& js, kj::String hostname, ResolvePtrCallback callback); - - using ResolveSoaCallback = jsg::Function; - void resolveSoa(jsg::Lock& js, kj::String hostname, ResolveSoaCallback callback); - - using ResolveSrvCallback = jsg::Function; - void resolveSrv(jsg::Lock& js, kj::String hostname, ResolveSrvCallback callback); - - using ResolveTxtCallback = jsg::Function; - void resolveTxt(jsg::Lock& js, kj::String hostname, ResolveTxtCallback callback); - - using ReverseCallback = jsg::Function; - void reverse(jsg::Lock& js, kj::String ip, ReverseCallback callback); - - void setDefaultResultOrder(jsg::Lock& js, kj::String order); - kj::StringPtr getDefaultResultOrder(jsg::Lock& js); - void setServers(kj::Array servers); + CaaRecord parseCaaRecord(kj::String record); + NaptrRecord parseNaptrRecord(kj::String record); JSG_RESOURCE_TYPE(DnsUtil) { - // Callback implementations - JSG_METHOD(getServers); - JSG_METHOD(lookup); - JSG_METHOD(lookupService); - JSG_METHOD(resolve); - JSG_METHOD(resolve4); - JSG_METHOD(resolve6); - JSG_METHOD(resolveAny); - JSG_METHOD(resolveCname); - JSG_METHOD(resolveCaa); - JSG_METHOD(resolveMx); - JSG_METHOD(resolveNaptr); - JSG_METHOD(resolveNs); - JSG_METHOD(resolvePtr); - JSG_METHOD(resolveSoa); - JSG_METHOD(resolveSrv); - JSG_METHOD(resolveTxt); - JSG_METHOD(reverse); - - // Getter, setters - JSG_METHOD(setDefaultResultOrder); - JSG_METHOD(getDefaultResultOrder); - JSG_METHOD(setServers); + JSG_METHOD(parseCaaRecord); + JSG_METHOD(parseNaptrRecord); } }; #define EW_NODE_DNS_ISOLATE_TYPES \ - api::node::DnsUtil, api::node::DnsUtil::LookupOptions, api::node::DnsUtil::Resolve4Options, \ - api::node::DnsUtil::Resolve6Options + api::node::DnsUtil, api::node::DnsUtil::CaaRecord, api::node::DnsUtil::NaptrRecord } // namespace workerd::api::node diff --git a/src/workerd/api/node/tests/dns-nodejs-test.js b/src/workerd/api/node/tests/dns-nodejs-test.js index b0479f32b01..f30ebb685c0 100644 --- a/src/workerd/api/node/tests/dns-nodejs-test.js +++ b/src/workerd/api/node/tests/dns-nodejs-test.js @@ -1,6 +1,57 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// Copyright Joyent and Node contributors. All rights reserved. MIT license. import dns from 'node:dns'; import dnsPromises from 'node:dns/promises'; -import { strictEqual } from 'node:assert'; +import { strictEqual, ok, deepStrictEqual } from 'node:assert'; +import { inspect } from 'node:util'; + +// Taken from Node.js +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/common/internet.js +const addresses = { + // A generic host that has registered common DNS records, + // supports both IPv4 and IPv6, and provides basic HTTP/HTTPS services + INET_HOST: 'nodejs.org', + // A host that provides IPv4 services + INET4_HOST: 'nodejs.org', + // A host that provides IPv6 services + INET6_HOST: 'nodejs.org', + // An accessible IPv4 IP, + // defaults to the Google Public DNS IPv4 address + INET4_IP: '8.8.8.8', + // An accessible IPv6 IP, + // defaults to the Google Public DNS IPv6 address + INET6_IP: '2001:4860:4860::8888', + // An invalid host that cannot be resolved + // See https://tools.ietf.org/html/rfc2606#section-2 + INVALID_HOST: 'something.invalid', + // A host with MX records registered + MX_HOST: 'nodejs.org', + // On some systems, .invalid returns a server failure/try again rather than + // record not found. Use this to guarantee record not found. + NOT_FOUND: 'come.on.fhqwhgads.test', + // A host with SRV records registered + SRV_HOST: '_caldav._tcp.google.com', + // A host with PTR records registered + PTR_HOST: '8.8.8.8.in-addr.arpa', + // A host with NAPTR records registered + NAPTR_HOST: 'sip2sip.info', + // A host with SOA records registered + SOA_HOST: 'nodejs.org', + // A host with CAA record registered + CAA_HOST: 'google.com', + // A host with CNAME records registered + CNAME_HOST: 'blog.nodejs.org', + // A host with NS records registered + NS_HOST: 'nodejs.org', + // A host with TXT records registered + TXT_HOST: 'nodejs.org', + // An accessible IPv4 DNS server + DNS4_SERVER: '8.8.8.8', + // An accessible IPv4 DNS server + DNS6_SERVER: '2001:4860:4860::8888', +}; export const functionsExist = { async test() { @@ -19,7 +70,6 @@ export const functionsExist = { 'resolvePtr', 'resolveSoa', 'resolveSrv', - 'reverse', 'setDefaultResultOrder', 'getDefaultResultOrder', 'setServers', @@ -37,3 +87,278 @@ export const errorCodesExist = { strictEqual(typeof dnsPromises.NODATA, 'string'); }, }; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/internet/test-dns.js#L483 +export const resolveTxt = { + async test() { + function validateResult(result) { + ok(Array.isArray(result[0])); + strictEqual(result.length, 1); + ok(result[0][0].startsWith('v=spf1')); + } + + validateResult(await dnsPromises.resolveTxt(addresses.TXT_HOST)); + + { + // Callback API + const { promise, resolve, reject } = Promise.withResolvers(); + dns.resolveTxt(addresses.TXT_HOST, (error, results) => { + if (error) { + reject(error); + return; + } + validateResult(results); + resolve(); + }); + await promise; + } + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/internet/test-dns.js#L402 +export const resolveCaa = { + async test() { + function validateResult(result) { + ok(Array.isArray(result), `expected array, got ${inspect(result)}`); + strictEqual(result.length, 1); + strictEqual(typeof result[0].critical, 'number'); + strictEqual(result[0].critical, 0); + strictEqual(result[0].issue, 'pki.goog'); + } + + validateResult(await dnsPromises.resolveCaa(addresses.CAA_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/internet/test-dns.js#L142 +export const resolveMx = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'object'); + ok(item.exchange); + strictEqual(typeof item.exchange, 'string'); + strictEqual(typeof item.priority, 'number'); + } + } + validateResult(await dnsPromises.resolveMx(addresses.MX_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L442 +export const resolveCname = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + ok(item); + strictEqual(typeof item, 'string'); + } + } + + validateResult(await dnsPromises.resolveCname(addresses.CNAME_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L184 +export const resolveNs = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + ok(item); + strictEqual(typeof item, 'string'); + } + } + + validateResult(await dnsPromises.resolveNs(addresses.NS_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L268 +export const resolvePtr = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + ok(item); + strictEqual(typeof item, 'string'); + } + } + + validateResult(await dnsPromises.resolvePtr(addresses.PTR_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L224 +export const resolveSrv = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'object'); + ok(item.name); + strictEqual(typeof item.name, 'string'); + strictEqual(typeof item.port, 'number'); + strictEqual(typeof item.priority, 'number'); + strictEqual(typeof item.weight, 'number'); + } + } + + validateResult(await dnsPromises.resolveSrv(addresses.SRV_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L353 +export const resolveSoa = { + async test() { + function validateResult(result) { + strictEqual(typeof result, 'object'); + strictEqual(typeof result.nsname, 'string'); + ok(result.nsname.length > 0); + strictEqual(typeof result.hostmaster, 'string'); + ok(result.hostmaster.length > 0); + strictEqual(typeof result.serial, 'number'); + ok(result.serial > 0 && result.serial < 4294967295); + strictEqual(typeof result.refresh, 'number'); + ok(result.refresh > 0 && result.refresh < 2147483647); + strictEqual(typeof result.retry, 'number'); + ok(result.retry > 0 && result.retry < 2147483647); + strictEqual(typeof result.expire, 'number'); + ok(result.expire > 0 && result.expire < 2147483647); + strictEqual(typeof result.minttl, 'number'); + ok(result.minttl >= 0 && result.minttl < 2147483647); + } + + validateResult(await dnsPromises.resolveSoa(addresses.SOA_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d7fdbb994cda8b2e1da4240eb97270c6abbaa9dd/test/internet/test-dns.js#L308 +export const resolveNaptr = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'object'); + strictEqual(typeof item.flags, 'string'); + strictEqual(typeof item.service, 'string'); + strictEqual(typeof item.regexp, 'string'); + strictEqual(typeof item.replacement, 'string'); + strictEqual(typeof item.order, 'number'); + strictEqual(typeof item.preference, 'number'); + } + } + + validateResult(await dnsPromises.resolveNaptr(addresses.NAPTR_HOST)); + }, +}; + +export const resolve4 = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'string'); + // TODO(soon): Validate IPv4 + // ok(isIPv4(item)); + } + } + + validateResult(await dnsPromises.resolve4(addresses.INET4_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/internet/test-dns.js#L86 +export const resolve4TTL = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'object'); + strictEqual(typeof item.ttl, 'number'); + strictEqual(typeof item.address, 'string'); + ok(item.ttl >= 0); + // TODO(soon): Validate IPv4 + // ok(isIPv4(item.address)); + } + } + + validateResult( + await dnsPromises.resolve4(addresses.INET4_HOST, { + ttl: true, + }) + ); + }, +}; + +export const resolve6 = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'string'); + // TODO(soon): Validate IPv6 + // ok(isIPv6(item)); + } + } + + validateResult(await dnsPromises.resolve6(addresses.INET6_HOST)); + }, +}; + +// Tests are taken from +// https://github.com/nodejs/node/blob/d5d1e80763202ffa73307213211148571deac27c/test/internet/test-dns.js#L114 +export const resolve6TTL = { + async test() { + function validateResult(result) { + ok(result.length > 0); + + for (const item of result) { + strictEqual(typeof item, 'object'); + strictEqual(typeof item.ttl, 'number'); + strictEqual(typeof item.address, 'string'); + ok(item.ttl >= 0); + // TODO(soon): Validate IPv6 + // ok(isIPv6(item.address)); + } + } + + validateResult( + await dnsPromises.resolve6(addresses.INET6_HOST, { + ttl: true, + }) + ); + }, +}; + +export const getServers = { + async test() { + deepStrictEqual(await dnsPromises.getServers(), [ + '1.1.1.1', + '2606:4700:4700::1111', + '1.0.0.1', + '2606:4700:4700::1001', + ]); + }, +};