From 569923c86d576f191a0ebb6f1fd1615a5552fa2b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 12 Oct 2020 14:19:57 +0200 Subject: [PATCH 1/6] feat: resolve multiaddrs --- package.json | 8 ++- src/index.d.ts | 7 +++ src/index.js | 48 +++++++++++++++++ src/resolvers/dns.browser.js | 3 ++ src/resolvers/dns.js | 3 ++ src/resolvers/index.js | 31 +++++++++++ test/resolvers.spec.js | 101 +++++++++++++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/resolvers/dns.browser.js create mode 100644 src/resolvers/dns.js create mode 100644 src/resolvers/index.js create mode 100644 test/resolvers.spec.js diff --git a/package.json b/package.json index 6b139ffb..2965a71e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "docs": "aegir docs", "size": "aegir build -b" }, + "browser": { + "./src/resolvers/dns.js": "./src/resolvers/dns.browser.js" + }, "files": [ "src", "dist" @@ -34,6 +37,8 @@ "dependencies": { "cids": "^1.0.0", "class-is": "^1.1.0", + "dns-over-http-resolver": "vasco-santos/dns-over-http-resolver#feat/initial-implementation", + "err-code": "^2.0.3", "is-ip": "^3.1.0", "multibase": "^3.0.0", "uint8arrays": "^1.1.0", @@ -45,6 +50,7 @@ "@types/mocha": "^8.0.1", "@types/node": "^14.0.11", "aegir": "^26.0.0", + "sinon": "^9.2.0", "typescript": "^3.9.5" }, "contributors": [ @@ -79,4 +85,4 @@ "Linus Unnebäck ", "Alex Potsides " ] -} \ No newline at end of file +} diff --git a/src/index.d.ts b/src/index.d.ts index 430a538f..19330778 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -52,6 +52,8 @@ declare class Multiaddr { bytes: Uint8Array; + resolvers: Map Promise>> + /** * Returns Multiaddr as a String */ @@ -152,6 +154,11 @@ declare class Multiaddr { * `{IPv4, IPv6}/{TCP, UDP}` */ isThinWaistAddress(addr?: Multiaddr): boolean; + + /** + * Resolve multiaddr if containing resolvable hostname. + */ + resolve(options?: object): Promise> } declare namespace Multiaddr { diff --git a/src/index.js b/src/index.js index 39391c70..f2c0a93c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ const protocols = require('./protocols-table') const varint = require('varint') const CID = require('cids') const withIs = require('class-is') +const errCode = require('err-code') const inspect = Symbol.for('nodejs.util.inspect.custom') const uint8ArrayToString = require('uint8arrays/to-string') const uint8ArrayEquals = require('uint8arrays/equals') @@ -45,6 +46,8 @@ const Multiaddr = withIs.proto(function (addr) { } else { throw new Error('addr must be a string, Buffer, or another Multiaddr') } + + this.resolvers = new Map() }, { className: 'Multiaddr', symbolName: '@multiformats/js-multiaddr/multiaddr' }) /** @@ -366,6 +369,51 @@ Multiaddr.prototype.equals = function equals (addr) { return uint8ArrayEquals(this.bytes, addr.bytes) } +/** + * Resolve multiaddr if containing resolvable hostname. + * + * @param {object} options + * @param {bool} [options.recursive = true] recursive resolve until no resolvable multiaddr reached + * @returns {Promise>} + * @example + * const mh1 = Multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') + * mh1.resolvers.set('dnsaddr', resolverFunction) + * const resolvedMultiaddrs = await mh1.resolve() + * // [ + * // , + * // , + * // + * // ] + */ +Multiaddr.prototype.resolve = async function resolve ({ recursive = true } = {}) { + const resolvableProto = this.protos().find((p) => p.resolvable) + + // Multiaddr is not resolvable? + if (!resolvableProto) { + return this + } + + const resolver = this.resolvers.get(resolvableProto.name) + if (!resolver) { + throw errCode(new Error(`no available resolver for ${resolvableProto.name}`), 'ERR_NO_AVAILABLE_RESOLVER') + } + + const addresses = await resolver(this) + const newMultiaddrs = addresses.map(a => Multiaddr(a)) + + if (!recursive) { + return newMultiaddrs + } + + // Inject Resolvers and resolve + const recursiveMultiaddrs = await Promise.all(newMultiaddrs.map((nm) => { + nm.resolvers = this.resolvers + return nm.resolve() + })) + + return recursiveMultiaddrs.flat() +} + /** * Gets a Multiaddrs node-friendly address object. Note that protocol information * is left out: in Node (and most network systems) the protocol is unknowable diff --git a/src/resolvers/dns.browser.js b/src/resolvers/dns.browser.js new file mode 100644 index 00000000..9b8da5e9 --- /dev/null +++ b/src/resolvers/dns.browser.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports = require('dns-over-http-resolver') diff --git a/src/resolvers/dns.js b/src/resolvers/dns.js new file mode 100644 index 00000000..47e76023 --- /dev/null +++ b/src/resolvers/dns.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports = require('dns').promises diff --git a/src/resolvers/index.js b/src/resolvers/index.js new file mode 100644 index 00000000..008f8923 --- /dev/null +++ b/src/resolvers/index.js @@ -0,0 +1,31 @@ +'use strict' + +const Multiaddr = require('..') // eslint-disable-line no-unused-vars + +/** + * Resolver for dnsaddr addresses. + * + * @param {Multiaddr} addr + * @returns {Promise>} + */ +async function dnsaddrResolver (addr) { + const { Resolver } = require('./dns') + const resolver = new Resolver() + + const peerId = addr.getPeerId() + const hostname = addr.toString().split('dnsaddr')[1].split('/')[1] + + const records = await resolver.resolveTxt(`_dnsaddr.${hostname}`) + // @ts-ignore + let addresses = records.flat().map((a) => a.split('=')[1]) + + if (peerId) { + addresses = addresses.filter((entry) => entry.includes(peerId)) + } + + return addresses +} + +module.exports = { + dnsaddrResolver +} diff --git a/test/resolvers.spec.js b/test/resolvers.spec.js new file mode 100644 index 00000000..1967b2a0 --- /dev/null +++ b/test/resolvers.spec.js @@ -0,0 +1,101 @@ +/* eslint-env mocha */ +'use strict' + +const { expect } = require('aegir/utils/chai') +const sinon = require('sinon') + +const multiaddr = require('../src') +const resolvers = require('../src/resolvers') +const { Resolver } = require('../src/resolvers/dns') + +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'] +] + +describe('multiaddr resolve', () => { + describe('dnsaddr', () => { + afterEach(() => { + sinon.restore() + }) + + it('should throw if no resolver is available', async () => { + const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io') + + // Resolve + await expect(ma.resolve()).to.eventually.be.rejected() + .and.to.have.property('code', 'ERR_NO_AVAILABLE_RESOLVER') + }) + + 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)) + + // Set resolvers + ma.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) + + // Resolve + const resolvedMas = await ma.resolve({ recursive: false }) + + 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) + }) + }) + + it('can resolve dnsaddr with peerId recursively', 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)) + + // Set resolvers + ma.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) + + // Resolve + const resolvedMas = await ma.resolve() + + 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) + }) + }) + + it('can resolve dnsaddr with peerId not recursively', 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)) + + // Set resolvers + ma.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) + + // Resolve + const resolvedMas = await ma.resolve({ recursive: false }) + + expect(resolvedMas).to.have.length(1) + expect(resolvedMas[0].equals(multiaddr('/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'))).to.eql(true) + }) + }) +}) From f064934245d2f8343c715ff36a1043eb6ccd117c Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 12 Oct 2020 16:38:33 +0200 Subject: [PATCH 2/6] chore: use node lts and stable for ci --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 77e59738..81758d88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,8 @@ stages: - cov node_js: - - '10' + - 'lts/*' + - 'stable' os: - linux From 5957d959187dbac8b1a37fda4f6852a9f6e056d5 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 12 Oct 2020 17:15:12 +0200 Subject: [PATCH 3/6] chore: revert recursive option for now --- src/index.d.ts | 2 +- src/index.js | 19 +++---------------- test/resolvers.spec.js | 30 ++++++++++++++++++------------ 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 19330778..1a0d26d7 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -158,7 +158,7 @@ declare class Multiaddr { /** * Resolve multiaddr if containing resolvable hostname. */ - resolve(options?: object): Promise> + resolve(): Promise> } declare namespace Multiaddr { diff --git a/src/index.js b/src/index.js index f2c0a93c..cdfe5f70 100644 --- a/src/index.js +++ b/src/index.js @@ -373,7 +373,6 @@ Multiaddr.prototype.equals = function equals (addr) { * Resolve multiaddr if containing resolvable hostname. * * @param {object} options - * @param {bool} [options.recursive = true] recursive resolve until no resolvable multiaddr reached * @returns {Promise>} * @example * const mh1 = Multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') @@ -385,12 +384,12 @@ Multiaddr.prototype.equals = function equals (addr) { * // * // ] */ -Multiaddr.prototype.resolve = async function resolve ({ recursive = true } = {}) { +Multiaddr.prototype.resolve = async function resolve () { const resolvableProto = this.protos().find((p) => p.resolvable) // Multiaddr is not resolvable? if (!resolvableProto) { - return this + return [this] } const resolver = this.resolvers.get(resolvableProto.name) @@ -399,19 +398,7 @@ Multiaddr.prototype.resolve = async function resolve ({ recursive = true } = {}) } const addresses = await resolver(this) - const newMultiaddrs = addresses.map(a => Multiaddr(a)) - - if (!recursive) { - return newMultiaddrs - } - - // Inject Resolvers and resolve - const recursiveMultiaddrs = await Promise.all(newMultiaddrs.map((nm) => { - nm.resolvers = this.resolvers - return nm.resolve() - })) - - return recursiveMultiaddrs.flat() + return addresses.map(a => Multiaddr(a)) } /** diff --git a/test/resolvers.spec.js b/test/resolvers.spec.js index 1967b2a0..740604f4 100644 --- a/test/resolvers.spec.js +++ b/test/resolvers.spec.js @@ -50,7 +50,7 @@ describe('multiaddr resolve', () => { ma.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) // Resolve - const resolvedMas = await ma.resolve({ recursive: false }) + const resolvedMas = await ma.resolve() expect(resolvedMas).to.have.length(dnsaddrStub1.length) resolvedMas.forEach((ma, index) => { @@ -60,7 +60,7 @@ describe('multiaddr resolve', () => { }) }) - it('can resolve dnsaddr with peerId recursively', async () => { + it('can resolve dnsaddr with peerId', async () => { const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') const stub = sinon.stub(Resolver.prototype, 'resolveTxt') @@ -73,15 +73,11 @@ describe('multiaddr resolve', () => { // Resolve const resolvedMas = await ma.resolve() - 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) - }) + expect(resolvedMas).to.have.length(1) + expect(resolvedMas[0].equals(multiaddr('/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb'))).to.eql(true) }) - it('can resolve dnsaddr with peerId not recursively', async () => { + it('can resolve dnsaddr with peerId two levels', async () => { const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb') const stub = sinon.stub(Resolver.prototype, 'resolveTxt') @@ -92,10 +88,20 @@ describe('multiaddr resolve', () => { ma.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) // Resolve - const resolvedMas = await ma.resolve({ recursive: false }) + const resolvedInitialMas = await ma.resolve() + const resolvedSecondMas = await Promise.all(resolvedInitialMas.map(nm => { + nm.resolvers.set('dnsaddr', resolvers.dnsaddrResolver) + return nm.resolve() + })) + // @ts-ignore + const resolvedMas = resolvedSecondMas.flat() - 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.have.length(dnsaddrStub2.length) + resolvedMas.forEach((ma, index) => { + const stubAddr = dnsaddrStub2[index][0].split('=')[1] + + expect(ma.equals(multiaddr(stubAddr))).to.equal(true) + }) }) }) }) From e309d5305cd54ab54fc371857581a09e747dcfb0 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 13 Oct 2020 14:38:41 +0200 Subject: [PATCH 4/6] chore: use released package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2965a71e..c18c6cdd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "dependencies": { "cids": "^1.0.0", "class-is": "^1.1.0", - "dns-over-http-resolver": "vasco-santos/dns-over-http-resolver#feat/initial-implementation", + "dns-over-http-resolver": "^1.0.0", "err-code": "^2.0.3", "is-ip": "^3.1.0", "multibase": "^3.0.0", From eeb5822dd029d5a41175f2a643ac6db127cc0c7f Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 13 Oct 2020 15:15:52 +0200 Subject: [PATCH 5/6] chore: address review --- README.md | 20 ++++++++++++++++++++ package.json | 3 --- src/index.d.ts | 4 ++-- src/index.js | 9 +++++---- src/resolvers/dns.browser.js | 3 --- src/resolvers/dns.js | 10 +++++++++- src/resolvers/index.js | 5 ++++- test/resolvers.spec.js | 29 +++++++++++++++++------------ 8 files changed, 57 insertions(+), 26 deletions(-) delete mode 100644 src/resolvers/dns.browser.js diff --git a/README.md b/README.md index aae83fdf..66257b03 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ js-multiaddr - [Browser: `