@chainfuse/helpers 4.2.14 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dns.d.mts +81 -71
- package/dist/dns.mjs +325 -35
- package/package.json +6 -4
package/dist/dns.d.mts
CHANGED
|
@@ -1,74 +1,84 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import type { CacheStorageLike } from '@chainfuse/types';
|
|
2
|
+
import type { DurableObjectState, ExecutionContext } from '@cloudflare/workers-types/experimental';
|
|
3
|
+
import * as dnsPacket from 'dns-packet';
|
|
4
|
+
import * as zm from 'zod/mini';
|
|
5
|
+
export declare enum DNSRecordType {
|
|
6
|
+
'A (IPv4 Address)' = "A",
|
|
7
|
+
'AAAA (IPv6 Address)' = "AAAA",
|
|
8
|
+
'AFSDB (AFS database)' = "AFSDB",
|
|
9
|
+
'APL (Address Prefix List)' = "APL",
|
|
10
|
+
'AXFR (Zone transfer)' = "AXFR",
|
|
11
|
+
'CAA (CA authorizations)' = "CAA",
|
|
12
|
+
'CDNSKEY (Child DNSKEY)' = "CDNSKEY",
|
|
13
|
+
'CDS (Child DS)' = "CDS",
|
|
14
|
+
'CERT (Certificate)' = "CERT",
|
|
15
|
+
'CNAME (Canonical Name)' = "CNAME",
|
|
16
|
+
'DNAME (Delegation Name)' = "DNAME",
|
|
17
|
+
'DHCID (DHCP Identifier)' = "DHCID",
|
|
18
|
+
'DLV (DNSSEC Lookaside Validation)' = "DLV",
|
|
19
|
+
'DNSKEY (DNSSEC Key)' = "DNSKEY",
|
|
20
|
+
'DS (Delegation Signer)' = "DS",
|
|
21
|
+
'HINFO (Host Info)' = "HINFO",
|
|
22
|
+
'HIP (Host Identity Protocol)' = "HIP",
|
|
23
|
+
'IXFR (Incremental Zone Transfer)' = "IXFR",
|
|
24
|
+
'IPSECKEY (IPSEC Key)' = "IPSECKEY",
|
|
25
|
+
'KEY (Key record)' = "KEY",
|
|
26
|
+
'KX (Key Exchanger)' = "KX",
|
|
27
|
+
'LOC (Location)' = "LOC",
|
|
28
|
+
'MX (Mail Exchange)' = "MX",
|
|
29
|
+
'NAPTR (Name Authority Pointer)' = "NAPTR",
|
|
30
|
+
'NS (Name Server)' = "NS",
|
|
31
|
+
'NSEC (Next Secure)' = "NSEC",
|
|
32
|
+
'NSEC3 (Next Secure v3)' = "NSEC3",
|
|
33
|
+
'NSEC3PARAM (NSEC3 Parameters)' = "NSEC3PARAM",
|
|
34
|
+
'NULL (Experimental null RR)' = "NULL",
|
|
35
|
+
'OPT (EDNS Options)' = "OPT",
|
|
36
|
+
'PTR (Pointer)' = "PTR",
|
|
37
|
+
'RRSIG (DNSSEC Signature)' = "RRSIG",
|
|
38
|
+
'RP (Responsible Person)' = "RP",
|
|
39
|
+
'SIG (Signature)' = "SIG",
|
|
40
|
+
'SOA (Start of Authority)' = "SOA",
|
|
41
|
+
'SRV (Service)' = "SRV",
|
|
42
|
+
'SSHFP (SSH Fingerprint)' = "SSHFP",
|
|
43
|
+
'TA (DNSSEC Trust Anchor)' = "TA",
|
|
44
|
+
'TKEY (Transaction Key)' = "TKEY",
|
|
45
|
+
'TLSA (certificate associations)' = "TLSA",
|
|
46
|
+
'TSIG (Transaction Signature)' = "TSIG",
|
|
47
|
+
'TXT (Text)' = "TXT",
|
|
48
|
+
'URI (Uniform Resource Identifier)' = "URI"
|
|
49
|
+
}
|
|
50
|
+
export declare class DnsHelpers<C extends CacheStorageLike, EC extends Pick<ExecutionContext | DurableObjectState, 'waitUntil'> = Pick<ExecutionContext | DurableObjectState, 'waitUntil'>> {
|
|
51
|
+
private nameservers;
|
|
52
|
+
private cache?;
|
|
53
|
+
private backgroundContext?;
|
|
54
|
+
static readonly constructorArgs: zm.ZodMiniObject<{
|
|
55
|
+
nameservers: zm.ZodMiniArray<zm.ZodMiniCodec<zm.ZodMiniURL, zm.ZodMiniCustom<URL, URL>>>;
|
|
56
|
+
}, zm.z.core.$strip>;
|
|
14
57
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
58
|
+
* Create a DNS helper instance.
|
|
59
|
+
* @param args Parsed constructor args containing the resolver nameserver URLs.
|
|
60
|
+
* @param cacheStore Optional CacheStorage-like implementation to persist DNS lookups; if null, cache is disabled.
|
|
17
61
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
TC: boolean;
|
|
41
|
-
/** True if the Recursive Desired bit was set. This is always set to true for Cloudflare DNS over HTTPS */
|
|
42
|
-
RD: boolean;
|
|
43
|
-
/** True if the Recursion Available bit was set. This is always set to true for Cloudflare DNS over HTTPS */
|
|
44
|
-
RA: boolean;
|
|
45
|
-
/** True if every record in the answer was verified with DNSSEC */
|
|
46
|
-
AD: boolean;
|
|
47
|
-
/** True if the client asked to disable DNSSEC validation. In this case, Cloudflare will still fetch the DNSSEC-related records, but it will not attempt to validate the records */
|
|
48
|
-
CD: boolean;
|
|
49
|
-
/** The record name requested */
|
|
50
|
-
Question: Record[];
|
|
51
|
-
/** The answer record */
|
|
52
|
-
Answer?: Record[];
|
|
53
|
-
/** The authority record */
|
|
54
|
-
Authority: Record[];
|
|
55
|
-
/** The additional record */
|
|
56
|
-
Additional: Record[];
|
|
57
|
-
/** List of EDE messages. Refer to Extended DNS error codes for more information */
|
|
58
|
-
Comment?: string[];
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* An error response from Cloudflare's 1.1.1.1 DNS over HTTPS API
|
|
62
|
-
*/
|
|
63
|
-
export interface DohErrorResponse {
|
|
64
|
-
/** An explanation of the error that occurred */
|
|
65
|
-
error: string;
|
|
66
|
-
}
|
|
67
|
-
export declare class DnsHelpers {
|
|
68
|
-
private nameserver_url;
|
|
69
|
-
constructor(nameserver_url: string | URL);
|
|
70
|
-
query(qName: string, qType?: string | number, qDo?: string | number | boolean, qCd?: string | number | boolean, timeout?: number): Promise<DohSuccessfulResponse | DohErrorResponse>;
|
|
71
|
-
private makeGetQuery;
|
|
72
|
-
private sendDohMsg;
|
|
62
|
+
constructor(args: zm.input<(typeof DnsHelpers)['constructorArgs']>, cacheStore?: C | null, backgroundContext?: EC);
|
|
63
|
+
static readonly queryArgs: zm.ZodMiniObject<{
|
|
64
|
+
questions: zm.ZodMiniArray<zm.ZodMiniObject<{
|
|
65
|
+
hostname: zm.ZodMiniString<string>;
|
|
66
|
+
recordType: zm.ZodMiniDefault<zm.ZodMiniEnum<typeof DNSRecordType>>;
|
|
67
|
+
}, zm.z.core.$strip>>;
|
|
68
|
+
flags: zm.ZodMiniDefault<zm.ZodMiniObject<{
|
|
69
|
+
recursion: zm.ZodMiniDefault<zm.ZodMiniBoolean<boolean>>;
|
|
70
|
+
dnssecCheck: zm.ZodMiniDefault<zm.ZodMiniBoolean<boolean>>;
|
|
71
|
+
}, zm.z.core.$strip>>;
|
|
72
|
+
timeout: zm.ZodMiniUnion<readonly [zm.ZodMiniCustom<AbortSignal, AbortSignal>, zm.ZodMiniPipe<zm.ZodMiniDefault<zm.ZodMiniNumberFormat>, zm.ZodMiniTransform<AbortSignal, number>>]>;
|
|
73
|
+
}, zm.z.core.$strip>;
|
|
74
|
+
query(_args: zm.input<(typeof DnsHelpers)['queryArgs']>): Promise<{
|
|
75
|
+
readonly flags: {
|
|
76
|
+
readonly 'Official Authority': boolean;
|
|
77
|
+
readonly 'DNSSEC Verified': boolean;
|
|
78
|
+
readonly 'Recursion Available': boolean;
|
|
79
|
+
readonly Truncated: boolean;
|
|
80
|
+
};
|
|
81
|
+
readonly answers: dnsPacket.Answer[] | undefined;
|
|
82
|
+
}>;
|
|
83
|
+
private _query;
|
|
73
84
|
}
|
|
74
|
-
export {};
|
package/dist/dns.mjs
CHANGED
|
@@ -1,46 +1,336 @@
|
|
|
1
|
+
import * as dnsPacket from 'dns-packet';
|
|
2
|
+
import * as zm from 'zod/mini';
|
|
3
|
+
import { CryptoHelpers } from "./crypto.mjs";
|
|
4
|
+
export var DNSRecordType;
|
|
5
|
+
(function (DNSRecordType) {
|
|
6
|
+
DNSRecordType["A (IPv4 Address)"] = "A";
|
|
7
|
+
DNSRecordType["AAAA (IPv6 Address)"] = "AAAA";
|
|
8
|
+
DNSRecordType["AFSDB (AFS database)"] = "AFSDB";
|
|
9
|
+
DNSRecordType["APL (Address Prefix List)"] = "APL";
|
|
10
|
+
DNSRecordType["AXFR (Zone transfer)"] = "AXFR";
|
|
11
|
+
DNSRecordType["CAA (CA authorizations)"] = "CAA";
|
|
12
|
+
DNSRecordType["CDNSKEY (Child DNSKEY)"] = "CDNSKEY";
|
|
13
|
+
DNSRecordType["CDS (Child DS)"] = "CDS";
|
|
14
|
+
DNSRecordType["CERT (Certificate)"] = "CERT";
|
|
15
|
+
DNSRecordType["CNAME (Canonical Name)"] = "CNAME";
|
|
16
|
+
DNSRecordType["DNAME (Delegation Name)"] = "DNAME";
|
|
17
|
+
DNSRecordType["DHCID (DHCP Identifier)"] = "DHCID";
|
|
18
|
+
DNSRecordType["DLV (DNSSEC Lookaside Validation)"] = "DLV";
|
|
19
|
+
DNSRecordType["DNSKEY (DNSSEC Key)"] = "DNSKEY";
|
|
20
|
+
DNSRecordType["DS (Delegation Signer)"] = "DS";
|
|
21
|
+
DNSRecordType["HINFO (Host Info)"] = "HINFO";
|
|
22
|
+
DNSRecordType["HIP (Host Identity Protocol)"] = "HIP";
|
|
23
|
+
DNSRecordType["IXFR (Incremental Zone Transfer)"] = "IXFR";
|
|
24
|
+
DNSRecordType["IPSECKEY (IPSEC Key)"] = "IPSECKEY";
|
|
25
|
+
DNSRecordType["KEY (Key record)"] = "KEY";
|
|
26
|
+
DNSRecordType["KX (Key Exchanger)"] = "KX";
|
|
27
|
+
DNSRecordType["LOC (Location)"] = "LOC";
|
|
28
|
+
DNSRecordType["MX (Mail Exchange)"] = "MX";
|
|
29
|
+
DNSRecordType["NAPTR (Name Authority Pointer)"] = "NAPTR";
|
|
30
|
+
DNSRecordType["NS (Name Server)"] = "NS";
|
|
31
|
+
DNSRecordType["NSEC (Next Secure)"] = "NSEC";
|
|
32
|
+
DNSRecordType["NSEC3 (Next Secure v3)"] = "NSEC3";
|
|
33
|
+
DNSRecordType["NSEC3PARAM (NSEC3 Parameters)"] = "NSEC3PARAM";
|
|
34
|
+
DNSRecordType["NULL (Experimental null RR)"] = "NULL";
|
|
35
|
+
DNSRecordType["OPT (EDNS Options)"] = "OPT";
|
|
36
|
+
DNSRecordType["PTR (Pointer)"] = "PTR";
|
|
37
|
+
DNSRecordType["RRSIG (DNSSEC Signature)"] = "RRSIG";
|
|
38
|
+
DNSRecordType["RP (Responsible Person)"] = "RP";
|
|
39
|
+
DNSRecordType["SIG (Signature)"] = "SIG";
|
|
40
|
+
DNSRecordType["SOA (Start of Authority)"] = "SOA";
|
|
41
|
+
DNSRecordType["SRV (Service)"] = "SRV";
|
|
42
|
+
DNSRecordType["SSHFP (SSH Fingerprint)"] = "SSHFP";
|
|
43
|
+
DNSRecordType["TA (DNSSEC Trust Anchor)"] = "TA";
|
|
44
|
+
DNSRecordType["TKEY (Transaction Key)"] = "TKEY";
|
|
45
|
+
DNSRecordType["TLSA (certificate associations)"] = "TLSA";
|
|
46
|
+
DNSRecordType["TSIG (Transaction Signature)"] = "TSIG";
|
|
47
|
+
DNSRecordType["TXT (Text)"] = "TXT";
|
|
48
|
+
DNSRecordType["URI (Uniform Resource Identifier)"] = "URI";
|
|
49
|
+
})(DNSRecordType || (DNSRecordType = {}));
|
|
1
50
|
export class DnsHelpers {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
51
|
+
nameservers;
|
|
52
|
+
cache;
|
|
53
|
+
backgroundContext;
|
|
54
|
+
static constructorArgs = zm.object({
|
|
55
|
+
nameservers: zm
|
|
56
|
+
.array(zm.codec(zm.url({ protocol: /^(https|tls)$/, hostname: zm.regexes.domain }).check(zm.trim(), zm.minLength(1)), zm.instanceof(URL), {
|
|
57
|
+
decode: (urlString) => new URL(urlString),
|
|
58
|
+
encode: (url) => url.href,
|
|
59
|
+
}))
|
|
60
|
+
.check(zm.minLength(1)),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Create a DNS helper instance.
|
|
64
|
+
* @param args Parsed constructor args containing the resolver nameserver URLs.
|
|
65
|
+
* @param cacheStore Optional CacheStorage-like implementation to persist DNS lookups; if null, cache is disabled.
|
|
66
|
+
*/
|
|
67
|
+
constructor(args, cacheStore, backgroundContext) {
|
|
68
|
+
const { nameservers } = DnsHelpers.constructorArgs.parse(args);
|
|
69
|
+
this.nameservers = nameservers;
|
|
70
|
+
if (cacheStore !== null) {
|
|
71
|
+
cacheStore ??= globalThis.caches;
|
|
72
|
+
if ('open' in cacheStore && typeof cacheStore.open === 'function') {
|
|
73
|
+
this.cache = cacheStore.open('dns');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new Error('Cache store must be a CacheStorage (or equivalent of)');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.backgroundContext = backgroundContext;
|
|
5
80
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
81
|
+
static queryArgs = zm.object({
|
|
82
|
+
questions: zm
|
|
83
|
+
.array(zm.object({
|
|
84
|
+
hostname: zm.string().check(zm.trim(), zm.minLength(1),
|
|
85
|
+
// DNS dpec is 255 - final period and non-printed zero octect for root
|
|
86
|
+
zm.maxLength(253), zm.regex(zm.regexes.domain)),
|
|
87
|
+
recordType: zm._default(zm.enum(DNSRecordType), DNSRecordType['A (IPv4 Address)']),
|
|
88
|
+
}))
|
|
89
|
+
.check(zm.minLength(1)),
|
|
90
|
+
flags: zm._default(zm.object({
|
|
91
|
+
recursion: zm._default(zm.boolean(), true),
|
|
92
|
+
dnssecCheck: zm._default(zm.boolean(), true),
|
|
93
|
+
}), { recursion: true, dnssecCheck: true }),
|
|
94
|
+
timeout: zm.union([
|
|
95
|
+
zm.instanceof(AbortSignal),
|
|
96
|
+
zm.pipe(zm._default(zm.int().check(zm.positive()), 30 * 1000), zm.transform((ms) => AbortSignal.timeout(ms))),
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
query(_args) {
|
|
100
|
+
const args = DnsHelpers.queryArgs.parse(_args);
|
|
101
|
+
// Throw immediately if already aborted
|
|
102
|
+
args.timeout.throwIfAborted();
|
|
103
|
+
return Promise.race([
|
|
104
|
+
// Passthrough signal and carry over the rest
|
|
105
|
+
this._query(args.timeout, args.questions, args.flags),
|
|
106
|
+
// Shortcircuit on abort
|
|
107
|
+
new Promise((_, reject) => args.timeout.addEventListener('abort', () => {
|
|
108
|
+
if (args.timeout.reason instanceof DOMException) {
|
|
109
|
+
reject(new Error(`${args.timeout.reason.name}: ${args.timeout.reason.message}`, { cause: args.timeout.reason.cause }));
|
|
110
|
+
}
|
|
111
|
+
else if (args.timeout.reason instanceof Error) {
|
|
112
|
+
reject(args.timeout.reason);
|
|
113
|
+
}
|
|
114
|
+
else if (typeof args.timeout.reason === 'string') {
|
|
115
|
+
reject(new Error(args.timeout.reason));
|
|
116
|
+
}
|
|
117
|
+
else if (args.timeout.reason) {
|
|
118
|
+
reject(new Error(JSON.stringify(args.timeout.reason)));
|
|
12
119
|
}
|
|
13
120
|
else {
|
|
14
|
-
|
|
15
|
-
reject(response.status);
|
|
121
|
+
reject(new Error('AbortError'));
|
|
16
122
|
}
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
});
|
|
123
|
+
}, { once: true })),
|
|
124
|
+
]);
|
|
20
125
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
126
|
+
async _query(signal, questions, flags) {
|
|
127
|
+
let cacheRequest;
|
|
128
|
+
let computedflags = 0;
|
|
129
|
+
if ('recursion' in flags && flags.recursion === true) {
|
|
130
|
+
computedflags |= dnsPacket.RECURSION_DESIRED;
|
|
131
|
+
}
|
|
132
|
+
if ('dnssecCheck' in flags) {
|
|
133
|
+
if (flags.dnssecCheck) {
|
|
134
|
+
computedflags |= dnsPacket.DNSSEC_OK;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
computedflags |= dnsPacket.CHECKING_DISABLED;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let responsePacket;
|
|
141
|
+
const errors = [];
|
|
142
|
+
for (const nameserver of this.nameservers) {
|
|
143
|
+
// Reset
|
|
144
|
+
cacheRequest = undefined;
|
|
145
|
+
responsePacket = undefined;
|
|
146
|
+
if (this.cache) {
|
|
147
|
+
const cacheServerUrl = new URL(nameserver);
|
|
148
|
+
// Cache hard requires http or https protocol
|
|
149
|
+
cacheServerUrl.protocol = 'https:';
|
|
150
|
+
// For cache niceness, follow similar to `application/dns-json` format
|
|
151
|
+
cacheServerUrl.searchParams.set('questions', await CryptoHelpers.getHash('SHA-256', JSON.stringify(questions)));
|
|
152
|
+
cacheServerUrl.searchParams.set('flags', computedflags.toString());
|
|
153
|
+
cacheRequest = new Request(cacheServerUrl, { headers: { Accept: 'application/dns-json' }, signal });
|
|
154
|
+
responsePacket = await (await this.cache).match(cacheRequest).then((response) => {
|
|
155
|
+
if (response?.ok) {
|
|
156
|
+
return response.json().then((json) => json);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const fromCache = Boolean(responsePacket);
|
|
164
|
+
try {
|
|
165
|
+
responsePacket ??= await (() => {
|
|
166
|
+
const queryPacket = {
|
|
167
|
+
type: 'query',
|
|
168
|
+
// 1 (inclusive) to 65536 (exclusive)
|
|
169
|
+
id: Math.floor(Math.random() * 65535) + 1,
|
|
170
|
+
flags: computedflags,
|
|
171
|
+
questions: questions.map((q) => ({
|
|
172
|
+
name: q.hostname,
|
|
173
|
+
type: q.recordType,
|
|
174
|
+
class: 'IN',
|
|
175
|
+
})),
|
|
176
|
+
};
|
|
177
|
+
if (nameserver.protocol === 'https:') {
|
|
178
|
+
const dnsQueryBuf = dnsPacket.encode(queryPacket);
|
|
179
|
+
return fetch(nameserver, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': 'application/dns-message',
|
|
183
|
+
Accept: 'application/dns-message',
|
|
184
|
+
},
|
|
185
|
+
signal,
|
|
186
|
+
body: new Uint8Array(dnsQueryBuf),
|
|
187
|
+
})
|
|
188
|
+
.then(async (response) => {
|
|
189
|
+
if (response.ok) {
|
|
190
|
+
return response.arrayBuffer();
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error(`${response?.status} ${response?.statusText}`, { cause: await response?.text() });
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.then((buf) => import('node:buffer').then(({ Buffer }) => dnsPacket.decode(Buffer.from(buf))));
|
|
197
|
+
}
|
|
198
|
+
else if (nameserver.protocol === 'tls:') {
|
|
199
|
+
const dnsQueryBuf = dnsPacket.streamEncode(queryPacket);
|
|
200
|
+
return Promise.all([import('node:tls'), import('node:buffer')]).then(([{ connect }, { Buffer }]) => new Promise((resolve, reject) => {
|
|
201
|
+
// Setup TLS client
|
|
202
|
+
const client = connect({
|
|
203
|
+
// RFC 7858 requires 1.2+
|
|
204
|
+
minVersion: 'TLSv1.2',
|
|
205
|
+
port: nameserver.port === '' ? 853 : parseInt(nameserver.port, 10),
|
|
206
|
+
host: nameserver.hostname,
|
|
207
|
+
...(nameserver.pathname !== '' && { path: nameserver.pathname }),
|
|
208
|
+
}, () => {
|
|
209
|
+
client.write(dnsQueryBuf);
|
|
210
|
+
});
|
|
211
|
+
// Setup abort handling
|
|
212
|
+
const onAbort = () => {
|
|
213
|
+
const error = (() => {
|
|
214
|
+
if (signal.reason instanceof DOMException) {
|
|
215
|
+
return new Error(`${signal.reason.name}: ${signal.reason.message}`, { cause: signal.reason.cause });
|
|
216
|
+
}
|
|
217
|
+
else if (signal.reason instanceof Error) {
|
|
218
|
+
return signal.reason;
|
|
219
|
+
}
|
|
220
|
+
else if (typeof signal.reason === 'string') {
|
|
221
|
+
return new Error(signal.reason);
|
|
222
|
+
}
|
|
223
|
+
else if (signal.reason) {
|
|
224
|
+
return new Error(JSON.stringify(signal.reason));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
return new Error('AbortError');
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
client.destroy(error);
|
|
231
|
+
reject(error);
|
|
232
|
+
};
|
|
233
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
234
|
+
// Finish setting up client
|
|
235
|
+
client.once('error', reject);
|
|
236
|
+
let rawBody = Buffer.from(new Uint8Array(0));
|
|
237
|
+
let expectedLength = 0;
|
|
238
|
+
client.on('data', (data) => {
|
|
239
|
+
if (rawBody.byteLength === 0) {
|
|
240
|
+
expectedLength = data.readUInt16BE(0);
|
|
241
|
+
if (expectedLength < 12) {
|
|
242
|
+
reject(new Error('Below DNS minimum packet length (DNS Header is 12 bytes)'));
|
|
243
|
+
}
|
|
244
|
+
rawBody = Buffer.from(data);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
rawBody = Buffer.concat([rawBody, data]);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* @link https://tools.ietf.org/html/rfc7858#section-3.3
|
|
251
|
+
* @link https://tools.ietf.org/html/rfc1035#section-4.2.2
|
|
252
|
+
* The message is prefixed with a two byte length field which gives the message length, excluding the two byte length field.
|
|
253
|
+
*/
|
|
254
|
+
if (rawBody.length === expectedLength + 2) {
|
|
255
|
+
client.destroy();
|
|
256
|
+
resolve(dnsPacket.streamDecode(rawBody));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
client.once('end', () => signal.removeEventListener('abort', onAbort));
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
throw new Error(`Unsupported protocol: ${nameserver.protocol}`);
|
|
264
|
+
}
|
|
265
|
+
})().then(async (packet) => {
|
|
266
|
+
const formattedPacket = {
|
|
267
|
+
...packet,
|
|
268
|
+
answers: await import('node:buffer').then(({ Buffer }) => (packet.answers ?? []).map((a) => {
|
|
269
|
+
if ('data' in a) {
|
|
270
|
+
if (Array.isArray(a.data)) {
|
|
271
|
+
return { ...a, data: a.data.map((part) => (typeof part === 'string' ? part : part.toString('utf8'))) };
|
|
272
|
+
}
|
|
273
|
+
else if (Buffer.isBuffer(a.data)) {
|
|
274
|
+
return { ...a, data: a.data.toString('utf8') };
|
|
275
|
+
}
|
|
276
|
+
else if (typeof a.data === 'object') {
|
|
277
|
+
return { ...a, data: a.data };
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
return { ...a, data: a.data.toString() };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
return a;
|
|
285
|
+
}
|
|
286
|
+
})),
|
|
287
|
+
};
|
|
288
|
+
if (this.cache && !fromCache) {
|
|
289
|
+
// Re-assign response to make it mutable
|
|
290
|
+
const cacheResponse = new Response(JSON.stringify(formattedPacket), { headers: { 'Content-Type': 'application/dns-json' } });
|
|
291
|
+
const cachePromise = (async () => {
|
|
292
|
+
const answersWithTtl = (formattedPacket.answers ?? []).filter((a) => 'ttl' in a);
|
|
293
|
+
const ttl = answersWithTtl.length > 0 ? Math.min(...answersWithTtl.map((a) => ('ttl' in a ? a.ttl : 0))) : 0;
|
|
294
|
+
cacheResponse.headers.set('Cache-Control', `public, max-age=${ttl}, s-maxage=${ttl}`);
|
|
295
|
+
await CryptoHelpers.generateETag(cacheResponse)
|
|
296
|
+
.then((etag) => cacheResponse.headers.set('ETag', etag))
|
|
297
|
+
.catch((err) => console.warn('ETag generation failed', err));
|
|
298
|
+
return (await this.cache).put(cacheRequest, cacheResponse);
|
|
299
|
+
})();
|
|
300
|
+
if (this.backgroundContext) {
|
|
301
|
+
this.backgroundContext.waitUntil(cachePromise);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
await cachePromise;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Go back to nice JSON format
|
|
308
|
+
return formattedPacket;
|
|
309
|
+
});
|
|
310
|
+
if (responsePacket)
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (responsePacket) {
|
|
319
|
+
return {
|
|
320
|
+
flags: {
|
|
321
|
+
'Official Authority': Boolean((responsePacket.flags ?? 0) & dnsPacket.AUTHORITATIVE_ANSWER),
|
|
322
|
+
'DNSSEC Verified': Boolean((responsePacket.flags ?? 0) & dnsPacket.AUTHENTIC_DATA),
|
|
323
|
+
'Recursion Available': Boolean((responsePacket.flags ?? 0) & dnsPacket.RECURSION_AVAILABLE),
|
|
324
|
+
Truncated: Boolean((responsePacket.flags ?? 0) & dnsPacket.TRUNCATED_RESPONSE),
|
|
325
|
+
},
|
|
326
|
+
answers: responsePacket.answers,
|
|
327
|
+
};
|
|
41
328
|
}
|
|
42
329
|
else {
|
|
43
|
-
|
|
330
|
+
if (errors.length > 0) {
|
|
331
|
+
throw new AggregateError(errors, 'No nameserver responded');
|
|
332
|
+
}
|
|
333
|
+
throw new Error('No nameserver responded');
|
|
44
334
|
}
|
|
45
335
|
}
|
|
46
336
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainfuse/helpers",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "ChainFuse",
|
|
6
6
|
"homepage": "https://github.com/ChainFuse/packages/tree/main/packages/helpers#readme",
|
|
@@ -84,13 +84,15 @@
|
|
|
84
84
|
"@discordjs/rest": "^2.6.0",
|
|
85
85
|
"chalk": "^5.6.2",
|
|
86
86
|
"cloudflare": "^5.2.0",
|
|
87
|
+
"dns-packet": "^5.6.1",
|
|
87
88
|
"drizzle-orm": "^0.45.1",
|
|
88
89
|
"strip-ansi": "^7.1.2",
|
|
89
90
|
"uuid": "^13.0.0",
|
|
90
|
-
"zod": "^4.3.
|
|
91
|
+
"zod": "^4.3.6"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
|
93
|
-
"@cloudflare/workers-types": "^4.
|
|
94
|
+
"@cloudflare/workers-types": "^4.20260128.0",
|
|
95
|
+
"@types/dns-packet": "^5.6.5"
|
|
94
96
|
},
|
|
95
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "3a294f5b789b6beedafb3aa9925a797c26fdc00a"
|
|
96
98
|
}
|