Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/lib/Dns.ts
1030 views
1
import * as moment from 'moment';
2
import * as net from 'net';
3
import { promises as dns } from 'dns';
4
import { createPromise } from '@secret-agent/commons/utils';
5
import IResolvablePromise from '@secret-agent/interfaces/IResolvablePromise';
6
import IDnsSettings from '@secret-agent/interfaces/IDnsSettings';
7
import DnsOverTlsSocket from './DnsOverTlsSocket';
8
import RequestSession from '../handlers/RequestSession';
9
10
export class Dns {
11
public static dnsEntries = new Map<string, IResolvablePromise<IDnsEntry>>();
12
public socket: DnsOverTlsSocket;
13
private readonly dnsSettings: IDnsSettings = {};
14
15
constructor(private requestSession?: RequestSession) {
16
requestSession?.plugins?.onDnsConfiguration(this.dnsSettings);
17
}
18
19
public async lookupIp(host: string, retries = 3): Promise<string> {
20
// disable dns lookups when using a proxy
21
if (this.requestSession.upstreamProxyUrl) return host;
22
if (!this.dnsSettings.dnsOverTlsConnection || host === 'localhost' || net.isIP(host))
23
return host;
24
25
try {
26
// get cached (or in process resolver)
27
const cachedRecord = await this.getNextCachedARecord(host);
28
if (cachedRecord) return cachedRecord;
29
} catch (error) {
30
if (retries === 0) throw error;
31
// if the cache lookup failed, likely because another lookup failed... try again
32
return this.lookupIp(host, retries - 1);
33
}
34
35
// if not found in cache, perform dns lookup
36
let lookupError: Error;
37
try {
38
const dnsEntry = await this.lookupDnsEntry(host);
39
const ip = this.nextIp(dnsEntry);
40
if (ip) return ip;
41
} catch (error) {
42
lookupError = error;
43
}
44
45
// try to resolve using system interface
46
try {
47
const dnsEntry = await this.systemLookup(host);
48
return this.nextIp(dnsEntry);
49
} catch (error) {
50
// don't throw error, throw original error
51
throw lookupError;
52
}
53
}
54
55
public close(): void {
56
this.socket?.close();
57
this.socket = null;
58
this.requestSession = null;
59
}
60
61
private async systemLookup(host: string): Promise<IDnsEntry> {
62
const dnsEntry = createPromise<IDnsEntry>(10e3);
63
Dns.dnsEntries.set(host, dnsEntry);
64
try {
65
const lookupAddresses = await dns.lookup(host.split(':').shift(), {
66
all: true,
67
family: 4,
68
});
69
const entry = <IDnsEntry>{
70
aRecords: lookupAddresses.map(x => ({
71
expiry: moment().add(10, 'minutes').toDate(),
72
ip: x.address,
73
})),
74
};
75
dnsEntry.resolve(entry);
76
} catch (error) {
77
dnsEntry.reject(error);
78
Dns.dnsEntries.delete(host);
79
}
80
return dnsEntry.promise;
81
}
82
83
private async lookupDnsEntry(host: string): Promise<IDnsEntry> {
84
const existing = Dns.dnsEntries.get(host);
85
if (existing && !existing.isResolved) return existing.promise;
86
87
const dnsEntry = createPromise<IDnsEntry>(10e3);
88
Dns.dnsEntries.set(host, dnsEntry);
89
try {
90
if (!this.socket) {
91
this.socket = new DnsOverTlsSocket(
92
this.dnsSettings,
93
this.requestSession,
94
() => (this.socket = null),
95
);
96
}
97
98
const response = await this.socket.lookupARecords(host);
99
100
const entry = <IDnsEntry>{
101
aRecords: response.answers
102
.filter(x => x.type === 'A') // gives non-query records sometimes
103
.map(x => ({
104
ip: x.data,
105
expiry: moment().add(x.ttl, 'seconds').toDate(),
106
})),
107
};
108
dnsEntry.resolve(entry);
109
} catch (error) {
110
dnsEntry.reject(error);
111
Dns.dnsEntries.delete(host);
112
}
113
return dnsEntry.promise;
114
}
115
116
private nextIp(dnsEntry: IDnsEntry): string {
117
// implement rotating
118
for (let i = 0; i < dnsEntry.aRecords.length; i += 1) {
119
const record = dnsEntry.aRecords[i];
120
if (record.expiry > new Date()) {
121
// move record to back
122
dnsEntry.aRecords.splice(i, 1);
123
dnsEntry.aRecords.push(record);
124
return record.ip;
125
}
126
}
127
return null;
128
}
129
130
private async getNextCachedARecord(name: string): Promise<string> {
131
const cached = await Dns.dnsEntries.get(name)?.promise;
132
if (cached?.aRecords?.length) {
133
return this.nextIp(cached);
134
}
135
return null;
136
}
137
}
138
139
interface IDnsEntry {
140
aRecords: { ip: string; expiry: Date }[];
141
}
142
143