Files
bun.sh/src/js/node/dns.ts
Meghan Denny 2d481e7bcb fix more node:stream (#16385)
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2025-01-16 22:40:39 -08:00

999 lines
23 KiB
TypeScript

// Hardcoded module "node:dns"
const dns = Bun.dns;
const utilPromisifyCustomSymbol = Symbol.for("nodejs.util.promisify.custom");
const { isIP } = require("./net");
const {
validateFunction,
validateArray,
validateString,
validateBoolean,
validateNumber,
} = require("internal/validators");
const errorCodes = {
NODATA: "ENODATA",
FORMERR: "EFORMERR",
SERVFAIL: "ESERVFAIL",
NOTFOUND: "ENOTFOUND",
NOTIMP: "ENOTIMP",
REFUSED: "EREFUSED",
BADQUERY: "EBADQUERY",
BADNAME: "EBADNAME",
BADFAMILY: "EBADFAMILY",
BADRESP: "EBADRESP",
CONNREFUSED: "ECONNREFUSED",
TIMEOUT: "ETIMEOUT",
EOF: "EOF",
FILE: "EFILE",
NOMEM: "ENOMEM",
DESTRUCTION: "EDESTRUCTION",
BADSTR: "EBADSTR",
BADFLAGS: "EBADFLAGS",
NONAME: "ENONAME",
BADHINTS: "EBADHINTS",
NOTINITIALIZED: "ENOTINITIALIZED",
LOADIPHLPAPI: "ELOADIPHLPAPI",
ADDRGETNETWORKPARAMS: "EADDRGETNETWORKPARAMS",
CANCELLED: "ECANCELLED",
};
const IANA_DNS_PORT = 53;
const IPv6RE = /^\[([^[\]]*)\]/;
const addrSplitRE = /(^.+?)(?::(\d+))?$/;
function translateErrorCode(promise: Promise<any>) {
return promise.catch(error => {
return Promise.reject(withTranslatedError(error));
});
}
// We prefix the error codes with "DNS_" to make it clear it's a DNS error code.
// Node does not do this, so we have to translate.
function withTranslatedError(error: any) {
const code = error?.code;
if (code?.startsWith?.("DNS_")) {
error.code = code.slice(4);
}
return error;
}
function getServers() {
return dns.getServers();
}
function setServers(servers) {
return setServersOn(servers, dns);
}
const getRuntimeDefaultResultOrderOption = $newZigFunction(
"dns_resolver.zig",
"DNSResolver.getRuntimeDefaultResultOrderOption",
0,
);
function newResolver(options) {
if (!newResolver.zig) {
newResolver.zig = $newZigFunction("dns_resolver.zig", "DNSResolver.newResolver", 1);
}
return newResolver.zig(options);
}
function defaultResultOrder() {
if (typeof defaultResultOrder.value === "undefined") {
defaultResultOrder.value = getRuntimeDefaultResultOrderOption();
}
return defaultResultOrder.value;
}
function setDefaultResultOrder(order) {
validateOrder(order);
defaultResultOrder.value = order;
}
function getDefaultResultOrder() {
return defaultResultOrder;
}
function setServersOn(servers, object) {
validateArray(servers, "servers");
const triples = [];
servers.forEach((server, i) => {
validateString(server, `servers[${i}]`);
let ipVersion = isIP(server);
if (ipVersion !== 0) {
triples.push([ipVersion, server, IANA_DNS_PORT]);
return;
}
const match = IPv6RE.exec(server);
// Check for an IPv6 in brackets.
if (match) {
ipVersion = isIP(match[1]);
if (ipVersion !== 0) {
const port = parseInt(addrSplitRE[Symbol.replace](server, "$2")) || IANA_DNS_PORT;
triples.push([ipVersion, match[1], port]);
return;
}
}
// addr:port
const addrSplitMatch = addrSplitRE.exec(server);
if (addrSplitMatch) {
const hostIP = addrSplitMatch[1];
const port = addrSplitMatch[2] || IANA_DNS_PORT;
ipVersion = isIP(hostIP);
if (ipVersion !== 0) {
triples.push([ipVersion, hostIP, parseInt(port)]);
return;
}
}
throw $ERR_INVALID_IP_ADDRESS(server);
});
object.setServers(triples);
}
function validateFlagsOption(options) {
if (options.flags === undefined) {
return;
}
validateNumber(options.flags);
if ((options.flags & ~(dns.ALL | dns.ADDRCONFIG | dns.V4MAPPED)) != 0) {
throw $ERR_INVALID_ARG_VALUE("hints", options.flags, "is invalid");
}
}
function validateFamily(family) {
if (family !== 6 && family !== 4 && family !== 0) {
throw $ERR_INVALID_ARG_VALUE("family", family, "must be one of 0, 4 or 6");
}
}
function validateFamilyOption(options) {
if (options.family != null) {
switch (options.family) {
case "IPv4":
options.family = 4;
break;
case "IPv6":
options.family = 6;
break;
default:
validateFamily(options.family);
break;
}
}
}
function validateAllOption(options) {
if (options.all !== undefined) {
validateBoolean(options.all);
}
}
function validateVerbatimOption(options) {
if (options.verbatim !== undefined) {
validateBoolean(options.verbatim);
}
}
function validateOrder(order) {
if (!["ipv4first", "ipv6first", "verbatim"].includes(order)) {
throw $ERR_INVALID_ARG_VALUE("order", order, "is invalid");
}
}
function validateOrderOption(options) {
if (options.order !== undefined) {
validateOrder(options.order);
}
}
function validateResolve(hostname, callback) {
if (typeof hostname !== "string") {
throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname);
}
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
}
function validateLocalAddresses(first, second) {
validateString(first);
if (typeof second !== "undefined") {
validateString(second);
}
}
function invalidHostname(hostname) {
if (invalidHostname.warned) {
return;
}
invalidHostname.warned = true;
process.emitWarning(
`The provided hostname "${String(hostname)}" is not a valid hostname, and is supported in the dns module solely for compatibility.`,
"DeprecationWarning",
"DEP0118",
);
}
function translateLookupOptions(options) {
if (!options || typeof options !== "object") {
options = { family: options };
}
let { family, order, verbatim, hints: flags, all } = options;
if (order === undefined && typeof verbatim === "boolean") {
order = verbatim ? "verbatim" : "ipv4first";
}
order ??= defaultResultOrder();
return {
family,
flags,
all,
order,
verbatim,
};
}
function validateLookupOptions(options) {
validateFlagsOption(options);
validateFamilyOption(options);
validateAllOption(options);
validateVerbatimOption(options);
validateOrderOption(options);
}
function lookup(hostname, options, callback) {
if (typeof hostname !== "string" && hostname) {
throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname);
}
if (typeof options === "function") {
callback = options;
options = { family: 0 };
} else if (typeof options === "number") {
validateFunction(callback, "callback");
validateFamily(options);
options = { family: options };
} else if (options !== undefined && typeof options !== "object") {
validateFunction(arguments.length === 2 ? options : callback, "callback");
throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options);
}
validateFunction(callback, "callback");
options = translateLookupOptions(options);
validateLookupOptions(options);
if (!hostname) {
invalidHostname(hostname);
if (options.all) {
callback(null, []);
} else {
callback(null, null, 4);
}
return;
}
const family = isIP(hostname);
if (family) {
if (options.all) {
process.nextTick(callback, null, [{ address: hostname, family }]);
} else {
process.nextTick(callback, null, hostname, family);
}
return;
}
dns.lookup(hostname, options).then(res => {
throwIfEmpty(res);
if (options.order == "ipv4first") {
res.sort((a, b) => a.family - b.family);
} else if (options.order == "ipv6first") {
res.sort((a, b) => b.family - a.family);
}
if (options?.all) {
callback(null, res.map(mapLookupAll));
} else {
const [{ address, family }] = res;
callback(null, address, family);
}
}, callback);
}
function lookupService(address, port, callback) {
if (arguments.length < 3) {
throw $ERR_MISSING_ARGS("address", "port", "callback");
}
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
validateString(address);
dns.lookupService(address, port).then(
results => {
callback(null, ...results);
},
error => {
callback(withTranslatedError(error));
},
);
}
function validateResolverOptions(options) {
if (options === undefined) {
return;
}
for (const key of ["timeout", "tries"]) {
if (key in options) {
if (typeof options[key] !== "number") {
throw $ERR_INVALID_ARG_TYPE(key, "number", options[key]);
}
}
}
if ("timeout" in options) {
const timeout = options.timeout;
if ((timeout < 0 && timeout != -1) || Math.floor(timeout) != timeout || timeout >= 2 ** 31) {
throw $ERR_OUT_OF_RANGE("Invalid timeout", timeout);
}
}
}
var InternalResolver = class Resolver {
#resolver;
constructor(options) {
validateResolverOptions(options);
this.#resolver = this._handle = newResolver(options);
}
cancel() {
this.#resolver.cancel();
}
static #getResolver(object) {
return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns;
}
getServers() {
return Resolver.#getResolver(this).getServers() || [];
}
resolve(hostname, rrtype, callback) {
if (typeof rrtype === "function") {
callback = rrtype;
rrtype = "A";
} else if (typeof rrtype === "undefined") {
rrtype = "A";
} else if (typeof rrtype !== "string") {
throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype);
}
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolve(hostname)
.then(
results => {
switch (rrtype?.toLowerCase()) {
case "a":
case "aaaa":
callback(null, hostname, results.map(mapResolveX));
break;
default:
callback(null, results);
break;
}
},
error => {
callback(withTranslatedError(error));
},
);
}
resolve4(hostname, options, callback) {
if (typeof options == "function") {
callback = options;
options = null;
}
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolve(hostname, "A")
.then(
addresses => {
callback(null, options?.ttl ? addresses : addresses.map(mapResolveX));
},
error => {
callback(withTranslatedError(error));
},
);
}
resolve6(hostname, options, callback) {
if (typeof options == "function") {
callback = options;
options = null;
}
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolve(hostname, "AAAA")
.then(
addresses => {
callback(null, options?.ttl ? addresses : addresses.map(mapResolveX));
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveAny(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveAny(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveCname(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveCname(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveMx(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveMx(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveNaptr(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveNaptr(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveNs(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveNs(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolvePtr(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolvePtr(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveSrv(hostname, callback) {
validateResolve(hostname, callback);
Resolver.#getResolver(this)
.resolveSrv(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveCaa(hostname, callback) {
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
Resolver.#getResolver(this)
.resolveCaa(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveTxt(hostname, callback) {
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
Resolver.#getResolver(this)
.resolveTxt(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
resolveSoa(hostname, callback) {
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
Resolver.#getResolver(this)
.resolveSoa(hostname)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
reverse(ip, callback) {
if (typeof callback !== "function") {
throw $ERR_INVALID_ARG_TYPE("callback", "function", callback);
}
Resolver.#getResolver(this)
.reverse(ip)
.then(
results => {
callback(null, results);
},
error => {
callback(withTranslatedError(error));
},
);
}
setLocalAddress(first, second) {
validateLocalAddresses(first, second);
Resolver.#getResolver(this).setLocalAddress(first, second);
}
setServers(servers) {
return setServersOn(servers, Resolver.#getResolver(this));
}
};
function Resolver(options) {
return new InternalResolver(options);
}
Resolver.prototype = {};
Object.setPrototypeOf(Resolver.prototype, InternalResolver.prototype);
Object.setPrototypeOf(Resolver, InternalResolver);
var {
resolve,
resolve4,
resolve6,
resolveAny,
resolveCname,
resolveCaa,
resolveMx,
resolveNaptr,
resolveNs,
resolvePtr,
resolveSoa,
resolveSrv,
reverse,
resolveTxt,
} = InternalResolver.prototype;
const mapLookupAll = res => {
const { address, family } = res;
return { address, family };
};
function throwIfEmpty(res) {
if (res.length === 0) {
const err = new Error("No records found");
err.name = "DNSException";
err.code = "ENODATA";
// Hardcoded errno
err.errno = 1;
err.syscall = "getaddrinfo";
throw err;
}
}
Object.defineProperty(throwIfEmpty, "name", { value: "::bunternal::" });
const promisifyLookup = order => res => {
throwIfEmpty(res);
if (order == "ipv4first") {
res.sort((a, b) => a.family - b.family);
} else if (order == "ipv6first") {
res.sort((a, b) => b.family - a.family);
}
const [{ address, family }] = res;
return { address, family };
};
const promisifyLookupAll = order => res => {
throwIfEmpty(res);
if (order == "ipv4first") {
res.sort((a, b) => a.family - b.family);
} else if (order == "ipv6first") {
res.sort((a, b) => b.family - a.family);
}
return res.map(mapLookupAll);
};
const mapResolveX = a => a.address;
const promisifyResolveX = ttl => {
if (ttl) {
return res => res;
} else {
return res => {
return res?.map(mapResolveX);
};
}
};
// promisified versions
const promises = {
...errorCodes,
lookup(hostname, options) {
if (typeof hostname !== "string" && hostname) {
throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname);
}
if (typeof options === "number") {
validateFamily(options);
options = { family: options };
} else if (options !== undefined && typeof options !== "object") {
throw $ERR_INVALID_ARG_TYPE("options", ["integer", "object"], options);
}
options = translateLookupOptions(options);
validateLookupOptions(options);
if (!hostname) {
invalidHostname(hostname);
return Promise.resolve(
options.all
? []
: {
address: null,
family: 4,
},
);
}
const family = isIP(hostname);
if (family) {
const obj = { address: hostname, family };
return Promise.resolve(options.all ? [obj] : obj);
}
if (options.all) {
return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookupAll(options.order)));
}
return translateErrorCode(dns.lookup(hostname, options).then(promisifyLookup(options.order)));
},
lookupService(address, port) {
if (arguments.length !== 2) {
throw $ERR_MISSING_ARGS("address", "port");
}
validateString(address);
try {
return translateErrorCode(dns.lookupService(address, port)).then(([hostname, service]) => ({
hostname,
service,
}));
} catch (err) {
if (err.name === "TypeError" || err.name === "RangeError") {
throw err;
}
return Promise.reject(withTranslatedError(err));
}
},
resolve(hostname, rrtype) {
if (typeof hostname !== "string") {
throw $ERR_INVALID_ARG_TYPE("hostname", "string", hostname);
}
if (typeof rrtype === "undefined") {
rrtype = "A";
} else if (typeof rrtype !== "string") {
throw $ERR_INVALID_ARG_TYPE("rrtype", "string", rrtype);
}
switch (rrtype?.toLowerCase()) {
case "a":
case "aaaa":
return translateErrorCode(dns.resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder())));
default:
return translateErrorCode(dns.resolve(hostname, rrtype));
}
},
resolve4(hostname, options) {
return translateErrorCode(dns.resolve(hostname, "A").then(promisifyResolveX(options?.ttl)));
},
resolve6(hostname, options) {
return translateErrorCode(dns.resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl)));
},
resolveAny(hostname) {
return translateErrorCode(dns.resolveAny(hostname));
},
resolveSrv(hostname) {
return translateErrorCode(dns.resolveSrv(hostname));
},
resolveTxt(hostname) {
return translateErrorCode(dns.resolveTxt(hostname));
},
resolveSoa(hostname) {
return translateErrorCode(dns.resolveSoa(hostname));
},
resolveNaptr(hostname) {
return translateErrorCode(dns.resolveNaptr(hostname));
},
resolveMx(hostname) {
return translateErrorCode(dns.resolveMx(hostname));
},
resolveCaa(hostname) {
return translateErrorCode(dns.resolveCaa(hostname));
},
resolveNs(hostname) {
return translateErrorCode(dns.resolveNs(hostname));
},
resolvePtr(hostname) {
return translateErrorCode(dns.resolvePtr(hostname));
},
resolveCname(hostname) {
return translateErrorCode(dns.resolveCname(hostname));
},
reverse(ip) {
return translateErrorCode(dns.reverse(ip));
},
Resolver: class Resolver {
#resolver;
constructor(options) {
validateResolverOptions(options);
this.#resolver = this._handle = newResolver(options);
}
cancel() {
this.#resolver.cancel();
}
static #getResolver(object) {
return typeof object !== "undefined" && #resolver in object ? object.#resolver : dns;
}
getServers() {
return Resolver.#getResolver(this).getServers() || [];
}
resolve(hostname, rrtype) {
if (typeof rrtype === "undefined") {
rrtype = "A";
} else if (typeof rrtype !== "string") {
rrtype = null;
}
switch (rrtype?.toLowerCase()) {
case "a":
case "aaaa":
return translateErrorCode(
Resolver.#getResolver(this).resolve(hostname, rrtype).then(promisifyLookup(defaultResultOrder())),
);
default:
return translateErrorCode(Resolver.#getResolver(this).resolve(hostname, rrtype));
}
}
resolve4(hostname, options) {
return translateErrorCode(
Resolver.#getResolver(this).resolve(hostname, "A").then(promisifyResolveX(options?.ttl)),
);
}
resolve6(hostname, options) {
return translateErrorCode(
Resolver.#getResolver(this).resolve(hostname, "AAAA").then(promisifyResolveX(options?.ttl)),
);
}
resolveAny(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveAny(hostname));
}
resolveCname(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveCname(hostname));
}
resolveMx(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveMx(hostname));
}
resolveNaptr(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveNaptr(hostname));
}
resolveNs(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveNs(hostname));
}
resolvePtr(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolvePtr(hostname));
}
resolveSoa(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveSoa(hostname));
}
resolveSrv(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveSrv(hostname));
}
resolveCaa(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveCaa(hostname));
}
resolveTxt(hostname) {
return translateErrorCode(Resolver.#getResolver(this).resolveTxt(hostname));
}
reverse(ip) {
return translateErrorCode(Resolver.#getResolver(this).reverse(ip));
}
setLocalAddress(first, second) {
validateLocalAddresses(first, second);
Resolver.#getResolver(this).setLocalAddress(first, second);
}
setServers(servers) {
return setServersOn(servers, Resolver.#getResolver(this));
}
},
setDefaultResultOrder,
setServers,
};
// Compatibility with util.promisify(dns[method])
for (const [method, pMethod] of [
[lookup, promises.lookup],
[lookupService, promises.lookupService],
[resolve, promises.resolve],
[reverse, promises.reverse],
[resolve4, promises.resolve4],
[resolve6, promises.resolve6],
[resolveAny, promises.resolveAny],
[resolveCname, promises.resolveCname],
[resolveCaa, promises.resolveCaa],
[resolveMx, promises.resolveMx],
[resolveNs, promises.resolveNs],
[resolvePtr, promises.resolvePtr],
[resolveSoa, promises.resolveSoa],
[resolveSrv, promises.resolveSrv],
[resolveTxt, promises.resolveTxt],
[resolveNaptr, promises.resolveNaptr],
]) {
method[utilPromisifyCustomSymbol] = pMethod;
}
export default {
ADDRCONFIG: dns.ADDRCONFIG,
ALL: dns.ALL,
V4MAPPED: dns.V4MAPPED,
// ERROR CODES
...errorCodes,
lookup,
lookupService,
Resolver,
setServers,
setDefaultResultOrder,
getDefaultResultOrder,
resolve,
reverse,
resolve4,
resolve6,
resolveAny,
resolveCname,
resolveCaa,
resolveMx,
resolveNs,
resolvePtr,
resolveSoa,
resolveSrv,
resolveTxt,
resolveNaptr,
promises,
getServers,
};