Skip to content

Commit

Permalink
feat: cache
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Nov 5, 2020
1 parent dc1e19d commit 2a2aa61
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ dist
# TernJS port file
.tern-port
package-lock.json

docs/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"homepage": "https://github.com/vasco-santos/dns-over-http-resolver#readme",
"dependencies": {
"debug": "^4.2.0",
"native-fetch": "^2.0.1"
"native-fetch": "^2.0.1",
"timed-cache": "^1.1.4"
},
"contributors": [
"Vasco Santos <vasco.santos@moxy.studio>"
Expand Down
65 changes: 52 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ const debug = require('debug')
const log = debug('dns-over-http-resolver')
log.error = debug('dns-over-http-resolver:error')

const { buildResource, fetch } = require('./utils')
const Cache = require('timed-cache')

const {
buildResource,
fetch,
getCacheKey
} = require('./utils')

/**
* DNS over HTTP resolver.
Expand All @@ -14,6 +20,7 @@ class Resolver {
* @class
*/
constructor () {
this._cache = new Cache()
this._servers = [
'https://cloudflare-dns.com/dns-query',
'https://dns.google/resolve'
Expand Down Expand Up @@ -66,22 +73,33 @@ class Resolver {
* @returns {Promise<Array<string>>}
*/
async resolve4 (hostname) {
const recordType = 'A'
const cached = this._cache.get(getCacheKey(hostname, recordType))
if (cached) {
return cached
}

for (const server of this._servers) {
try {
const response = await fetch(buildResource({
serverResolver: server,
hostname,
recordType: 'A'
recordType
}))

const d = await response.json()
return d.Answer.map(a => a.data)
const data = d.Answer.map(a => a.data)
const ttl = Math.min(d.Answer.map(a => a.TTL))

this._cache.put(getCacheKey(hostname, recordType), data, { ttl })

return data
} catch (err) {
log.error(`${server} could not resolve ${hostname} record A`)
log.error(`${server} could not resolve ${hostname} record ${recordType}`)
}
}

throw new Error(`Could not resolve ${hostname} record A`)
throw new Error(`Could not resolve ${hostname} record ${recordType}`)
}

/**
Expand All @@ -91,22 +109,33 @@ class Resolver {
* @returns {Promise<Array<string>>}
*/
async resolve6 (hostname) {
const recordType = 'AAAA'
const cached = this._cache.get(getCacheKey(hostname, recordType))
if (cached) {
return cached
}

for (const server of this._servers) {
try {
const response = await fetch(buildResource({
serverResolver: server,
hostname,
recordType: 'AAAA'
recordType
}))

const d = await response.json()
return d.Answer.map(a => a.data)
const data = d.Answer.map(a => a.data)
const ttl = Math.min(d.Answer.map(a => a.TTL))

this._cache.put(getCacheKey(hostname, recordType), data, { ttl })

return data
} catch (err) {
log.error(`${server} could not resolve ${hostname} record AAAA`)
log.error(`${server} could not resolve ${hostname} record ${recordType}`)
}
}

throw new Error(`Could not resolve ${hostname} record AAAA`)
throw new Error(`Could not resolve ${hostname} record ${recordType}`)
}

/**
Expand All @@ -116,23 +145,33 @@ class Resolver {
* @returns {Promise<Array<Array<string>>>}
*/
async resolveTxt (hostname) {
const recordType = 'TXT'
const cached = this._cache.get(getCacheKey(hostname, recordType))
if (cached) {
return cached
}

for (const server of this._servers) {
try {
const response = await fetch(buildResource({
serverResolver: server,
hostname,
recordType: 'TXT'
recordType
}))

const d = await response.json()
const data = d.Answer.map(a => [a.data.replace(/['"]+/g, '')])
const ttl = Math.min(d.Answer.map(a => a.TTL))

this._cache.put(getCacheKey(hostname, recordType), data, { ttl })

return d.Answer.map(a => [a.data.replace(/['"]+/g, '')])
return data
} catch (err) {
log.error(`${server} could not resolve ${hostname} record TXT`)
log.error(`${server} could not resolve ${hostname} record ${recordType}`)
}
}

throw new Error(`Could not resolve ${hostname} record TXT`)
throw new Error(`Could not resolve ${hostname} record ${recordType}`)
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ function buildResource ({ serverResolver, hostname, recordType }) {

module.exports.buildResource = buildResource

/**
* Use fetch to find the record.
*
* @param {object} resource
* @returns {Promise}
*/
function fetch (resource) {
return nativeFetch(resource, {
headers: new Headers({
Expand All @@ -26,3 +32,16 @@ function fetch (resource) {
}

module.exports.fetch = fetch

/**
* Creates cache key composed by recordType and hostname.
*
* @param {string} hostname
* @param {string} recordType
* @returns {string}
*/
function getCacheKey (hostname, recordType) {
return `${recordType}_${hostname}`
}

module.exports.getCacheKey = getCacheKey
108 changes: 100 additions & 8 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const { isBrowser } = require('ipfs-utils/src/env')

const DnsOverHttpResolver = require('../')

const getFetchPair = () => isBrowser ? [window, 'fetch'] : [nativeFetch, 'Promise']
const getFetch = () => isBrowser ? window.fetch : nativeFetch.Promise

describe('dns-over-http-resolver', () => {
let resolver

Expand All @@ -16,6 +19,7 @@ describe('dns-over-http-resolver', () => {
})

afterEach(() => {
resolver._cache.clear()
sinon.restore()
})

Expand All @@ -37,7 +41,7 @@ describe('dns-over-http-resolver', () => {
const hostname = 'google.com'
const recordType = 'A'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Expand All @@ -53,7 +57,7 @@ describe('dns-over-http-resolver', () => {
it('resolves a dns record using IPv4', async () => {
const hostname = 'google.com'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Expand All @@ -66,11 +70,33 @@ describe('dns-over-http-resolver', () => {
expect(response).to.eql(['216.58.212.142'])
})

it('resolves a dns record using IPv4 and caches it for next resolve', async () => {
const hostname = 'google.com'

const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Answer: [{ name: 'google.com', type: 1, TTL: 1000, data: '216.58.212.142' }]
})
}))

const response1 = await resolver.resolve4(hostname)
expect(response1).to.exist()
expect(getFetch().callCount).to.eql(1)

const response2 = await resolver.resolve4(hostname)
expect(response2).to.exist()
expect(getFetch().callCount).to.eql(1)

expect(response1).to.eql(response2)
})

it('resolves a dns record of type AAAA', async () => {
const hostname = 'google.com'
const recordType = 'AAAA'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Expand All @@ -93,7 +119,7 @@ describe('dns-over-http-resolver', () => {
it('resolves a dns record using IPv6', async () => {
const hostname = 'google.com'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Expand All @@ -113,11 +139,40 @@ describe('dns-over-http-resolver', () => {
expect(response).to.eql(['2a00:1450:4001:801::200e'])
})

it('resolves a dns record using IPv6 and caches it for next resolve', async () => {
const hostname = 'google.com'

const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'google.com', type: 1 }],
Answer: [
{
name: 'google.com',
type: 28,
TTL: 1000,
data: '2a00:1450:4001:801::200e'
}
]
})
}))

const response1 = await resolver.resolve6(hostname)
expect(response1).to.exist()
expect(getFetch().callCount).to.eql(1)

const response2 = await resolver.resolve6(hostname)
expect(response2).to.exist()
expect(getFetch().callCount).to.eql(1)

expect(response1).to.eql(response2)
})

it('resolves a dns record of type TXT', async () => {
const hostname = 'google.com'
const recordType = 'TXT'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'example.com', type: 1 }],
Expand Down Expand Up @@ -147,7 +202,7 @@ describe('dns-over-http-resolver', () => {
it('resolves a dns record using TXT', async () => {
const hostname = 'example.com'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'example.com', type: 1 }],
Expand All @@ -174,11 +229,48 @@ describe('dns-over-http-resolver', () => {
expect(response).to.eql([['v=spf1 -all'], ['docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e']])
})

it('resolves a dns record using TXT and caches it for next resolve', async () => {
const hostname = 'example.com'

const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.resolve({
json: () => ({
Question: [{ name: 'example.com', type: 1 }],
Answer: [
{
name: 'example.com',
type: 16,
TTL: 86400,
data: '"v=spf1 -all"'
},
{
name: 'example.com',
type: 16,
TTL: 86400,
data: '"docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"'
}
]
})
}))

const response1 = await resolver.resolveTxt(hostname)
expect(response1).to.exist()
expect(response1).to.have.length(2)
expect(getFetch().callCount).to.eql(1)

const response2 = await resolver.resolveTxt(hostname)
expect(response2).to.exist()
expect(response2).to.have.length(2)
expect(getFetch().callCount).to.eql(1)

expect(response1).to.eql(response2)
})

it('should fail if cannot resolve', async () => {
const hostname = 'example.com'
const recordType = 'TXT'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.returns(Promise.reject(new Error()))

await expect(resolver.resolve(hostname, recordType)).to.eventually.be.rejected()
Expand All @@ -187,7 +279,7 @@ describe('dns-over-http-resolver', () => {
it('resolved a dns record from the second server if the first fails', async () => {
const hostname = 'example.com'

const stub = isBrowser ? sinon.stub(window, 'fetch') : sinon.stub(nativeFetch, 'Promise')
const stub = sinon.stub(...getFetchPair())
stub.onCall(0).returns(Promise.reject(new Error()))
stub.onCall(1).returns(Promise.resolve({
json: () => ({
Expand Down

0 comments on commit 2a2aa61

Please sign in to comment.