/
mod.ts
201 lines (175 loc) · 7.08 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
export type BootstrapServiceRegistry = {
version: string; // e.g. "1.0"
publication: string; // e.g. "YYYY-MM-DDTHH:MM:SSZ"
description: string; // e.g. "Some text"
services: BootstrapService[];
};
export type BootstrapService = [Entry[], ServiceURL[]];
export type Entry = string;
export type ServiceURL = string;
export function getDNSBootstrapFile(): Promise<BootstrapServiceRegistry> {
const DNS_BOOTSTRAP_FILE_URL = "https://data.iana.org/rdap/dns.json";
return fetch(DNS_BOOTSTRAP_FILE_URL).then((response) => response.json());
}
export function getBootstrapServiceForTLD(
tld: string,
bootstrapFile: BootstrapServiceRegistry,
): BootstrapService | undefined {
return bootstrapFile.services.find((service) => service[0].includes(tld));
}
export function getServiceURLs(bootstrapService: BootstrapService): ServiceURL[] {
return bootstrapService[1];
}
export function queryServiceForDomain(serviceURL: ServiceURL, domain: string): Promise<Response> {
return fetch(`${serviceURL}domain/${domain}`);
}
export function checkDomainAvailability(serviceURL: ServiceURL, domain: string): Promise<boolean> {
return queryServiceForDomain(serviceURL, domain).then((response) => {
if (response.ok) {
return false;
} else if (response.status === 404) {
return true;
} else {
throw response;
}
});
}
const ALL_POSSIBLE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789-".split("");
const POSSIBLE_ALPHABET_CHARACTERS = "abcdefghijklmnopqrstuvwxyz".split("");
const POSSIBLE_NUMBER_CHARACTERS = "0123456789".split("");
export function findDomainNamesMatchingPattern(pattern: string): string[] {
// The author chose to filter out domains which are fundamentally invalid from the results
// to prevent reporting false positives to the user
return _findDomainNamesMatchingPattern(pattern).filter(
(domainName) =>
// Domain names cannot start or end with a hyphen ("-")
domainName.at(0) !== "-" &&
domainName.at(domainName.indexOf(".") - 1) !== "-" &&
// Domain names cannot contain consecutive hyphens ("-")
!domainName.includes("--") &&
// Domain names must only consist of the 26 letters of the ISO basic Latin alphabet
// in a case insensitive manner, numbers and hyphens ("-")
//
// Since domain names are separated from their top-level domain with a period character
// ("."), we ignore those while checking for the validity of the domain name.
domainName
.split("")
.every(
(char) => char === "." || ALL_POSSIBLE_CHARACTERS.includes(char.toLowerCase()),
),
);
}
function _findDomainNamesMatchingPattern(pattern: string): string[] {
const indexOfFirstAsterisk = pattern.indexOf("*");
const indexOfFirstQuestionMark = pattern.indexOf("?");
const indexOfFirstNumberSign = pattern.indexOf("#");
const wildcardIndexes = [
indexOfFirstAsterisk,
indexOfFirstQuestionMark,
indexOfFirstNumberSign,
];
if (Math.max(...wildcardIndexes) === -1) {
// No wildcard was found
return [pattern];
}
const firstWildcardIndex = Math.min(...wildcardIndexes.filter((n) => n != -1));
const replacementCharacters = firstWildcardIndex === indexOfFirstQuestionMark
? POSSIBLE_ALPHABET_CHARACTERS
: firstWildcardIndex === indexOfFirstNumberSign
? POSSIBLE_NUMBER_CHARACTERS
: ALL_POSSIBLE_CHARACTERS;
// Replace the first wildcard with all possible characters that could take its place and recurse
return replacementCharacters.flatMap((character) =>
_findDomainNamesMatchingPattern(
replaceIndexInString(pattern, character, firstWildcardIndex),
)
);
}
export function queryServiceForDomainOrRetry(
serviceURL: ServiceURL,
domain: string,
// The choice of 100 ms as a default for timeouts is almost arbitrary.
//
// The program was tested for performance on a ~700 queries long test set for the .net TLD
// with no chunks or chunks of size 50 and the following timeout settings:
//
// - 1 ms
// - 10 ms
// - 50 ms
// - 100 ms
// - 500 ms
// - 1000 ms
//
// Performance differences were within the margin of error for all of the timeout settings with
// the exception of the 1000 ms timeout tests which were slower by around 10 %.
//
// Since no conclusion could be taken from a performance point of view on the basis of these
// tests, the author chose 100 ms as a good middle ground between minimizing the performance
// impact of a failed request on a small query set and limiting unnecessary requests to the
// RDAP services.
waitMs = 100,
): Promise<Response> {
return resolveOrRetry(() => queryServiceForDomain(serviceURL, domain), waitMs);
}
export function checkDomainAvailabilityOrRetry(
serviceURL: ServiceURL,
domain: string,
waitMs = 100,
): Promise<boolean> {
return resolveOrRetry(() => checkDomainAvailability(serviceURL, domain), waitMs);
}
export function queryServiceForDomainsAsync(
serviceURL: ServiceURL,
domains: string[],
): Promise<Response[]> {
return Promise.all(domains.map((domain) => queryServiceForDomainOrRetry(serviceURL, domain)));
}
export function checkDomainsAvailabilityAsync(
serviceURL: ServiceURL,
domains: string[],
): Promise<boolean[]> {
return Promise.all(domains.map((domain) => checkDomainAvailabilityOrRetry(serviceURL, domain)));
}
export function queryServiceForDomainsSequential(
serviceURL: ServiceURL,
domains: string[],
): Promise<Response[]> {
return sequentialize(domains.map((domain) => () => queryServiceForDomain(serviceURL, domain)));
}
export function checkDomainsAvailabilitySequential(
serviceURL: ServiceURL,
domains: string[],
): Promise<boolean[]> {
return sequentialize(
domains.map((domain) => () => checkDomainAvailability(serviceURL, domain)),
);
}
export function splitIntoChunks<T>(arr: T[], maxChunkSize: number): T[][] {
const result = [];
const numberOfChunks = Math.ceil(arr.length / maxChunkSize);
for (let i = 0; i < numberOfChunks; i++) {
result.push(arr.slice(maxChunkSize * i, maxChunkSize * (i + 1)));
}
return result;
}
function resolveOrRetry<T>(f: () => Promise<T>, waitMs: number): Promise<T> {
return f().catch(() => sleep(waitMs).then(() => resolveOrRetry(f, waitMs)));
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function sequentialize<T>(fs: (() => Promise<T>)[]): Promise<T[]> {
const results: T[] = [];
return fs
.reduce(
(p, f) =>
p.then(f).then((response) => {
results.push(response);
}),
Promise.resolve(),
)
.then(() => results);
}
function replaceIndexInString(original: string, replacement: string, index: number): string {
return original.substring(0, index) + replacement + original.substring(index + 1);
}