-
Notifications
You must be signed in to change notification settings - Fork 6
feat: update dns-over-https default resolver #111
feat: update dns-over-https default resolver #111
Conversation
This also refactors `dns-resolvers/default.ts` so that the recursive logic of the `defaultResolver` will be shared between browser and nodejs consumers.
bdb49d0
to
a35be43
Compare
This fixes an error seen in CI when response.Answer.filter() was called and we got an error: TypeError: Cannot read properties of undefined (reading 'filter') at findDNSLinkAnswer (file:///home/runner/work/helia-ipns/helia-ipns/packages/ipns/src/utils/dns.ts:45:34) at ipfsPath (file:///home/runner/work/helia-ipns/helia-ipns/packages/ipns/src/utils/dns.ts:29:18) at httpQueue.add.signal (file:///home/runner/work/helia-ipns/helia-ipns/packages/ipns/src/dns-resolvers/dns-json-over-https.ts:71:22)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self review
const makeCall = async (): Promise<void> => { | ||
await resolver('ipfs.io', { | ||
onProgress: (evt) => { | ||
if (evt.type.includes('dnslink:cache')) { | ||
usedCache = true | ||
} | ||
} | ||
}) | ||
} | ||
|
||
for (let i = 0; i < 10; i++) { | ||
// resolve again, should use the cache | ||
// TTL can be pretty low which means this can be flaky if executed more slowly than the TTL | ||
await makeCall() | ||
if (usedCache) { | ||
break | ||
} | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it(`${name} should abort if signal is aborted`, async () => { | ||
const signal = AbortSignal.timeout(1) | ||
|
||
await expect(resolver('ipfs.io', { signal })).to.eventually.be.rejected() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check that abort signals are being respected (was missing in coverage)
// if the result is a CID, or depth is 1, we've reached the end of the recursion | ||
// if depth is 1, another recursive call will be made, but it would throw. | ||
// we could return if depth is 1 and allow users to handle, but that may be a breaking change | ||
if (isIPFSCID) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed depth === 0 because that's not possible due to above check. see #55 (comment)
// result is now a `/ipfs/<cid>` or `/ipns/<cid>` string | ||
const domainOrCID = result.split('/')[2] // e.g. ["", "ipfs", "<cid>"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just some comments
@@ -59,7 +59,7 @@ export const findTTL = (domain: string, response: DNSResponse): number => { | |||
|
|||
export const MAX_RECURSIVE_DEPTH = 32 | |||
|
|||
export const recursiveResolveDnslink = async (domain: string, depth: number, resolve: (domain: string, options: ResolveDnsLinkOptions) => Promise<string>, options: ResolveDnsLinkOptions = {}): Promise<string> => { | |||
export const recursiveResolveDnslink = async (domain: string, depth: number, resolve: DNSResolver, options: ResolveDnsLinkOptions = {}): Promise<string> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the defined type
@@ -167,15 +167,22 @@ export interface DNSResolver { | |||
(domain: string, options?: ResolveDnsLinkOptions): Promise<string> | |||
} | |||
|
|||
export interface ResolveDNSOptions extends AbortOptions, ProgressOptions<ResolveDnsLinkProgressEvents> { | |||
export interface ResolveDNSOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDnsLinkProgressEvents> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolveDns
(which takes ResolveDNSOptions
) ultimately calls #resolve
, which is also called by resolve
(which takes ResolveOptions
).
This could be cleaned up but I'll leave this for @achingbrain to decide if he wants this cleaned up further. Ultimately, #resolve
should take a superset of resolve
and resolveDns
option types.
options.onProgress?.(new CustomProgressEvent<string>('dnslink:query', { detail: domain })) | ||
const dnslinkRecord = await resolveFn(resolver, domain) | ||
|
||
options.onProgress?.(new CustomProgressEvent<DNSResponse>('dnslink:answer', { detail: dnslinkRecord })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dnslink
onProgress events were previously not being used in the nodejs resolve function. see
helia-ipns/packages/ipns/src/dns-resolvers/default.ts
Lines 13 to 39 in b87b9e3
async function resolve (domain: string, options: AbortOptions = {}): Promise<string> { | |
const resolver = new Resolver() | |
const listener = (): void => { | |
resolver.cancel() | |
} | |
options.signal?.addEventListener('abort', listener) | |
try { | |
const DNSLINK_REGEX = /^dnslink=.+$/ | |
const records = await resolver.resolveTxt(domain) | |
const dnslinkRecords = records.reduce((rs, r) => rs.concat(r), []) | |
.filter(record => DNSLINK_REGEX.test(record)) | |
const dnslinkRecord = dnslinkRecords[0] | |
// we now have dns text entries as an array of strings | |
// only records passing the DNSLINK_REGEX text are included | |
if (dnslinkRecord == null) { | |
throw new CodeError(`No dnslink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND') | |
} | |
return dnslinkRecord | |
} finally { | |
options.signal?.removeEventListener('abort', listener) | |
} | |
} |
return { | ||
name: a.name, | ||
type: txtType, | ||
TTL: a.ttl ?? ttl, | ||
// @ts-expect-error we have already checked that a.data is not empty | ||
data: uint8ArrayToString(a.data[0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TS error was not that data was empty, but that a.data[0] === string | number | Buffer
We may need more smarts around this check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
around the *above check of !Buffer.isBuffer(a.data[0])
TC: response.flag_tc ?? false, | ||
RD: response.flag_rd ?? false, | ||
RA: response.flag_ra ?? false, | ||
AD: response.flag_ad ?? false, | ||
CD: response.flag_cd ?? false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See module augmentation type in dns-packet.d.ts
Builds on top of #55 with the following
changes:
dns-over-http-resolver
insteadresolve
logic fromdns-resolvers/default.ts
into/utils/dns.ts
asresolveFn
resolve
logic split todns-resolvers/resolver.ts
anddns-resolvers/resolver.browser.ts
defaultResolver
is now isomorphic so functionality (use ofrecursiveResolveDnslink
) is the same in browser and nodejs.Open Questions
MAX_RECURSIVE_DEPTH
would allow users to specify lower recursive values and make their own decision whether to continue to resolve dnslink entries.