Skip to content

Commit

Permalink
added: node-dns resolver, resolver for runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreiIgna committed May 23, 2024
1 parent 2195ebc commit 5e9d3a3
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 227 deletions.
20 changes: 20 additions & 0 deletions dist/dns-resolvers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DnsRecord } from './index.js';
export declare function dnsRecordsCloudflare(name: string, type?: string): Promise<DnsRecord[]>;
export declare function dnsRecordsGoogle(name: string, type?: string): Promise<DnsRecord[]>;
/**
* Get DNS records using the `dig` command in Node.js
*
* @param names The name(s) to query
* @param types The DNS type(s) to query
* @param server The DNS server to query. If not provided, the default DNS server on the network will be used
* @returns The DNS records
*/
export declare function dnsRecordsNodeDig(names: string | string[], types?: string | string[], server?: string): Promise<DnsRecord[]>;
/**
* Get DNS records using the Node.js DNS module
*
* @param names The name to query
* @param types The DNS type to query
* @returns The DNS records
*/
export declare function dnsRecordsNodeDns(name: string, type?: string): Promise<DnsRecord[]>;
198 changes: 198 additions & 0 deletions dist/dns-resolvers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { toASCII } from 'punycode';
const dnsTypeNumbers = {
1: 'A',
2: 'NS',
5: 'CNAME',
6: 'SOA',
12: 'PTR',
15: 'MX',
16: 'TXT',
24: 'SIG',
25: 'KEY',
28: 'AAAA',
33: 'SRV',
257: 'CAA',
};
export async function dnsRecordsCloudflare(name, type = 'A') {
const re = await fetch(`https://cloudflare-dns.com/dns-query?name=${toASCII(name)}&type=${type}&cd=1`, {
headers: {
accept: 'application/dns-json',
}
});
if (!re.ok) {
throw new Error(`Error fetching DNS records for ${name}: ${re.status} ${re.statusText}`);
}
const json = await re.json();
const records = (json.Answer || []).map((record) => {
const type = dnsTypeNumbers[record.type] || String(record.type);
let data = record.data;
if (['CNAME', 'NS'].includes(type) && record.data.endsWith('.')) {
data = data.slice(0, -1);
}
return { name: record.name, type, ttl: record.TTL, data };
});
return records;
}
export async function dnsRecordsGoogle(name, type = 'A') {
const re = await fetch(`https://dns.google/resolve?name=${toASCII(name)}&type=${type}&cd=1`);
if (!re.ok) {
throw new Error(`Error fetching DNS records for ${name}: ${re.status} ${re.statusText}`);
}
const json = await re.json();
const records = (json.Answer || []).map((record) => {
const type = dnsTypeNumbers[record.type] || String(record.type);
let data = record.data;
if (['CNAME', 'NS'].includes(type) && record.data.endsWith('.')) {
data = data.slice(0, -1);
}
return {
name: record.name,
type,
ttl: record.TTL,
data,
};
});
return records;
}
/**
* Get DNS records using the `dig` command in Node.js
*
* @param names The name(s) to query
* @param types The DNS type(s) to query
* @param server The DNS server to query. If not provided, the default DNS server on the network will be used
* @returns The DNS records
*/
export async function dnsRecordsNodeDig(names, types = 'A', server) {
// start building the arguments list for the `dig` command
const args = [];
// append @ to server if not present
if (server) {
if (!server.startsWith('@')) {
server = `@${server}`;
}
args.push(server);
}
if (!Array.isArray(names)) {
names = [names];
}
names.forEach(name => {
name = toASCII(name);
if (Array.isArray(types) && types.length) {
types.forEach(type => {
args.push(name, type);
});
}
else if (types && typeof types === 'string') {
args.push(name, types);
}
else {
args.push(name);
}
});
// +noall' // don't display any texts (authority, question, stats, etc) in response,
// +answer // except the answer
// +cdflag // no DNSSEC check, faster
// https://linux.die.net/man/1/dig
const { spawnSync } = await import('node:child_process');
const dig = spawnSync('dig', [...args, '+noall', '+answer', '+cdflag']);
let re = dig.stdout.toString();
const dnsRecords = [];
// split lines & ignore comments or empty
re.split("\n")
.filter(line => line.length && !line.startsWith(';'))
.forEach(line => {
// replace tab(s) with space, then split by space
const parts = line.replace(/[\t]+/g, " ").split(" ");
let name = String(parts[0]);
if (name.endsWith('.')) {
name = name.slice(0, -1);
}
dnsRecords.push({
name,
ttl: Number(parts[1]),
type: String(parts[3]),
data: parts.slice(4).join(" ")
});
});
return dnsRecords;
}
/**
* Get DNS records using the Node.js DNS module
*
* @param names The name to query
* @param types The DNS type to query
* @returns The DNS records
*/
export async function dnsRecordsNodeDns(name, type = 'A') {
const { promises: dns } = await import('node:dns');
type = type.toUpperCase();
const dnsRecords = [];
try {
if (['A', 'AAAA'].includes(type)) {
const foundRecords = type === 'A' ? await dns.resolve4(name, { ttl: true }) : await dns.resolve6(name, { ttl: true });
foundRecords.forEach(record => {
dnsRecords.push({
name,
type,
ttl: record.ttl,
data: record.address,
});
});
}
else if (type === 'CNAME') {
const foundRecords = await dns.resolveCname(name);
foundRecords.forEach(record => {
dnsRecords.push({
name,
type,
ttl: 0,
data: record,
});
});
}
else if (type === 'MX') {
const foundRecords = await dns.resolveMx(name);
foundRecords.forEach(record => {
dnsRecords.push({
name,
type,
ttl: 0,
data: `${record.priority} ${record.exchange}`,
});
});
}
else if (type === 'NS') {
const foundRecords = await dns.resolveNs(name);
foundRecords.forEach(record => {
dnsRecords.push({
name,
type,
ttl: 0,
data: record,
});
});
}
else if (type === 'SOA') {
const foundRecords = await dns.resolveSoa(name);
dnsRecords.push({
name,
type,
ttl: 0,
data: Object.values(foundRecords).join(' '),
});
}
else if (type === 'TXT') {
const foundRecords = await dns.resolveTxt(name);
foundRecords.forEach(record => {
dnsRecords.push({
name,
type,
ttl: 0,
data: record.join(' '),
});
});
}
}
catch (e) { }
return dnsRecords;
}
10 changes: 4 additions & 6 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface DnsRecord {
*
* @param name Fully qualified domain name.
* @param type DNS record type: A, AAAA, TXT, CNAME, MX, etc.
* @param resolver DNS resolver to use. Default: cloudflare-dns.
* @param resolver Which DNS resolver to use. If not specified, the best DNS resolver for this runtime will be used.
* @returns Array of discovered `DnsRecord` objects.
*
* @example Get TXT records for example.com
Expand All @@ -31,17 +31,15 @@ export interface DnsRecord {
* const mxRecords = await getDnsRecords('android.com', 'MX', 'google-dns')
* ```
*/
export declare function getDnsRecords(name: string, type?: string, resolver?: string | Function): Promise<DnsRecord[]>;
export declare function getDnsRecords(name: string, type?: string, resolver?: string): Promise<DnsRecord[]>;
/** Options for discovering DNS records. */
export type GetAllDnsRecordsOptions = {
/**
* Which DNS resolver to use for DNS lookup.
*
* Options: cloudflare-dns, google-dns, custom resolver `Function`.
*
* @default 'cloudflare-dns'
* Options: cloudflare-dns, google-dns, node-dns, node-dig, deno-dns
* */
resolver?: string | Function;
resolver?: string;
/** List of extra subdomains to check for */
subdomains?: string[];
};
Expand Down
93 changes: 30 additions & 63 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toASCII } from 'punycode';
import { dnsRecordsCloudflare, dnsRecordsGoogle, dnsRecordsNodeDig, dnsRecordsNodeDns } from './dns-resolvers.js';
import { subdomainsRecords } from './subdomains.js';
const isTld = (tld) => {
if (tld.startsWith('.')) {
Expand All @@ -22,64 +23,23 @@ const isDomain = (domain) => {
return index ? !label.startsWith('-') && !label.endsWith('-') && labelTest.test(label) : isTld(label);
});
};
const dnsTypeNumbers = {
1: 'A',
2: 'NS',
5: 'CNAME',
6: 'SOA',
12: 'PTR',
15: 'MX',
16: 'TXT',
24: 'SIG',
25: 'KEY',
28: 'AAAA',
33: 'SRV',
257: 'CAA',
};
const dnsResolvers = {
'cloudflare-dns': async (name, type = 'A') => {
const re = await fetch(`https://cloudflare-dns.com/dns-query?name=${toASCII(name)}&type=${type}&cd=1`, {
headers: {
accept: 'application/dns-json',
}
});
if (!re.ok) {
throw new Error(`Error fetching DNS records for ${name}: ${re.status} ${re.statusText}`);
}
const json = await re.json();
const records = (json.Answer || []).map((record) => {
return {
name: record.name,
type: dnsTypeNumbers[record.type] || String(record.type),
ttl: record.TTL,
data: record.data,
};
});
return records;
},
'google-dns': async (name, type = 'A') => {
const re = await fetch(`https://dns.google/resolve?name=${toASCII(name)}&type=${type}&cd=1`);
if (!re.ok) {
throw new Error(`Error fetching DNS records for ${name}: ${re.status} ${re.statusText}`);
}
const json = await re.json();
const records = (json.Answer || []).map((record) => {
return {
name: record.name,
type: dnsTypeNumbers[record.type] || String(record.type),
ttl: record.TTL,
data: record.data,
};
});
return records;
},
};
function bestDnsResolverForThisRuntime() {
if (navigator.userAgent === 'Cloudflare-Workers') {
return 'cloudflare-dns';
}
else if (navigator.userAgent.startsWith('Node.js/')) {
return 'node-dns';
}
else {
return 'google-dns';
}
}
/**
* Get DNS records of a given type for a FQDN.
*
* @param name Fully qualified domain name.
* @param type DNS record type: A, AAAA, TXT, CNAME, MX, etc.
* @param resolver DNS resolver to use. Default: cloudflare-dns.
* @param resolver Which DNS resolver to use. If not specified, the best DNS resolver for this runtime will be used.
* @returns Array of discovered `DnsRecord` objects.
*
* @example Get TXT records for example.com
Expand All @@ -96,19 +56,27 @@ const dnsResolvers = {
* const mxRecords = await getDnsRecords('android.com', 'MX', 'google-dns')
* ```
*/
export async function getDnsRecords(name, type = 'A', resolver = 'cloudflare-dns') {
export async function getDnsRecords(name, type = 'A', resolver) {
if (!isDomain(name)) {
throw new Error(`"${name}" is not a valid domain name`);
}
if (typeof resolver === 'string' && resolver in dnsResolvers) {
const fn = dnsResolvers[resolver];
if (typeof fn !== 'function') {
throw new Error(`Invalid DNS resolver: ${resolver}`);
}
return fn(name, type);
if (!resolver) {
resolver = bestDnsResolverForThisRuntime();
}
if (resolver === 'cloudflare-dns') {
return dnsRecordsCloudflare(name, type);
}
else if (resolver === 'google-dns') {
return dnsRecordsGoogle(name, type);
}
else if (resolver === 'node-dig') {
return dnsRecordsNodeDig(name, type);
}
else if (resolver === 'node-dns') {
return dnsRecordsNodeDns(name, type);
}
if (typeof resolver === 'function') {
return resolver(name, type);
else if (resolver === 'deno-dns') {
throw new Error('Deno DNS not yet implemented');
}
throw new Error(`Invalid DNS resolver: ${resolver}`);
}
Expand All @@ -121,7 +89,6 @@ export async function getDnsRecords(name, type = 'A', resolver = 'cloudflare-dns
*/
export function getAllDnsRecordsStream(domain, options = {}) {
options = {
resolver: 'cloudflare-dns',
subdomains: [],
...options,
};
Expand Down
Loading

0 comments on commit 5e9d3a3

Please sign in to comment.