1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-04 17:18:23 -05:00
denoland-deno/ext/node/polyfills/internal/dns/utils.ts
Yoshiya Hinosawa d59599fc18
fix(ext/node): fix dns.lookup result ordering (#26264)
partially unblocks #25470

This PR aligns the resolution of `localhost` hostname to Node.js
behavior.

In Node.js `dns.lookup("localhost", (_, addr) => console.log(addr))`
prints ipv6 address `::1`, but it prints ipv4 address `127.0.0.1` in
Deno. That difference causes some errors in the work of enabling
`createConnection` option in `http.request` (#25470). This PR fixes the
issue by aligning `dns.lookup` behavior to Node.js.

This PR also changes the following behaviors (resolving TODOs):
- `http.createServer` now listens on ipv6 address `[::]` by default on
linux/mac
- `net.createServer` now listens on ipv6 address `[::]` by default on
linux/mac

These changes are also alignments to Node.js behaviors.
2024-10-16 20:58:44 +09:00

449 lines
11 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { getOptionValue } from "ext:deno_node/internal/options.ts";
import { emitWarning } from "node:process";
import {
AI_ADDRCONFIG,
AI_ALL,
AI_V4MAPPED,
} from "ext:deno_node/internal_binding/ares.ts";
import {
ChannelWrap,
strerror,
} from "ext:deno_node/internal_binding/cares_wrap.ts";
import {
ERR_DNS_SET_SERVERS_FAILED,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_IP_ADDRESS,
} from "ext:deno_node/internal/errors.ts";
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
import {
validateArray,
validateInt32,
validateOneOf,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import { isIP } from "ext:deno_node/internal/net.ts";
export interface LookupOptions {
family?: number | undefined;
hints?: number | undefined;
all?: boolean | undefined;
verbatim?: boolean | undefined;
}
export interface LookupOneOptions extends LookupOptions {
all?: false | undefined;
}
export interface LookupAllOptions extends LookupOptions {
all: true;
}
export interface LookupAddress {
address: string | null;
family: number;
}
export function isLookupOptions(
options: unknown,
): options is LookupOptions | undefined {
return typeof options === "object" || typeof options === "undefined";
}
export function isLookupCallback(
options: unknown,
): options is (...args: unknown[]) => void {
return typeof options === "function";
}
export function isFamily(options: unknown): options is number {
return typeof options === "number";
}
export interface ResolveOptions {
ttl?: boolean;
}
export interface ResolveWithTtlOptions extends ResolveOptions {
ttl: true;
}
export interface RecordWithTtl {
address: string;
ttl: number;
}
export interface AnyARecord extends RecordWithTtl {
type: "A";
}
export interface AnyAaaaRecord extends RecordWithTtl {
type: "AAAA";
}
export interface CaaRecord {
critial: number;
issue?: string | undefined;
issuewild?: string | undefined;
iodef?: string | undefined;
contactemail?: string | undefined;
contactphone?: string | undefined;
}
export interface MxRecord {
priority: number;
exchange: string;
}
export interface AnyMxRecord extends MxRecord {
type: "MX";
}
export interface NaptrRecord {
flags: string;
service: string;
regexp: string;
replacement: string;
order: number;
preference: number;
}
export interface AnyNaptrRecord extends NaptrRecord {
type: "NAPTR";
}
export interface SoaRecord {
nsname: string;
hostmaster: string;
serial: number;
refresh: number;
retry: number;
expire: number;
minttl: number;
}
export interface AnySoaRecord extends SoaRecord {
type: "SOA";
}
export interface SrvRecord {
priority: number;
weight: number;
port: number;
name: string;
}
export interface AnySrvRecord extends SrvRecord {
type: "SRV";
}
export interface AnyTxtRecord {
type: "TXT";
entries: string[];
}
export interface AnyNsRecord {
type: "NS";
value: string;
}
export interface AnyPtrRecord {
type: "PTR";
value: string;
}
export interface AnyCnameRecord {
type: "CNAME";
value: string;
}
export type AnyRecord =
| AnyARecord
| AnyAaaaRecord
| AnyCnameRecord
| AnyMxRecord
| AnyNaptrRecord
| AnyNsRecord
| AnyPtrRecord
| AnySoaRecord
| AnySrvRecord
| AnyTxtRecord;
export type Records =
| string[]
| AnyRecord[]
| MxRecord[]
| NaptrRecord[]
| SoaRecord
| SrvRecord[]
| string[];
export type ResolveCallback = (
err: ErrnoException | null,
addresses: Records,
) => void;
export function isResolveCallback(
callback: unknown,
): callback is ResolveCallback {
return typeof callback === "function";
}
const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;
export function validateTimeout(options?: { timeout?: number }) {
const { timeout = -1 } = { ...options };
validateInt32(timeout, "options.timeout", -1, 2 ** 31 - 1);
return timeout;
}
export function validateTries(options?: { tries?: number }) {
const { tries = 4 } = { ...options };
validateInt32(tries, "options.tries", 1, 2 ** 31 - 1);
return tries;
}
export interface ResolverOptions {
timeout?: number | undefined;
/**
* @default 4
*/
tries?: number;
}
/**
* An independent resolver for DNS requests.
*
* Creating a new resolver uses the default server settings. Setting
* the servers used for a resolver using `resolver.setServers()` does not affect
* other resolvers:
*
* ```js
* const { Resolver } = require('dns');
* const resolver = new Resolver();
* resolver.setServers(['4.4.4.4']);
*
* // This request will use the server at 4.4.4.4, independent of global settings.
* resolver.resolve4('example.org', (err, addresses) => {
* // ...
* });
* ```
*
* The following methods from the `dns` module are available:
*
* - `resolver.getServers()`
* - `resolver.resolve()`
* - `resolver.resolve4()`
* - `resolver.resolve6()`
* - `resolver.resolveAny()`
* - `resolver.resolveCaa()`
* - `resolver.resolveCname()`
* - `resolver.resolveMx()`
* - `resolver.resolveNaptr()`
* - `resolver.resolveNs()`
* - `resolver.resolvePtr()`
* - `resolver.resolveSoa()`
* - `resolver.resolveSrv()`
* - `resolver.resolveTxt()`
* - `resolver.reverse()`
* - `resolver.setServers()`
*/
export class Resolver {
_handle!: ChannelWrap;
constructor(options?: ResolverOptions) {
const timeout = validateTimeout(options);
const tries = validateTries(options);
this._handle = new ChannelWrap(timeout, tries);
}
cancel() {
this._handle.cancel();
}
getServers(): string[] {
return this._handle.getServers().map((val: [string, number]) => {
if (!val[1] || val[1] === IANA_DNS_PORT) {
return val[0];
}
const host = isIP(val[0]) === 6 ? `[${val[0]}]` : val[0];
return `${host}:${val[1]}`;
});
}
setServers(servers: ReadonlyArray<string>) {
validateArray(servers, "servers");
// Cache the original servers because in the event of an error while
// setting the servers, c-ares won't have any servers available for
// resolution.
const orig = this._handle.getServers();
const newSet: [number, string, number][] = [];
servers.forEach((serv, index) => {
validateString(serv, `servers[${index}]`);
let ipVersion = isIP(serv);
if (ipVersion !== 0) {
return newSet.push([ipVersion, serv, IANA_DNS_PORT]);
}
const match = serv.match(IPv6RE);
// Check for an IPv6 in brackets.
if (match) {
ipVersion = isIP(match[1]);
if (ipVersion !== 0) {
const port = Number.parseInt(serv.replace(addrSplitRE, "$2")) ||
IANA_DNS_PORT;
return newSet.push([ipVersion, match[1], port]);
}
}
// addr::port
const addrSplitMatch = serv.match(addrSplitRE);
if (addrSplitMatch) {
const hostIP = addrSplitMatch[1];
const port = addrSplitMatch[2] || `${IANA_DNS_PORT}`;
ipVersion = isIP(hostIP);
if (ipVersion !== 0) {
return newSet.push([ipVersion, hostIP, Number.parseInt(port)]);
}
}
throw new ERR_INVALID_IP_ADDRESS(serv);
});
const errorNumber = this._handle.setServers(newSet);
if (errorNumber !== 0) {
// Reset the servers to the old servers, because ares probably unset them.
this._handle.setServers(orig.join(","));
const err = strerror(errorNumber);
throw new ERR_DNS_SET_SERVERS_FAILED(err, servers.toString());
}
}
/**
* The resolver instance will send its requests from the specified IP address.
* This allows programs to specify outbound interfaces when used on multi-homed
* systems.
*
* If a v4 or v6 address is not specified, it is set to the default, and the
* operating system will choose a local address automatically.
*
* The resolver will use the v4 local address when making requests to IPv4 DNS
* servers, and the v6 local address when making requests to IPv6 DNS servers.
* The `rrtype` of resolution requests has no impact on the local address used.
*
* @param [ipv4='0.0.0.0'] A string representation of an IPv4 address.
* @param [ipv6='::0'] A string representation of an IPv6 address.
*/
setLocalAddress(ipv4: string, ipv6?: string) {
validateString(ipv4, "ipv4");
if (ipv6 !== undefined) {
validateString(ipv6, "ipv6");
}
this._handle.setLocalAddress(ipv4, ipv6);
}
}
let defaultResolver = new Resolver();
export function getDefaultResolver(): Resolver {
return defaultResolver;
}
export function setDefaultResolver<T extends Resolver>(resolver: T) {
defaultResolver = resolver;
}
export function validateHints(hints: number) {
if ((hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0) {
throw new ERR_INVALID_ARG_VALUE("hints", hints, "is invalid");
}
}
let invalidHostnameWarningEmitted = false;
export function emitInvalidHostnameWarning(hostname: string) {
if (invalidHostnameWarningEmitted) {
return;
}
invalidHostnameWarningEmitted = true;
emitWarning(
`The provided hostname "${hostname}" is not a valid ` +
"hostname, and is supported in the dns module solely for compatibility.",
"DeprecationWarning",
"DEP0118",
);
}
let dnsOrder = getOptionValue("--dns-result-order") || "verbatim";
export function getDefaultVerbatim() {
return dnsOrder !== "ipv4first";
}
/**
* Set the default value of `verbatim` in `lookup` and `dnsPromises.lookup()`.
* The value could be:
*
* - `ipv4first`: sets default `verbatim` `false`.
* - `verbatim`: sets default `verbatim` `true`.
*
* The default is `ipv4first` and `setDefaultResultOrder` have higher
* priority than `--dns-result-order`. When using `worker threads`,
* `setDefaultResultOrder` from the main thread won't affect the default
* dns orders in workers.
*
* @param order must be `'ipv4first'` or `'verbatim'`.
*/
export function setDefaultResultOrder(order: "ipv4first" | "verbatim") {
validateOneOf(order, "dnsOrder", ["verbatim", "ipv4first"]);
dnsOrder = order;
}
export function defaultResolverSetServers(servers: string[]) {
const resolver = new Resolver();
resolver.setServers(servers);
setDefaultResolver(resolver);
}