mirror of
https://github.com/denoland/deno.git
synced 2024-11-22 15:06:54 -05:00
d59599fc18
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.
554 lines
15 KiB
TypeScript
554 lines
15 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.
|
|
|
|
// This module ports:
|
|
// - https://github.com/nodejs/node/blob/master/src/cares_wrap.cc
|
|
// - https://github.com/nodejs/node/blob/master/src/cares_wrap.h
|
|
|
|
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
|
// deno-lint-ignore-file prefer-primordials
|
|
|
|
import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
|
|
import { isIPv4 } from "ext:deno_node/internal/net.ts";
|
|
import { codeMap } from "ext:deno_node/internal_binding/uv.ts";
|
|
import {
|
|
AsyncWrap,
|
|
providerType,
|
|
} from "ext:deno_node/internal_binding/async_wrap.ts";
|
|
import { ares_strerror } from "ext:deno_node/internal_binding/ares.ts";
|
|
import { notImplemented } from "ext:deno_node/_utils.ts";
|
|
import { isWindows } from "ext:deno_node/_util/os.ts";
|
|
|
|
interface LookupAddress {
|
|
address: string;
|
|
family: number;
|
|
}
|
|
|
|
export class GetAddrInfoReqWrap extends AsyncWrap {
|
|
family!: number;
|
|
hostname!: string;
|
|
|
|
callback!: (
|
|
err: ErrnoException | null,
|
|
addressOrAddresses?: string | LookupAddress[] | null,
|
|
family?: number,
|
|
) => void;
|
|
resolve!: (addressOrAddresses: LookupAddress | LookupAddress[]) => void;
|
|
reject!: (err: ErrnoException | null) => void;
|
|
oncomplete!: (err: number | null, addresses: string[]) => void;
|
|
|
|
constructor() {
|
|
super(providerType.GETADDRINFOREQWRAP);
|
|
}
|
|
}
|
|
|
|
export function getaddrinfo(
|
|
req: GetAddrInfoReqWrap,
|
|
hostname: string,
|
|
family: number,
|
|
_hints: number,
|
|
verbatim: boolean,
|
|
): number {
|
|
let addresses: string[] = [];
|
|
|
|
// TODO(cmorten): use hints
|
|
// REF: https://nodejs.org/api/dns.html#dns_supported_getaddrinfo_flags
|
|
|
|
const recordTypes: ("A" | "AAAA")[] = [];
|
|
|
|
if (family === 6) {
|
|
recordTypes.push("AAAA");
|
|
} else if (family === 4) {
|
|
recordTypes.push("A");
|
|
} else if (family === 0 && hostname === "localhost") {
|
|
// Ipv6 is preferred over Ipv4 for localhost
|
|
recordTypes.push("AAAA");
|
|
recordTypes.push("A");
|
|
} else if (family === 0) {
|
|
// Only get Ipv4 addresses for the other hostnames
|
|
// This simulates what `getaddrinfo` does when the family is not specified
|
|
recordTypes.push("A");
|
|
}
|
|
|
|
(async () => {
|
|
await Promise.allSettled(
|
|
recordTypes.map((recordType) =>
|
|
Deno.resolveDns(hostname, recordType).then((records) => {
|
|
records.forEach((record) => addresses.push(record));
|
|
})
|
|
),
|
|
);
|
|
|
|
const error = addresses.length ? 0 : codeMap.get("EAI_NODATA")!;
|
|
|
|
// TODO(cmorten): needs work
|
|
// REF: https://github.com/nodejs/node/blob/master/src/cares_wrap.cc#L1444
|
|
if (!verbatim) {
|
|
addresses.sort((a: string, b: string): number => {
|
|
if (isIPv4(a)) {
|
|
return -1;
|
|
} else if (isIPv4(b)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// TODO(@bartlomieju): Forces IPv4 as a workaround for Deno not
|
|
// aligning with Node on implicit binding on Windows
|
|
// REF: https://github.com/denoland/deno/issues/10762
|
|
if (isWindows && hostname === "localhost") {
|
|
addresses = addresses.filter((address) => isIPv4(address));
|
|
}
|
|
|
|
req.oncomplete(error, addresses);
|
|
})();
|
|
|
|
return 0;
|
|
}
|
|
|
|
export class QueryReqWrap extends AsyncWrap {
|
|
bindingName!: string;
|
|
hostname!: string;
|
|
ttl!: boolean;
|
|
|
|
callback!: (
|
|
err: ErrnoException | null,
|
|
// deno-lint-ignore no-explicit-any
|
|
records?: any,
|
|
) => void;
|
|
// deno-lint-ignore no-explicit-any
|
|
resolve!: (records: any) => void;
|
|
reject!: (err: ErrnoException | null) => void;
|
|
oncomplete!: (
|
|
err: number,
|
|
// deno-lint-ignore no-explicit-any
|
|
records: any,
|
|
ttls?: number[],
|
|
) => void;
|
|
|
|
constructor() {
|
|
super(providerType.QUERYWRAP);
|
|
}
|
|
}
|
|
|
|
export interface ChannelWrapQuery {
|
|
queryAny(req: QueryReqWrap, name: string): number;
|
|
queryA(req: QueryReqWrap, name: string): number;
|
|
queryAaaa(req: QueryReqWrap, name: string): number;
|
|
queryCaa(req: QueryReqWrap, name: string): number;
|
|
queryCname(req: QueryReqWrap, name: string): number;
|
|
queryMx(req: QueryReqWrap, name: string): number;
|
|
queryNs(req: QueryReqWrap, name: string): number;
|
|
queryTxt(req: QueryReqWrap, name: string): number;
|
|
querySrv(req: QueryReqWrap, name: string): number;
|
|
queryPtr(req: QueryReqWrap, name: string): number;
|
|
queryNaptr(req: QueryReqWrap, name: string): number;
|
|
querySoa(req: QueryReqWrap, name: string): number;
|
|
getHostByAddr(req: QueryReqWrap, name: string): number;
|
|
}
|
|
|
|
function fqdnToHostname(fqdn: string): string {
|
|
return fqdn.replace(/\.$/, "");
|
|
}
|
|
|
|
function compressIPv6(address: string): string {
|
|
const formatted = address.replace(/\b(?:0+:){2,}/, ":");
|
|
const finalAddress = formatted
|
|
.split(":")
|
|
.map((octet) => {
|
|
if (octet.match(/^\d+\.\d+\.\d+\.\d+$/)) {
|
|
// decimal
|
|
return Number(octet.replaceAll(".", "")).toString(16);
|
|
}
|
|
|
|
return octet.replace(/\b0+/g, "");
|
|
})
|
|
.join(":");
|
|
|
|
return finalAddress;
|
|
}
|
|
|
|
export class ChannelWrap extends AsyncWrap implements ChannelWrapQuery {
|
|
#servers: [string, number][] = [];
|
|
#timeout: number;
|
|
#tries: number;
|
|
|
|
constructor(timeout: number, tries: number) {
|
|
super(providerType.DNSCHANNEL);
|
|
|
|
this.#timeout = timeout;
|
|
this.#tries = tries;
|
|
}
|
|
|
|
async #query(query: string, recordType: Deno.RecordType) {
|
|
// TODO(@bartlomieju): TTL logic.
|
|
|
|
let code: number;
|
|
let ret: Awaited<ReturnType<typeof Deno.resolveDns>>;
|
|
|
|
if (this.#servers.length) {
|
|
for (const [ipAddr, port] of this.#servers) {
|
|
const resolveOptions = {
|
|
nameServer: {
|
|
ipAddr,
|
|
port,
|
|
},
|
|
};
|
|
|
|
({ code, ret } = await this.#resolve(
|
|
query,
|
|
recordType,
|
|
resolveOptions,
|
|
));
|
|
|
|
if (code === 0 || code === codeMap.get("EAI_NODATA")!) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
({ code, ret } = await this.#resolve(query, recordType));
|
|
}
|
|
|
|
return { code: code!, ret: ret! };
|
|
}
|
|
|
|
async #resolve(
|
|
query: string,
|
|
recordType: Deno.RecordType,
|
|
resolveOptions?: Deno.ResolveDnsOptions,
|
|
): Promise<{
|
|
code: number;
|
|
ret: Awaited<ReturnType<typeof Deno.resolveDns>>;
|
|
}> {
|
|
let ret: Awaited<ReturnType<typeof Deno.resolveDns>> = [];
|
|
let code = 0;
|
|
|
|
try {
|
|
ret = await Deno.resolveDns(query, recordType, resolveOptions);
|
|
} catch (e) {
|
|
if (e instanceof Deno.errors.NotFound) {
|
|
code = codeMap.get("EAI_NODATA")!;
|
|
} else {
|
|
// TODO(cmorten): map errors to appropriate error codes.
|
|
code = codeMap.get("UNKNOWN")!;
|
|
}
|
|
}
|
|
|
|
return { code, ret };
|
|
}
|
|
|
|
queryAny(req: QueryReqWrap, name: string): number {
|
|
// TODO(@bartlomieju): implemented temporary measure to allow limited usage of
|
|
// `resolveAny` like APIs.
|
|
//
|
|
// Ideally we move to using the "ANY" / "*" DNS query in future
|
|
// REF: https://github.com/denoland/deno/issues/14492
|
|
(async () => {
|
|
const records: { type: Deno.RecordType; [key: string]: unknown }[] = [];
|
|
|
|
await Promise.allSettled([
|
|
this.#query(name, "A").then(({ ret }) => {
|
|
ret.forEach((record) => records.push({ type: "A", address: record }));
|
|
}),
|
|
this.#query(name, "AAAA").then(({ ret }) => {
|
|
(ret as string[]).forEach((record) =>
|
|
records.push({ type: "AAAA", address: compressIPv6(record) })
|
|
);
|
|
}),
|
|
this.#query(name, "CAA").then(({ ret }) => {
|
|
(ret as Deno.CaaRecord[]).forEach(({ critical, tag, value }) =>
|
|
records.push({
|
|
type: "CAA",
|
|
[tag]: value,
|
|
critical: +critical && 128,
|
|
})
|
|
);
|
|
}),
|
|
this.#query(name, "CNAME").then(({ ret }) => {
|
|
ret.forEach((record) =>
|
|
records.push({ type: "CNAME", value: record })
|
|
);
|
|
}),
|
|
this.#query(name, "MX").then(({ ret }) => {
|
|
(ret as Deno.MxRecord[]).forEach(({ preference, exchange }) =>
|
|
records.push({
|
|
type: "MX",
|
|
priority: preference,
|
|
exchange: fqdnToHostname(exchange),
|
|
})
|
|
);
|
|
}),
|
|
this.#query(name, "NAPTR").then(({ ret }) => {
|
|
(ret as Deno.NaptrRecord[]).forEach(
|
|
({ order, preference, flags, services, regexp, replacement }) =>
|
|
records.push({
|
|
type: "NAPTR",
|
|
order,
|
|
preference,
|
|
flags,
|
|
service: services,
|
|
regexp,
|
|
replacement,
|
|
}),
|
|
);
|
|
}),
|
|
this.#query(name, "NS").then(({ ret }) => {
|
|
(ret as string[]).forEach((record) =>
|
|
records.push({ type: "NS", value: fqdnToHostname(record) })
|
|
);
|
|
}),
|
|
this.#query(name, "PTR").then(({ ret }) => {
|
|
(ret as string[]).forEach((record) =>
|
|
records.push({ type: "PTR", value: fqdnToHostname(record) })
|
|
);
|
|
}),
|
|
this.#query(name, "SOA").then(({ ret }) => {
|
|
(ret as Deno.SoaRecord[]).forEach(
|
|
({ mname, rname, serial, refresh, retry, expire, minimum }) =>
|
|
records.push({
|
|
type: "SOA",
|
|
nsname: fqdnToHostname(mname),
|
|
hostmaster: fqdnToHostname(rname),
|
|
serial,
|
|
refresh,
|
|
retry,
|
|
expire,
|
|
minttl: minimum,
|
|
}),
|
|
);
|
|
}),
|
|
this.#query(name, "SRV").then(({ ret }) => {
|
|
(ret as Deno.SrvRecord[]).forEach(
|
|
({ priority, weight, port, target }) =>
|
|
records.push({
|
|
type: "SRV",
|
|
priority,
|
|
weight,
|
|
port,
|
|
name: target,
|
|
}),
|
|
);
|
|
}),
|
|
this.#query(name, "TXT").then(({ ret }) => {
|
|
ret.forEach((record) =>
|
|
records.push({ type: "TXT", entries: record })
|
|
);
|
|
}),
|
|
]);
|
|
|
|
const err = records.length ? 0 : codeMap.get("EAI_NODATA")!;
|
|
|
|
req.oncomplete(err, records);
|
|
})();
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryA(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "A").then(({ code, ret }) => {
|
|
req.oncomplete(code, 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));
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryCaa(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "CAA").then(({ code, ret }) => {
|
|
const records = (ret as Deno.CaaRecord[]).map(
|
|
({ critical, tag, value }) => ({
|
|
[tag]: value,
|
|
critical: +critical && 128,
|
|
}),
|
|
);
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryCname(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "CNAME").then(({ code, ret }) => {
|
|
req.oncomplete(code, ret);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryMx(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "MX").then(({ code, ret }) => {
|
|
const records = (ret as Deno.MxRecord[]).map(
|
|
({ preference, exchange }) => ({
|
|
priority: preference,
|
|
exchange: fqdnToHostname(exchange),
|
|
}),
|
|
);
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryNaptr(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "NAPTR").then(({ code, ret }) => {
|
|
const records = (ret as Deno.NaptrRecord[]).map(
|
|
({ order, preference, flags, services, regexp, replacement }) => ({
|
|
flags,
|
|
service: services,
|
|
regexp,
|
|
replacement,
|
|
order,
|
|
preference,
|
|
}),
|
|
);
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryNs(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "NS").then(({ code, ret }) => {
|
|
const records = (ret as string[]).map((record) => fqdnToHostname(record));
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryPtr(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "PTR").then(({ code, ret }) => {
|
|
const records = (ret as string[]).map((record) => fqdnToHostname(record));
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
querySoa(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "SOA").then(({ code, ret }) => {
|
|
let record = {};
|
|
|
|
if (ret.length) {
|
|
const { mname, rname, serial, refresh, retry, expire, minimum } =
|
|
ret[0] as Deno.SoaRecord;
|
|
|
|
record = {
|
|
nsname: fqdnToHostname(mname),
|
|
hostmaster: fqdnToHostname(rname),
|
|
serial,
|
|
refresh,
|
|
retry,
|
|
expire,
|
|
minttl: minimum,
|
|
};
|
|
}
|
|
|
|
req.oncomplete(code, record);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
querySrv(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "SRV").then(({ code, ret }) => {
|
|
const records = (ret as Deno.SrvRecord[]).map(
|
|
({ priority, weight, port, target }) => ({
|
|
priority,
|
|
weight,
|
|
port,
|
|
name: target,
|
|
}),
|
|
);
|
|
|
|
req.oncomplete(code, records);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
queryTxt(req: QueryReqWrap, name: string): number {
|
|
this.#query(name, "TXT").then(({ code, ret }) => {
|
|
req.oncomplete(code, ret);
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
getHostByAddr(_req: QueryReqWrap, _name: string): number {
|
|
// TODO(@bartlomieju): https://github.com/denoland/deno/issues/14432
|
|
notImplemented("cares.ChannelWrap.prototype.getHostByAddr");
|
|
}
|
|
|
|
getServers(): [string, number][] {
|
|
return this.#servers;
|
|
}
|
|
|
|
setServers(servers: string | [number, string, number][]): number {
|
|
if (typeof servers === "string") {
|
|
const tuples: [string, number][] = [];
|
|
|
|
for (let i = 0; i < servers.length; i += 2) {
|
|
tuples.push([servers[i], parseInt(servers[i + 1])]);
|
|
}
|
|
|
|
this.#servers = tuples;
|
|
} else {
|
|
this.#servers = servers.map(([_ipVersion, ip, port]) => [ip, port]);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
setLocalAddress(_addr0: string, _addr1?: string) {
|
|
notImplemented("cares.ChannelWrap.prototype.setLocalAddress");
|
|
}
|
|
|
|
cancel() {
|
|
notImplemented("cares.ChannelWrap.prototype.cancel");
|
|
}
|
|
}
|
|
|
|
const DNS_ESETSRVPENDING = -1000;
|
|
const EMSG_ESETSRVPENDING = "There are pending queries.";
|
|
|
|
export function strerror(code: number) {
|
|
return code === DNS_ESETSRVPENDING
|
|
? EMSG_ESETSRVPENDING
|
|
: ares_strerror(code);
|
|
}
|