From e81b89f71e5602d6298f0ddd247639fde98dbfb2 Mon Sep 17 00:00:00 2001 From: Nitin Patel Date: Thu, 14 Mar 2019 18:21:34 +0530 Subject: [PATCH] feat: enable recursive lookups on ipfs.dns --- src/cli/commands/dns.js | 10 ++++-- src/core/runtime/dns-nodejs.js | 32 +++++++++++++++--- src/http/api/resources/dns.js | 6 ++-- test/cli/dns.js | 26 ++++++++++++++ test/core/dns.spec.js | 62 ++++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 test/core/dns.spec.js diff --git a/src/cli/commands/dns.js b/src/cli/commands/dns.js index 020259ef3f..cf4f023f23 100644 --- a/src/cli/commands/dns.js +++ b/src/cli/commands/dns.js @@ -7,15 +7,21 @@ module.exports = { describe: 'Resolve DNS links', builder: { + recursive: { + type: 'boolean', + default: false, + alias: 'r', + desc: 'Resolve until the result is not a DNS link' + }, format: { type: 'string' } }, - handler ({ getIpfs, domain, resolve }) { + handler ({ getIpfs, domain, resolve, ...opts }) { resolve((async () => { const ipfs = await getIpfs() - const path = await ipfs.dns(domain) + const path = await ipfs.dns(domain, opts) print(path) })()) } diff --git a/src/core/runtime/dns-nodejs.js b/src/core/runtime/dns-nodejs.js index a3d583ef92..a3dd5a0e30 100644 --- a/src/core/runtime/dns-nodejs.js +++ b/src/core/runtime/dns-nodejs.js @@ -2,13 +2,30 @@ const dns = require('dns') const _ = require('lodash') +const isIPFS = require('is-ipfs') const errcode = require('err-code') +const maxRecursiveDepth = 32 + module.exports = (domain, opts, callback) => { - resolveDnslink(domain) + const recursive = opts.recursive && opts.recursive.toString() === 'true' + let depth + if (recursive) { + depth = maxRecursiveDepth + } + + return recursiveResolveDnslink(domain, depth, callback) +} + +function recursiveResolveDnslink (domain, depth, callback) { + if (depth === 0) { + return callback(errcode(`recursion limit exceeded`, 'ERR_DNSLINK_RECURSION_LIMIT')) + } + + return resolveDnslink(domain) .catch(err => { - // If the code is not ENOTFOUND or ERR_DNSLINK_NOT_FOUND then throw the error - if (err.code !== 'ENOTFOUND' && err.code !== 'ERR_DNSLINK_NOT_FOUND') throw err + // If the code is not ENOTFOUND or ERR_DNSLINK_NOT_FOUND or ENODATA then throw the error + if (err.code !== 'ENOTFOUND' && err.code !== 'ERR_DNSLINK_NOT_FOUND' && err.code !== 'ENODATA') throw err if (domain.startsWith('_dnslink.')) { // The supplied domain contains a _dnslink component @@ -22,7 +39,14 @@ module.exports = (domain, opts, callback) => { return resolveDnslink(_dnslinkDomain) }) .then(dnslinkRecord => { - callback(null, dnslinkRecord.replace('dnslink=', '')) + const result = dnslinkRecord.replace('dnslink=', '') + const domainOrCID = result.split('/')[2] + const isIPFSCID = isIPFS.cid(domainOrCID) + + if (isIPFSCID || !depth) { + return callback(null, result) + } + return recursiveResolveDnslink(domainOrCID, depth - 1, callback) }) .catch(callback) } diff --git a/src/http/api/resources/dns.js b/src/http/api/resources/dns.js index db20a9f170..272334b332 100644 --- a/src/http/api/resources/dns.js +++ b/src/http/api/resources/dns.js @@ -3,11 +3,13 @@ const Boom = require('boom') module.exports = async (request, h) => { - if (!request.query.arg) { + const { arg: domain, ...opts } = request.query + + if (!domain) { throw Boom.badRequest("Argument 'domain' is required") } - const path = await request.server.app.ipfs.dns(request.query.arg) + const path = await request.server.app.ipfs.dns(domain, opts) return h.response({ Path: path }) diff --git a/test/cli/dns.js b/test/cli/dns.js index 55aea6f5b6..0bd74e7792 100644 --- a/test/cli/dns.js +++ b/test/cli/dns.js @@ -3,6 +3,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') +const isIPFS = require('is-ipfs') describe('dns', () => runOnAndOff((thing) => { let ipfs @@ -27,4 +28,29 @@ describe('dns', () => runOnAndOff((thing) => { expect(res.substr(0, 6)).to.eql('/ipns/') }) }) + + it('recursively resolve ipfs.io', function () { + this.timeout(60 * 1000) + + return ipfs('dns --recursive ipfs.io').then((res) => { + const resultingDomainOrCid = res.split('/')[2].trim() + expect(isIPFS.cid(resultingDomainOrCid)).to.eql(true) + }) + }) + + it('resolve subdomain docs.ipfs.io dns', function () { + this.timeout(60 * 1000) + + return ipfs('dns docs.ipfs.io').then(res => { + expect(res.substr(0, 6)).to.eql('/ipfs/') + }) + }) + + it('resolve subdomain _dnslink.docs.ipfs.io dns', function () { + this.timeout(60 * 1000) + + return ipfs('dns _dnslink.docs.ipfs.io').then(res => { + expect(res.substr(0, 6)).to.eql('/ipfs/') + }) + }) })) diff --git a/test/core/dns.spec.js b/test/core/dns.spec.js new file mode 100644 index 0000000000..cecd56b2fe --- /dev/null +++ b/test/core/dns.spec.js @@ -0,0 +1,62 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const IPFSFactory = require('ipfsd-ctl') +const IPFS = require('../../src/core') + +describe('.dns', () => { + let ipfsd, ipfs + + before(function (done) { + this.timeout(20 * 1000) + + const factory = IPFSFactory.create({ type: 'proc' }) + + factory.spawn({ + exec: IPFS, + initOptions: { bits: 512 }, + config: { Bootstrap: [] } + }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + ipfs = _ipfsd.api + done() + }) + }) + + after((done) => { + if (ipfsd) { + ipfsd.stop(done) + } else { + done() + } + }) + + // skipping because there is an error in https://ipfs.io/api/v0/dns?arg=ipfs.io + // unskip once this is resolved: https://github.com/ipfs/go-ipfs/issues/6086 + it.skip('should resolve ipfs.io', () => { + return ipfs.dns('ipfs.io').then(res => { + // matches pattern /ipns/ + expect(res).to.match(/\/ipns\/.+$/) + }) + }) + + it('should recursively resolve ipfs.io', () => { + return ipfs.dns('ipfs.io', { recursive: true }).then(res => { + // matches pattern /ipfs/ + expect(res).to.match(/\/ipfs\/.+$/) + }) + }) + + it('should resolve subdomain docs.ipfs.io', () => { + return ipfs.dns('docs.ipfs.io').then(res => { + // matches pattern /ipfs/ + expect(res).to.match(/\/ipfs\/.+$/) + }) + }) +})