Skip to content

Commit

Permalink
feat: resolve multiaddrs
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Oct 12, 2020
1 parent 64e7745 commit 653f468
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 1 deletion.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"docs": "aegir docs",
"size": "aegir build -b"
},
"browser": {
"./src/resolvers/dns.js": "./src/resolvers/dns.browser.js"
},
"files": [
"src",
"dist"
Expand All @@ -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",
Expand All @@ -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": [
Expand Down Expand Up @@ -79,4 +85,4 @@
"Linus Unnebäck <linus@folkdatorn.se>",
"Alex Potsides <alex@achingbrain.net>"
]
}
}
7 changes: 7 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ declare class Multiaddr {

bytes: Uint8Array;

resolvers: Map<string, (addr: Multiaddr) => Promise<Array<string>>>

/**
* Returns Multiaddr as a String
*/
Expand Down Expand Up @@ -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<Array<Multiaddr>>
}

declare namespace Multiaddr {
Expand Down
48 changes: 48 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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' })

/**
Expand Down Expand Up @@ -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<Array<Multiaddr>>}
* @example
* const mh1 = Multiaddr('/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb')
* mh1.resolvers.set('dnsaddr', resolverFunction)
* const resolvedMultiaddrs = await mh1.resolve()
* // [
* // <Multiaddr 04934b5353060fa1a503221220c10f9319dac35c270a6b74cd644cb3acfc1f6efc8c821f8eb282599fd1814f64 - /ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb>,
* // <Multiaddr 04934b53530601bbde03a503221220c10f9319dac35c270a6b74cd644cb3acfc1f6efc8c821f8eb282599fd1814f64 - /ip4/147.75.83.83/tcp/443/wss/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb>,
* // <Multiaddr 04934b535391020fa1cc03a503221220c10f9319dac35c270a6b74cd644cb3acfc1f6efc8c821f8eb282599fd1814f64 - /ip4/147.75.83.83/udp/4001/quic/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb>
* // ]
*/
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
Expand Down
3 changes: 3 additions & 0 deletions src/resolvers/dns.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict'

module.exports = require('dns-over-http-resolver')
3 changes: 3 additions & 0 deletions src/resolvers/dns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict'

module.exports = require('dns').promises
28 changes: 28 additions & 0 deletions src/resolvers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

/**
* Resolver for dnsaddr addresses.
*
* @param {Multiaddr} addr
* @returns {Promise<Array<string>>}
*/
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}`)
let addresses = records.flat().map((a) => a.split('=')[1])

if (peerId) {
addresses = addresses.filter((entry) => entry.includes(peerId))
}

return addresses
}

module.exports = {
dnsaddrResolver
}
101 changes: 101 additions & 0 deletions test/resolvers.spec.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})

0 comments on commit 653f468

Please sign in to comment.