Skip to content
File

Blob: src/node/internal/internal_dns.ts

typescript477 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4// Copyright Joyent and Node contributors. All rights reserved. MIT license.
5 
6/* eslint-disable @typescript-eslint/no-deprecated */
7import {
8 sendDnsRequest,
9 validateAnswer,
10 normalizeMx,
11 normalizeCname,
12 normalizeCaa,
13 normalizePtr,
14 normalizeNaptr,
15 normalizeNs,
16 normalizeSoa,
17 normalizeTxt,
18 normalizeSrv,
19 type MX,
20 type CAA,
21 type NAPTR,
22 type SOA,
23 type SRV,
24 type TTLResponse,
25} from 'node-internal:internal_dns_client';
26import {
27 DnsError,
28 ERR_INVALID_ARG_TYPE,
29 ERR_OPTION_NOT_IMPLEMENTED,
30 ERR_INVALID_ARG_VALUE,
31} from 'node-internal:internal_errors';
32import {
33 validateString,
34 validateFunction,
35 validateOneOf,
36 validateNumber,
37 validateBoolean,
38} from 'node-internal:validators';
39import * as errorCodes from 'node-internal:internal_dns_constants';
40import { isIP } from 'node-internal:internal_net';
41import type dns from 'node:dns';
42 
43type DnsOrder = 'verbatim' | 'ipv4first' | 'ipv6first';
44 
45export const validFamilies = [0, 4, 6];
46export const validDnsOrders: DnsOrder[] = [
47 'verbatim',
48 'ipv4first',
49 'ipv6first',
50];
51 
52let defaultDnsOrder: DnsOrder = 'verbatim';
53 
54export function getServers(): ReturnType<(typeof dns)['getServers']> {
55 return ['1.1.1.1', '2606:4700:4700::1111', '1.0.0.1', '2606:4700:4700::1001'];
56}
57 
58export type LookupCallback = (
59 err: Error | null,
60 address?: string | { address: string; family: number }[],
61 family?: number
62) => void;
63 
64export function lookup(
65 hostname: string,
66 options?: dns.LookupOptions | LookupCallback,
67 callback?: LookupCallback
68): void {
69 let family: 0 | 4 | 6 = 0;
70 let all = false;
71 let dnsOrder: DnsOrder = getDefaultResultOrder();
72 
73 // Parse arguments
74 if (hostname) {
75 validateString(hostname, 'hostname');
76 }
77 
78 if (typeof options === 'function') {
79 callback = options;
80 family = 0;
81 } else if (typeof options === 'number') {
82 validateFunction(callback, 'callback');
83 
84 validateOneOf(options, 'family', validFamilies);
85 family = options;
86 } else if (options !== undefined && typeof options !== 'object') {
87 validateFunction(arguments.length === 2 ? options : callback, 'callback');
88 throw new ERR_INVALID_ARG_TYPE('options', ['integer', 'object'], options);
89 } else {
90 validateFunction(callback, 'callback');
91 
92 if (options?.hints != null) {
93 validateNumber(options.hints, 'options.hints');
94 throw new ERR_OPTION_NOT_IMPLEMENTED('options.hints');
95 }
96 if (options?.family != null) {
97 switch (options.family) {
98 case 'IPv4':
99 family = 4;
100 break;
101 case 'IPv6':
102 family = 6;
103 break;
104 default:
105 validateOneOf(options.family, 'options.family', validFamilies);
106 family = options.family as 0 | 4 | 6;
107 break;
108 }
109 }
110 if (options?.all != null) {
111 validateBoolean(options.all, 'options.all');
112 all = options.all;
113 }
114 if (options?.verbatim != null) {
115 validateBoolean(options.verbatim, 'options.verbatim');
116 dnsOrder = options.verbatim ? 'verbatim' : 'ipv4first';
117 }
118 if (options?.order != null) {
119 validateOneOf(options.order, 'options.order', validDnsOrders);
120 dnsOrder = options.order;
121 }
122 }
123 
124 if (!hostname) {
125 if (all) {
126 process.nextTick(callback, null, []);
127 } else {
128 process.nextTick(callback, null, null, family === 6 ? 6 : 4);
129 }
130 return;
131 }
132 
133 const matchedFamily = isIP(hostname);
134 if (matchedFamily) {
135 if (all) {
136 process.nextTick(callback, null, [
137 { address: hostname, family: matchedFamily },
138 ]);
139 } else {
140 process.nextTick(callback, null, hostname, matchedFamily);
141 }
142 return;
143 }
144 
145 // If all is true and family is 0, we need to query both A and AAAA records
146 if (all && family === 0) {
147 Promise.all([
148 sendDnsRequest(hostname, 'A').catch(() => ({ Answer: [] })),
149 sendDnsRequest(hostname, 'AAAA').catch(() => ({ Answer: [] })),
150 ])
151 .then(([ipv4Response, ipv6Response]): void => {
152 const ipv4Addresses: { address: string; family: 4 }[] =
153 ipv4Response.Answer?.map((answer) => ({
154 address: answer.data,
155 family: 4,
156 })) ?? [];
157 const ipv6Addresses: { address: string; family: 6 }[] =
158 ipv6Response.Answer?.map((answer) => ({
159 address: answer.data,
160 family: 6,
161 })) ?? [];
162 
163 // No addresses found
164 if (ipv4Addresses.length === 0 && ipv6Addresses.length === 0) {
165 callback(new DnsError(hostname, errorCodes.NOTFOUND, 'queryA'));
166 return;
167 }
168 
169 // Order addresses based on dnsOrder
170 if (dnsOrder === 'ipv6first') {
171 callback(null, [...ipv6Addresses, ...ipv4Addresses]);
172 } else {
173 // verbatim - preserve order as received (still separating by type)
174 // ipv4first = same as verbatim
175 callback(null, [...ipv4Addresses, ...ipv6Addresses]);
176 }
177 })
178 .catch((error: unknown): void => {
179 process.nextTick(callback, error);
180 });
181 } else if (family === 0) {
182 // family=0, all=false: query both A and AAAA, return first result based on dnsOrder
183 Promise.all([
184 sendDnsRequest(hostname, 'A').catch(() => ({ Answer: [] })),
185 sendDnsRequest(hostname, 'AAAA').catch(() => ({ Answer: [] })),
186 ])
187 .then(([ipv4Response, ipv6Response]): void => {
188 const ipv4 = ipv4Response.Answer?.at(0)?.data;
189 const ipv6 = ipv6Response.Answer?.at(0)?.data;
190 
191 if (ipv4 == null && ipv6 == null) {
192 callback(new DnsError(hostname, errorCodes.NOTFOUND, 'queryA'));
193 return;
194 }
195 
196 // Return the preferred address based on dnsOrder
197 if (dnsOrder === 'ipv6first') {
198 if (ipv6 != null) {
199 callback(null, ipv6, 6);
200 } else {
201 callback(null, ipv4 as string, 4);
202 }
203 } else {
204 if (ipv4 != null) {
205 callback(null, ipv4, 4);
206 } else {
207 callback(null, ipv6 as string, 6);
208 }
209 }
210 })
211 .catch((error: unknown): void => {
212 process.nextTick(callback, error);
213 });
214 } else {
215 const requestType = family === 4 ? 'A' : 'AAAA';
216 
217 // Single request when family is specified (with or without all=true)
218 sendDnsRequest(hostname, requestType)
219 .then((json): void => {
220 validateAnswer(json.Answer, hostname, `query${requestType}`);
221 
222 if (all) {
223 // Return all addresses with the specified family
224 callback(
225 null,
226 json.Answer.map((answer) => ({
227 address: answer.data,
228 family,
229 }))
230 );
231 } else {
232 // Return just the first address
233 callback(null, json.Answer.at(0)?.data as string, family);
234 }
235 })
236 .catch((error: unknown): void => {
237 process.nextTick(callback, error);
238 });
239 }
240}
241 
242// eslint-disable-next-line @typescript-eslint/require-await
243export async function lookupService(): Promise<void> {
244 // TODO(soon): Implement this.
245 throw new Error('Not implemented');
246}
247 
248export function resolve(
249 name: string,
250 rrtype: string
251): Promise<unknown> | Promise<void> {
252 validateString(name, 'name');
253 
254 switch (rrtype) {
255 case 'A':
256 return resolve4(name);
257 case 'AAAA':
258 return resolve6(name);
259 case 'ANY':
260 return resolveAny();
261 case 'CAA':
262 return resolveCaa(name);
263 case 'CNAME':
264 return resolveCname(name);
265 case 'MX':
266 return resolveMx(name);
267 case 'NAPTR':
268 return resolveNaptr(name);
269 case 'NS':
270 return resolveNs(name);
271 case 'PTR':
272 return resolvePtr(name);
273 case 'SOA':
274 return resolveSoa(name);
275 case 'SRV':
276 return resolveSrv(name);
277 case 'TXT':
278 return resolveTxt(name);
279 default: {
280 throw new ERR_INVALID_ARG_VALUE('rrtype', rrtype);
281 }
282 }
283}
284 
285export function resolve4(
286 name: string,
287 options?: { ttl?: boolean }
288): Promise<(string | TTLResponse)[]> {
289 validateString(name, 'name');
290 
291 // The following change is done to comply with Node.js behavior
292 const ttl = !!options?.ttl;
293 
294 // Validation errors needs to be sync.
295 // Return a promise rather than using async qualifier.
296 return sendDnsRequest(name, 'A').then((json) => {
297 validateAnswer(json.Answer, name, 'queryA');
298 
299 return json.Answer.map((a) =>
300 ttl ? { ttl: a.TTL, address: a.data } : a.data
301 );
302 });
303}
304 
305export function resolve6(
306 name: string,
307 options?: { ttl?: boolean }
308): Promise<(string | TTLResponse)[]> {
309 validateString(name, 'name');
310 
311 // The following change is done to comply with Node.js behavior
312 const ttl = !!options?.ttl;
313 
314 // Validation errors needs to be sync.
315 // Return a promise rather than using async qualifier.
316 return sendDnsRequest(name, 'AAAA').then((json) => {
317 validateAnswer(json.Answer, name, 'queryAaaa');
318 
319 return json.Answer.map((a) =>
320 ttl ? { ttl: a.TTL, address: a.data } : a.data
321 );
322 });
323}
324 
325// eslint-disable-next-line @typescript-eslint/require-await
326export async function resolveAny(): Promise<void> {
327 // TODO(soon): Implement this
328 throw new Error('Not implemented');
329}
330 
331export function resolveCname(name: string): Promise<string[]> {
332 validateString(name, 'name');
333 
334 // Validation errors needs to be sync.
335 // Return a promise rather than using async qualifier.
336 return sendDnsRequest(name, 'CNAME').then((json) => {
337 validateAnswer(json.Answer, name, 'queryCname');
338 
339 return json.Answer.map(normalizeCname);
340 });
341}
342 
343export function resolveCaa(name: string): Promise<CAA[]> {
344 validateString(name, 'name');
345 
346 // Validation errors needs to be sync.
347 // Return a promise rather than using async qualifier.
348 return sendDnsRequest(name, 'CAA').then((json) => {
349 validateAnswer(json.Answer, name, 'queryCaa');
350 
351 return json.Answer.map(normalizeCaa);
352 });
353}
354 
355export function resolveMx(name: string): Promise<MX[]> {
356 validateString(name, 'name');
357 
358 // Validation errors needs to be sync.
359 // Return a promise rather than using async qualifier.
360 return sendDnsRequest(name, 'MX').then((json) => {
361 validateAnswer(json.Answer, name, 'queryMx');
362 
363 return json.Answer.map((answer) => normalizeMx(name, answer));
364 });
365}
366 
367export function resolveNaptr(name: string): Promise<NAPTR[]> {
368 validateString(name, 'name');
369 
370 // Validation errors needs to be sync.
371 // Return a promise rather than using async qualifier.
372 return sendDnsRequest(name, 'NAPTR').then((json) => {
373 validateAnswer(json.Answer, name, 'queryNaptr');
374 
375 return json.Answer.map(normalizeNaptr);
376 });
377}
378 
379export function resolveNs(name: string): Promise<string[]> {
380 validateString(name, 'name');
381 
382 // Validation errors needs to be sync.
383 // Return a promise rather than using async qualifier.
384 return sendDnsRequest(name, 'NS').then((json) => {
385 validateAnswer(json.Answer, name, 'queryNs');
386 
387 return json.Answer.map(normalizeNs);
388 });
389}
390 
391export function resolvePtr(name: string): Promise<string[]> {
392 validateString(name, 'name');
393 
394 // Validation errors needs to be sync.
395 // Return a promise rather than using async qualifier.
396 return sendDnsRequest(name, 'PTR').then((json) => {
397 validateAnswer(json.Answer, name, 'queryPtr');
398 
399 return json.Answer.map(normalizePtr);
400 });
401}
402 
403export function resolveSoa(name: string): Promise<SOA> {
404 validateString(name, 'name');
405 
406 // Validation errors needs to be sync.
407 // Return a promise rather than using async qualifier.
408 return sendDnsRequest(name, 'SOA').then((json) => {
409 validateAnswer(json.Answer, name, 'querySoa');
410 
411 // This is highly unlikely, but let's assert length just to be safe.
412 const firstElement = json.Answer.at(0);
413 if (!firstElement) {
414 throw new DnsError(name, errorCodes.NOTFOUND, 'querySoa');
415 }
416 
417 return normalizeSoa(firstElement);
418 });
419}
420 
421export function resolveSrv(name: string): Promise<SRV[]> {
422 validateString(name, 'name');
423 
424 // Validation errors needs to be sync.
425 // Return a promise rather than using async qualifier.
426 return sendDnsRequest(name, 'SRV').then((json) => {
427 validateAnswer(json.Answer, name, 'querySrv');
428 
429 return json.Answer.map(normalizeSrv);
430 });
431}
432 
433export function resolveTxt(name: string): Promise<string[][]> {
434 validateString(name, 'name');
435 
436 // Validation errors needs to be sync.
437 // Return a promise rather than using async qualifier.
438 return sendDnsRequest(name, 'TXT').then((json) => {
439 validateAnswer(json.Answer, name, 'queryTxt');
440 
441 return json.Answer.map(normalizeTxt);
442 });
443}
444 
445export function reverse(name: string): Promise<string[]> {
446 validateString(name, 'name');
447 
448 // Validation errors needs to be sync.
449 // Return a promise rather than using async qualifier.
450 return sendDnsRequest(name, 'PTR').then((json) => {
451 validateAnswer(json.Answer, name, 'queryPtr');
452 
453 return json.Answer.map(({ data }) => {
454 if (data.endsWith('.')) {
455 return data.slice(0, -1);
456 }
457 return data;
458 });
459 });
460}
461 
462export function setDefaultResultOrder(value: unknown): void {
463 validateOneOf(value, 'dnsOrder', validDnsOrders);
464 defaultDnsOrder = value as DnsOrder;
465}
466 
467export function getDefaultResultOrder(): DnsOrder {
468 return defaultDnsOrder;
469}
470 
471export function setServers(): void {
472 // This function does not apply to workerd model.
473 // Our implementation always use Cloudflare DNS and does not
474 // allow users to change the underlying DNS server.
475 throw new Error('Not implemented');
476}