From 5be4646ce9b6d6e79fa06754a05434c444e9499b Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 8 Mar 2024 20:01:47 +0100 Subject: [PATCH] feat: accept DNS resolver when resolving DNSADDR addresses Accept a `DNS` instance from `@multiformats/dns` when resolving DNSADDR addresses. Gives the user flexibility to control which DNS servers are used to resolve TXT records. --- README.md | 51 +++++++--- package.json | 6 +- src/index.ts | 76 +++++++++++---- src/multiaddr.ts | 8 +- src/resolvers/dns.browser.ts | 3 - src/resolvers/dns.ts | 3 - src/resolvers/dnsaddr.ts | 74 +++++++++++++++ src/resolvers/index.ts | 60 +----------- test/resolvers.spec.ts | 176 ++++++++++++++++++----------------- 9 files changed, 275 insertions(+), 182 deletions(-) delete mode 100644 src/resolvers/dns.browser.ts delete mode 100644 src/resolvers/dns.ts create mode 100644 src/resolvers/dnsaddr.ts diff --git a/README.md b/README.md index 129f49c2..58f73829 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ A standard way to represent addresses that ## Example -```js +```TypeScript import { multiaddr } from '@multiformats/multiaddr' const addr = multiaddr("/ip4/127.0.0.1/udp/1234") // Multiaddr(/ip4/127.0.0.1/udp/1234) @@ -65,25 +65,52 @@ addr.encapsulate('/sctp/5678') // Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678) ``` -## Resolvers +## Resolving DNSADDR addresses -`multiaddr` allows multiaddrs to be resolved when appropriate resolvers are provided. This module already has resolvers available, but you can also create your own. Resolvers should always be set in the same module that is calling `multiaddr.resolve()` to avoid conflicts if multiple versions of `multiaddr` are in your dependency tree. +[DNSADDR](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) is a spec that allows storing a TXT DNS record that contains a Multiaddr. -To provide multiaddr resolvers you can do: +To resolve DNSADDR addresses, call the `.resolve()` function the multiaddr, optionally passing a `DNS` resolver. -```js -import { resolvers } from '@multiformats/multiaddr' +DNSADDR addresses can resolve to multiple multiaddrs, since there is no limit to the number of TXT records that can be stored. -resolvers.set('dnsaddr', resolvers.dnsaddrResolver) +## Example - Resolving DNSADDR Multiaddrs + +```TypeScript +import { multiaddr, resolvers } from '@multiformats/multiaddr' +import { dnsaddr } from '@multiformats/multiaddr/resolvers' + +resolvers.set('dnsaddr', dnsaddr) + +const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') + +// resolve with a 5s timeout +const resolved = await ma.resolve({ + signal: AbortSignal.timeout(5000) +}) + +console.info(await ma.resolve(resolved) +// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...] ``` -The available resolvers are: +## Example - Using a custom DNS resolver to resolve DNSADDR Multiaddrs -| Name | type | Description | -| ----------------- | --------- | ----------------------------------- | -| `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records | +```TypeScript +import { multiaddr } from '@multiformats/multiaddr' +import { dns } from '@multiformats/dns' +import { dnsJsonOverHttps } from '@multiformats/dns/resolvers' + +const resolver = dns({ + '.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query') +}) -A resolver receives a `Multiaddr` as a parameter and returns a `Promise>`. +const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') +const resolved = await ma.resolve({ + dns: resolver +}) + +console.info(resolved) +// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...] +``` # Install diff --git a/package.json b/package.json index 9c7e83c4..ac3c3d3f 100644 --- a/package.json +++ b/package.json @@ -169,15 +169,17 @@ "@chainsafe/is-ip": "^2.0.1", "@chainsafe/netmask": "^2.0.0", "@libp2p/interface": "^1.0.0", - "dns-over-http-resolver": "^3.0.2", + "@multiformats/dns": "^1.0.1", "multiformats": "^13.0.0", + "race-signal": "^1.0.2", "uint8-varint": "^2.0.1", "uint8arrays": "^5.0.0" }, "devDependencies": { "@types/sinon": "^17.0.2", "aegir": "^42.2.2", - "sinon": "^17.0.0" + "sinon": "^17.0.0", + "sinon-ts": "^2.0.0" }, "browser": { "./dist/src/resolvers/dns.js": "./dist/src/resolvers/dns.browser.js" diff --git a/src/index.ts b/src/index.ts index b1381a08..08f81f3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ * * @example * - * ```js + * ```TypeScript * import { multiaddr } from '@multiformats/multiaddr' * const addr = multiaddr("/ip4/127.0.0.1/udp/1234") * // Multiaddr(/ip4/127.0.0.1/udp/1234) @@ -43,29 +43,58 @@ * // Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678) * ``` * - * ## Resolvers + * ## Resolving DNSADDR addresses * - * `multiaddr` allows multiaddrs to be resolved when appropriate resolvers are provided. This module already has resolvers available, but you can also create your own. Resolvers should always be set in the same module that is calling `multiaddr.resolve()` to avoid conflicts if multiple versions of `multiaddr` are in your dependency tree. + * [DNSADDR](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) is a spec that allows storing a TXT DNS record that contains a Multiaddr. * - * To provide multiaddr resolvers you can do: + * To resolve DNSADDR addresses, call the `.resolve()` function the multiaddr, optionally passing a `DNS` resolver. * - * ```js - * import { resolvers } from '@multiformats/multiaddr' + * DNSADDR addresses can resolve to multiple multiaddrs, since there is no limit to the number of TXT records that can be stored. + * + * @example Resolving DNSADDR Multiaddrs + * + * ```TypeScript + * import { multiaddr, resolvers } from '@multiformats/multiaddr' + * import { dnsaddr } from '@multiformats/multiaddr/resolvers' + * + * resolvers.set('dnsaddr', dnsaddr) + * + * const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') * - * resolvers.set('dnsaddr', resolvers.dnsaddrResolver) + * // resolve with a 5s timeout + * const resolved = await ma.resolve({ + * signal: AbortSignal.timeout(5000) + * }) + * + * console.info(await ma.resolve(resolved) + * // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...] * ``` * - * The available resolvers are: + * @example Using a custom DNS resolver to resolve DNSADDR Multiaddrs + * + * ```TypeScript + * import { multiaddr } from '@multiformats/multiaddr' + * import { dns } from '@multiformats/dns' + * import { dnsJsonOverHttps } from '@multiformats/dns/resolvers' * - * | Name | type | Description | - * | ----------------- | --------- | ----------------------------------- | - * | `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records | + * const resolver = dns({ + * '.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query') + * }) * - * A resolver receives a `Multiaddr` as a parameter and returns a `Promise>`. + * const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') + * const resolved = await ma.resolve({ + * dns: resolver + * }) + * + * console.info(resolved) + * // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...] + * ``` */ import { Multiaddr as MultiaddrClass, symbol } from './multiaddr.js' import { getProtocol } from './protocols-table.js' +import type { Resolver } from './resolvers/index.js' +import type { DNS } from '@multiformats/dns' /** * Protocols are present in the protocol table @@ -102,12 +131,6 @@ export interface NodeAddress { */ export type MultiaddrInput = string | Multiaddr | Uint8Array | null -/** - * A Resolver is a function that takes a {@link Multiaddr} and resolves it into one - * or more string representations of that {@link Multiaddr}. - */ -export interface Resolver { (addr: Multiaddr, options?: AbortOptions): Promise } - /** * A code/value pair */ @@ -132,6 +155,21 @@ export const resolvers = new Map() export { MultiaddrFilter } from './filter/multiaddr-filter.js' +export interface ResolveOptions extends AbortOptions { + /** + * An optional DNS resolver + */ + dns?: DNS + + /** + * When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs, + * limit how many times we will recursively resolve them. + * + * @default 32 + */ + maxRecursiveDepth?: number +} + export interface Multiaddr { bytes: Uint8Array @@ -388,7 +426,7 @@ export interface Multiaddr { * // ] * ``` */ - resolve(options?: AbortOptions): Promise + resolve(options?: ResolveOptions): Promise /** * Gets a Multiaddrs node-friendly address object. Note that protocol information diff --git a/src/multiaddr.ts b/src/multiaddr.ts index d1447a5c..a707a537 100644 --- a/src/multiaddr.ts +++ b/src/multiaddr.ts @@ -19,7 +19,8 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { bytesToMultiaddrParts, stringToMultiaddrParts, type MultiaddrParts, tuplesToBytes } from './codec.js' import { getProtocol, names } from './protocols-table.js' -import { isMultiaddr, type AbortOptions, type MultiaddrInput, type Multiaddr as MultiaddrInterface, type MultiaddrObject, type Protocol, type StringTuple, type Tuple, resolvers, type NodeAddress } from './index.js' +import { isMultiaddr, resolvers } from './index.js' +import type { MultiaddrInput, Multiaddr as MultiaddrInterface, MultiaddrObject, Protocol, StringTuple, Tuple, NodeAddress, ResolveOptions } from './index.js' const inspect = Symbol.for('nodejs.util.inspect.custom') export const symbol = Symbol.for('@multiformats/js-multiaddr/multiaddr') @@ -221,7 +222,7 @@ export class Multiaddr implements MultiaddrInterface { return uint8ArrayEquals(this.bytes, addr.bytes) } - async resolve (options?: AbortOptions): Promise { + async resolve (options?: ResolveOptions): Promise { const resolvableProto = this.protos().find((p) => p.resolvable) // Multiaddr is not resolvable? @@ -234,8 +235,7 @@ export class Multiaddr implements MultiaddrInterface { throw new CodeError(`no available resolver for ${resolvableProto.name}`, 'ERR_NO_AVAILABLE_RESOLVER') } - const addresses = await resolver(this, options) - return addresses.map((a) => new Multiaddr(a)) + return resolver(this, options) } nodeAddress (): NodeAddress { diff --git a/src/resolvers/dns.browser.ts b/src/resolvers/dns.browser.ts deleted file mode 100644 index fbb6f1eb..00000000 --- a/src/resolvers/dns.browser.ts +++ /dev/null @@ -1,3 +0,0 @@ -import dns from 'dns-over-http-resolver' - -export default dns diff --git a/src/resolvers/dns.ts b/src/resolvers/dns.ts deleted file mode 100644 index 681d36aa..00000000 --- a/src/resolvers/dns.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Resolver } from 'dns/promises' - -export default Resolver diff --git a/src/resolvers/dnsaddr.ts b/src/resolvers/dnsaddr.ts new file mode 100644 index 00000000..4222ac6a --- /dev/null +++ b/src/resolvers/dnsaddr.ts @@ -0,0 +1,74 @@ +import { CodeError } from '@libp2p/interface' +import { dns, RecordType } from '@multiformats/dns' +import { raceSignal } from 'race-signal' +import { multiaddr } from '../index.js' +import { getProtocol } from '../protocols-table.js' +import type { Resolver } from './index.js' +import type { AbortOptions, Multiaddr } from '../index.js' +import type { DNS } from '@multiformats/dns' + +const MAX_RECURSIVE_DEPTH = 32 +const { code: dnsaddrCode } = getProtocol('dnsaddr') + +export interface DNSADDROptions extends AbortOptions { + /** + * An optional DNS resolver + */ + dns?: DNS + + /** + * When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs, + * limit how many times we will recursively resolve them. + * + * @default 32 + */ + maxRecursiveDepth?: number +} + +export const dnsaddr: Resolver = async function dnsaddr (ma: Multiaddr, options: DNSADDROptions = {}): Promise { + const recursionLimit = options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH + + if (recursionLimit === 0) { + throw new CodeError('Max recursive depth reached', 'ERR_MAX_RECURSIVE_DEPTH_REACHED') + } + + const [, hostname] = ma.stringTuples().find(([proto]) => proto === dnsaddrCode) ?? [] + + const resolver = options?.dns ?? dns() + const result = await raceSignal(resolver.query(`_dnsaddr.${hostname}`, { + signal: options?.signal, + types: [ + RecordType.TXT + ] + }), options.signal) + + const peerId = ma.getPeerId() + const output: Multiaddr[] = [] + + for (const answer of result.Answer) { + const addr = answer.data.split('=')[1] + + if (addr == null) { + continue + } + + if (peerId != null && !addr.includes(peerId)) { + continue + } + + const ma = multiaddr(addr) + + if (addr.startsWith('/dnsaddr')) { + const resolved = await ma.resolve({ + ...options, + maxRecursiveDepth: recursionLimit - 1 + }) + + output.push(...resolved) + } else { + output.push(ma) + } + } + + return output +} diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 648b7c91..ae5ed671 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -1,59 +1,7 @@ -/** - * @packageDocumentation - * - * Provides strategies for resolving multiaddrs. - */ - -import { getProtocol } from '../protocols-table.js' -import Resolver from './dns.js' import type { AbortOptions, Multiaddr } from '../index.js' -const { code: dnsaddrCode } = getProtocol('dnsaddr') - -/** - * Resolver for dnsaddr addresses. - * - * @example - * - * ```typescript - * import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers' - * import { multiaddr } from '@multiformats/multiaddr' - * - * const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') - * const addresses = await dnsaddrResolver(ma) - * - * console.info(addresses) - * //[ - * // '/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', - * // '/dnsaddr/ny5.bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', - * // '/dnsaddr/sg1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', - * // '/dnsaddr/sv15.bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN' - * //] - * ``` - */ -export async function dnsaddrResolver (addr: Multiaddr, options: AbortOptions = {}): Promise { - const resolver = new Resolver() - - if (options.signal != null) { - options.signal.addEventListener('abort', () => { - resolver.cancel() - }) - } - - const peerId = addr.getPeerId() - const [, hostname] = addr.stringTuples().find(([proto]) => proto === dnsaddrCode) ?? [] - - if (hostname == null) { - throw new Error('No hostname found in multiaddr') - } - - const records = await resolver.resolveTxt(`_dnsaddr.${hostname}`) - - let addresses = records.flat().map((a) => a.split('=')[1]).filter(Boolean) - - if (peerId != null) { - addresses = addresses.filter((entry) => entry.includes(peerId)) - } - - return addresses +export interface Resolver { + (ma: Multiaddr, options?: ResolveOptions): Promise } + +export { dnsaddr } from './dnsaddr.js' diff --git a/test/resolvers.spec.ts b/test/resolvers.spec.ts index 07bd7ba2..2625f45c 100644 --- a/test/resolvers.spec.ts +++ b/test/resolvers.spec.ts @@ -1,51 +1,79 @@ -/* eslint-env mocha */ +import { RecordType } from '@multiformats/dns' import { expect } from 'aegir/chai' import sinon from 'sinon' +import { stubInterface, type StubbedInstance } from 'sinon-ts' import { multiaddr, resolvers } from '../src/index.js' -import Resolver from '../src/resolvers/dns.js' -import * as resolversInternal from '../src/resolvers/index.js' - -const dnsaddrStub1 = [ - ['dnsaddr=/dnsaddr/ams-1.bootstrap.libp2p.io/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd'], - ['dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/dnsaddr/lon-1.bootstrap.libp2p.io/p2p/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3'], - ['dnsaddr=/dnsaddr/nrt-1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt'], - ['dnsaddr=/dnsaddr/nyc-1.bootstrap.libp2p.io/p2p/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm'], - ['dnsaddr=/dnsaddr/sfo-2.bootstrap.libp2p.io/p2p/QmSoLnSGccFuZQJzRadHn95W2CrSFmZuTdDWP8HXaHca9z'] -] - -const dnsaddrStub2 = [ - ['dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/ip4/147.75.83.83/tcp/443/wss/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/ip4/147.75.83.83/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/443/wss/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/ip6/2604:1380:2000:7a00::1/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'] -] - -const dnsaddrStub3 = [ - ['dnsaddr=/dnsaddr/sv15.bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN'], - ['dnsaddr=/dnsaddr/ny5.bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa'], - ['dnsaddr_record_value'], - ['dnsaddr=/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'], - ['dnsaddr=/dnsaddr/sg1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt'] -] +import { dnsaddr } from '../src/resolvers/index.js' +import type { DNS } from '@multiformats/dns' + +const stubs: Record = { + '_dnsaddr.bootstrap.libp2p.io': [ + 'dnsaddr=/dnsaddr/ams-1.bootstrap.libp2p.io/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb' + ], + '_dnsaddr.ams-1.bootstrap.libp2p.io': [ + 'dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/ip4/147.75.83.83/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/ip4/147.75.83.83/udp/4001/quic/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/443/wss/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/udp/4001/quic/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd' + ], + '_dnsaddr.ams-2.bootstrap.libp2p.io': [ + 'dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/ip4/147.75.83.83/tcp/443/wss/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/ip4/147.75.83.83/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/443/wss/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/ip6/2604:1380:2000:7a00::1/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb' + ], + '_dnsaddr.bad-addrs.libp2p.io': [ + 'dnsaddr=/dnsaddr/sv15.bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + 'dnsaddr=/dnsaddr/ny5.bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + 'dnsaddr_record_value', + 'dnsaddr=/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + 'dnsaddr=/dnsaddr/sg1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' + ], + '_dnsaddr.am6.bootstrap.libp2p.io': [ + 'dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb' + ] +} describe('multiaddr resolve', () => { + let dns: StubbedInstance + + beforeEach(() => { + dns = stubInterface({ + query: sinon.stub().callsFake((domain) => { + if (stubs[domain] != null) { + return { + Answer: stubs[domain].map(data => ({ + name: '_dnsaddr.bootstrap.libp2p.io', + type: RecordType.TXT, + ttl: 100, + data + })) + } + } + + throw new Error(`No result stubbed for ${domain}`) + }) + }) + + resolvers.set('dnsaddr', dnsaddr) + }) + it('should throw if no resolver is available', async () => { const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') + resolvers.clear() + // Resolve await expect(ma.resolve()).to.eventually.be.rejected() .and.to.have.property('code', 'ERR_NO_AVAILABLE_RESOLVER') }) describe('dnsaddr', () => { - before(() => { - // Set resolvers - resolvers.set('dnsaddr', resolversInternal.dnsaddrResolver) - }) - afterEach(() => { sinon.restore() }) @@ -53,71 +81,41 @@ describe('multiaddr resolve', () => { it('can resolve dnsaddr without no peerId', async () => { const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') - const stub = sinon.stub(Resolver.prototype, 'resolveTxt') - stub.onCall(0).returns(Promise.resolve(dnsaddrStub1)) - // Resolve - const resolvedMas = await ma.resolve() - - expect(resolvedMas).to.have.length(dnsaddrStub1.length) - resolvedMas.forEach((ma, index) => { - const stubAddr = dnsaddrStub1[index][0].split('=')[1] - - expect(ma.equals(multiaddr(stubAddr))).to.equal(true) + const resolvedMas = await ma.resolve({ + dns }) - }) - - it('can resolve dnsaddr with peerId', async () => { - const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') - - const stub = sinon.stub(Resolver.prototype, 'resolveTxt') - stub.onCall(0).returns(Promise.resolve(dnsaddrStub1)) - stub.onCall(1).returns(Promise.resolve(dnsaddrStub2)) - - // Resolve - const resolvedMas = await ma.resolve() - expect(resolvedMas).to.have.length(1) - expect(resolvedMas[0].equals(multiaddr('/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'))).to.eql(true) + expect(resolvedMas).to.deep.equal([ + ...stubs['_dnsaddr.ams-1.bootstrap.libp2p.io'].map(addr => multiaddr(addr.split('=').pop())), + ...stubs['_dnsaddr.ams-2.bootstrap.libp2p.io'].map(addr => multiaddr(addr.split('=').pop())) + ]) }) - it('can resolve dnsaddr with peerId two levels', async () => { + it('can resolve dnsaddr with peerId', async () => { const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') - const stub = sinon.stub(Resolver.prototype, 'resolveTxt') - stub.onCall(0).returns(Promise.resolve(dnsaddrStub1)) - stub.onCall(1).returns(Promise.resolve(dnsaddrStub2)) - // Resolve - const resolvedInitialMas = await ma.resolve() - const resolvedSecondMas = await Promise.all(resolvedInitialMas.map(async nm => { - // nm.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) - return nm.resolve() - })) - - const resolvedMas = resolvedSecondMas.flat() - - expect(resolvedMas).to.have.length(dnsaddrStub2.length) - - resolvedMas.forEach((ma, index) => { - const stubAddr = dnsaddrStub2[index][0].split('=')[1] - - expect(ma.equals(multiaddr(stubAddr))).to.equal(true) + const resolvedMas = await ma.resolve({ + dns }) + + expect(resolvedMas).to.deep.equal([ + ...stubs['_dnsaddr.ams-2.bootstrap.libp2p.io'].map(addr => multiaddr(addr.split('=').pop())) + ]) }) it('can resolve dnsaddr with bad record', async () => { - const ma = multiaddr('/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') - - const stub = sinon.stub(Resolver.prototype, 'resolveTxt') - stub.onCall(0).returns(Promise.resolve(dnsaddrStub3)) + const ma = multiaddr('/dnsaddr/bad-addrs.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') // Resolve - const resolvedMas = await ma.resolve() + const resolvedMas = await ma.resolve({ + dns + }) // Should only have one address with the same peer id and should ignore the bad record expect(resolvedMas).to.have.lengthOf(1) - expect(resolvedMas[0].toString()).to.equal(ma.toString()) + expect(resolvedMas[0].toString()).to.equal(stubs['_dnsaddr.am6.bootstrap.libp2p.io'][0].split('=').pop()) }) it('can cancel resolving', async () => { @@ -131,7 +129,19 @@ describe('multiaddr resolve', () => { controller.abort() - await expect(resolvePromise).to.eventually.be.rejected().with.property('code', 'ECANCELLED') + await expect(resolvePromise).to.eventually.be.rejected().with.property('code', 'ABORT_ERR') + }) + + it('should abort resolving deeply nested records', async () => { + const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') + + // Resolve + const resolvePromise = ma.resolve({ + dns, + maxRecursiveDepth: 1 + }) + + await expect(resolvePromise).to.eventually.be.rejected().with.property('code', 'ERR_MAX_RECURSIVE_DEPTH_REACHED') }) }) })