328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
|
|
export type TDnsProvider = 'google' | 'cloudflare';
|
|
|
|
export const makeNodeProcessUseDnsProvider = (providerArg: TDnsProvider) => {
|
|
switch (providerArg) {
|
|
case 'cloudflare':
|
|
plugins.dns.setServers([
|
|
'1.1.1.1',
|
|
'1.0.0.1',
|
|
'[2606:4700:4700::1111]',
|
|
'[2606:4700:4700::1001]',
|
|
]);
|
|
break;
|
|
case 'google':
|
|
plugins.dns.setServers([
|
|
'8.8.8.8',
|
|
'8.8.4.4',
|
|
'[2001:4860:4860::8888]',
|
|
'[2606:4700:4700::1001]',
|
|
]);
|
|
}
|
|
};
|
|
|
|
export type TResolutionStrategy = 'doh' | 'system' | 'prefer-system';
|
|
|
|
export interface ISmartDnsConstructorOptions {
|
|
strategy?: TResolutionStrategy; // default: 'prefer-system'
|
|
allowDohFallback?: boolean; // allow fallback to DoH if system fails (default: true)
|
|
timeoutMs?: number; // optional per-query timeout
|
|
}
|
|
|
|
export interface IDnsJsonResponse {
|
|
Status: number;
|
|
TC: boolean;
|
|
RD: boolean;
|
|
RA: boolean;
|
|
AD: boolean;
|
|
CD: boolean;
|
|
Question: Array<{ name: string; type: number }>;
|
|
Answer: Array<{ name: string; type: number; TTL: number; data: string }>;
|
|
Additional: [];
|
|
Comment: string;
|
|
}
|
|
|
|
/**
|
|
* class dnsly offers methods for working with dns from a dns provider like Google DNS
|
|
*/
|
|
export class Smartdns {
|
|
public dnsServerIp: string;
|
|
public dnsServerPort: number;
|
|
private strategy: TResolutionStrategy = 'prefer-system';
|
|
private allowDohFallback = true;
|
|
private timeoutMs: number | undefined;
|
|
|
|
public dnsTypeMap: { [key: string]: number } = {
|
|
A: 1,
|
|
AAAA: 28,
|
|
CNAME: 5,
|
|
MX: 15,
|
|
TXT: 16,
|
|
};
|
|
|
|
/**
|
|
* constructor for class dnsly
|
|
*/
|
|
constructor(optionsArg: ISmartDnsConstructorOptions) {
|
|
this.strategy = optionsArg?.strategy || 'prefer-system';
|
|
this.allowDohFallback =
|
|
optionsArg?.allowDohFallback === undefined ? true : optionsArg.allowDohFallback;
|
|
this.timeoutMs = optionsArg?.timeoutMs;
|
|
}
|
|
|
|
/**
|
|
* check a dns record until it has propagated to Google DNS
|
|
* should be considerably fast
|
|
* @param recordNameArg
|
|
* @param recordTypeArg
|
|
* @param expectedValue
|
|
*/
|
|
public async checkUntilAvailable(
|
|
recordNameArg: string,
|
|
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
|
expectedValue: string,
|
|
cyclesArg: number = 50,
|
|
intervalArg: number = 500
|
|
) {
|
|
let runCycles = 0;
|
|
const doCheck = async () => {
|
|
if (runCycles < cyclesArg) {
|
|
runCycles++;
|
|
try {
|
|
let myRecordArray: plugins.tsclass.network.IDnsRecord[];
|
|
if (runCycles % 2 === 0 || !plugins.dns) {
|
|
myRecordArray = await this.getRecords(recordNameArg, recordTypeArg, 0);
|
|
} else {
|
|
myRecordArray = await this.getRecordWithNodeDNS(recordNameArg, recordTypeArg);
|
|
}
|
|
const myRecord = myRecordArray[0].value;
|
|
if (myRecord === expectedValue) {
|
|
console.log(
|
|
`smartdns: .checkUntilAvailable() verified that wanted >>>${recordTypeArg}<<< record exists for >>>${recordNameArg}<<< with value >>>${expectedValue}<<<`
|
|
);
|
|
return true;
|
|
} else {
|
|
await plugins.smartdelay.delayFor(intervalArg);
|
|
return await doCheck();
|
|
}
|
|
} catch (err) {
|
|
// console.log(err);
|
|
await plugins.smartdelay.delayFor(intervalArg);
|
|
return await doCheck();
|
|
}
|
|
} else {
|
|
console.log(
|
|
`smartdns: .checkUntilAvailable() failed permanently for ${recordNameArg} with value ${recordTypeArg} - ${expectedValue}...`
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
return await doCheck();
|
|
}
|
|
|
|
/**
|
|
* get A Dns Record
|
|
*/
|
|
public async getRecordsA(recordNameArg: string): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
|
return await this.getRecords(recordNameArg, 'A');
|
|
}
|
|
|
|
/**
|
|
* get AAAA Record
|
|
*/
|
|
public async getRecordsAAAA(recordNameArg: string) {
|
|
return await this.getRecords(recordNameArg, 'AAAA');
|
|
}
|
|
|
|
/**
|
|
* gets a txt record
|
|
*/
|
|
public async getRecordsTxt(recordNameArg: string): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
|
return await this.getRecords(recordNameArg, 'TXT');
|
|
}
|
|
|
|
public async getRecords(
|
|
recordNameArg: string,
|
|
recordTypeArg: plugins.tsclass.network.TDnsRecordType,
|
|
retriesCounterArg = 20
|
|
): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
|
const trySystem = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
|
// Prefer dns.lookup for A/AAAA so hosts file and OS resolver are honored
|
|
if (recordTypeArg === 'A' || recordTypeArg === 'AAAA') {
|
|
const family = recordTypeArg === 'A' ? 4 : 6;
|
|
const addresses = await new Promise<{ address: string }[]>((resolve, reject) => {
|
|
const timer = this.timeoutMs
|
|
? setTimeout(() => reject(new Error('system lookup timeout')), this.timeoutMs)
|
|
: null;
|
|
plugins.dns.lookup(
|
|
recordNameArg,
|
|
{ family, all: true },
|
|
(err, result) => {
|
|
if (timer) clearTimeout(timer as any);
|
|
if (err) return reject(err);
|
|
resolve(result || []);
|
|
}
|
|
);
|
|
});
|
|
return addresses.map((a) => ({
|
|
name: recordNameArg,
|
|
type: recordTypeArg,
|
|
dnsSecEnabled: false,
|
|
value: a.address,
|
|
}));
|
|
}
|
|
if (recordTypeArg === 'TXT') {
|
|
const records = await new Promise<string[][]>((resolve, reject) => {
|
|
const timer = this.timeoutMs
|
|
? setTimeout(() => reject(new Error('system resolveTxt timeout')), this.timeoutMs)
|
|
: null;
|
|
plugins.dns.resolveTxt(recordNameArg, (err, res) => {
|
|
if (timer) clearTimeout(timer as any);
|
|
if (err) return reject(err);
|
|
resolve(res || []);
|
|
});
|
|
});
|
|
return records.map((chunks) => ({
|
|
name: recordNameArg,
|
|
type: 'TXT',
|
|
dnsSecEnabled: false,
|
|
value: chunks.join(''),
|
|
}));
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const tryDoh = async (): Promise<plugins.tsclass.network.IDnsRecord[]> => {
|
|
const requestUrl = `https://cloudflare-dns.com/dns-query?name=${recordNameArg}&type=${recordTypeArg}&do=1`;
|
|
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
|
|
const getResponseBody = async (counterArg = 0): Promise<IDnsJsonResponse> => {
|
|
const response = await plugins.smartrequest.request(requestUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
accept: 'application/dns-json',
|
|
},
|
|
timeout: this.timeoutMs,
|
|
});
|
|
const responseBody: IDnsJsonResponse = response.body;
|
|
if (responseBody?.Status !== 0 && counterArg < retriesCounterArg) {
|
|
await plugins.smartdelay.delayFor(500);
|
|
return getResponseBody(counterArg + 1);
|
|
} else {
|
|
return responseBody;
|
|
}
|
|
};
|
|
const responseBody = await getResponseBody();
|
|
if (!responseBody || !responseBody.Answer || !typeof (responseBody.Answer as any)[Symbol.iterator]) {
|
|
return returnArray;
|
|
}
|
|
for (const dnsEntry of responseBody.Answer) {
|
|
if (typeof dnsEntry.data === 'string' && dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
|
dnsEntry.data = dnsEntry.data.replace(/^\"(.*)\"$/, '$1');
|
|
}
|
|
if (dnsEntry.name.endsWith('.')) {
|
|
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
|
|
}
|
|
returnArray.push({
|
|
name: dnsEntry.name,
|
|
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
|
|
dnsSecEnabled: !!responseBody.AD,
|
|
value: dnsEntry.data,
|
|
});
|
|
}
|
|
return returnArray;
|
|
};
|
|
|
|
try {
|
|
if (this.strategy === 'system') {
|
|
return await trySystem();
|
|
}
|
|
if (this.strategy === 'doh') {
|
|
return await tryDoh();
|
|
}
|
|
// prefer-system
|
|
try {
|
|
const sysRes = await trySystem();
|
|
if (sysRes.length > 0) return sysRes;
|
|
return this.allowDohFallback ? await tryDoh() : [];
|
|
} catch (err) {
|
|
return this.allowDohFallback ? await tryDoh() : [];
|
|
}
|
|
} catch (finalErr) {
|
|
return [];
|
|
}
|
|
}
|
|
for (const dnsEntry of responseBody.Answer) {
|
|
if (dnsEntry.data.startsWith('"') && dnsEntry.data.endsWith('"')) {
|
|
dnsEntry.data = dnsEntry.data.replace(/^"(.*)"$/, '$1');
|
|
}
|
|
if (dnsEntry.name.endsWith('.')) {
|
|
dnsEntry.name = dnsEntry.name.substring(0, dnsEntry.name.length - 1);
|
|
}
|
|
returnArray.push({
|
|
name: dnsEntry.name,
|
|
type: this.convertDnsTypeNumberToTypeName(dnsEntry.type),
|
|
dnsSecEnabled: responseBody.AD,
|
|
value: dnsEntry.data,
|
|
});
|
|
}
|
|
// console.log(responseBody);
|
|
return returnArray;
|
|
}
|
|
|
|
/**
|
|
* gets a record using nodejs dns resolver
|
|
*/
|
|
public async getRecordWithNodeDNS(
|
|
recordNameArg: string,
|
|
recordTypeArg: plugins.tsclass.network.TDnsRecordType
|
|
): Promise<plugins.tsclass.network.IDnsRecord[]> {
|
|
const done = plugins.smartpromise.defer<plugins.tsclass.network.IDnsRecord[]>();
|
|
plugins.dns.resolve(recordNameArg, recordTypeArg, (err, recordsArg) => {
|
|
if (err) {
|
|
done.reject(err);
|
|
return;
|
|
}
|
|
const returnArray: plugins.tsclass.network.IDnsRecord[] = [];
|
|
for (const recordKey in recordsArg) {
|
|
returnArray.push({
|
|
name: recordNameArg,
|
|
value: recordsArg[recordKey][0],
|
|
type: recordTypeArg,
|
|
dnsSecEnabled: false,
|
|
});
|
|
}
|
|
done.resolve(returnArray);
|
|
});
|
|
return done.promise;
|
|
}
|
|
|
|
public async getNameServers(domainNameArg: string): Promise<string[]> {
|
|
const done = plugins.smartpromise.defer<string[]>();
|
|
plugins.dns.resolveNs(domainNameArg, (err, result) => {
|
|
if (!err) {
|
|
done.resolve(result);
|
|
} else {
|
|
console.log(err);
|
|
done.reject(err);
|
|
}
|
|
});
|
|
return await done.promise;
|
|
}
|
|
|
|
public convertDnsTypeNameToTypeNumber(dnsTypeNameArg: string): number {
|
|
return this.dnsTypeMap[dnsTypeNameArg];
|
|
}
|
|
|
|
public convertDnsTypeNumberToTypeName(
|
|
dnsTypeNumberArg: number
|
|
): plugins.tsclass.network.TDnsRecordType {
|
|
for (const key in this.dnsTypeMap) {
|
|
if (this.dnsTypeMap[key] === dnsTypeNumberArg) {
|
|
return key as plugins.tsclass.network.TDnsRecordType;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|