From 569923c86d576f191a0ebb6f1fd1615a5552fa2b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 12 Oct 2020 14:19:57 +0200 Subject: [PATCH] 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) + }) + }) +})