Skip to content

Commit

Permalink
Respect IPv4 file entries
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak committed Mar 3, 2020
1 parent ab058fa commit baea349
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 60 deletions.
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ Default: [`new dns.promises.Resolver()`](https://nodejs.org/api/dns.html#dns_cla

An instance of [DNS Resolver](https://nodejs.org/api/dns.html#dns_class_dns_resolver) used to make DNS queries.

##### options.customHostsPath

Type: `string`<br>
Default: `undefined` (OS-specific)

The full path to the `hosts` file.

### Entry object

Type: `object`
Expand Down Expand Up @@ -175,18 +182,18 @@ Clears the cache.
See the benchmarks (queries `localhost`, performed on i7-7700k):

```
CacheableLookup#lookupAsync x 2,525,219 ops/sec ±1.66% (86 runs sampled)
CacheableLookup#lookupAsync.all x 2,648,106 ops/sec ±0.59% (88 runs sampled)
CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,263,173 ops/sec ±0.95% (88 runs sampled)
CacheableLookup#lookup x 2,108,952 ops/sec ±0.97% (89 runs sampled)
CacheableLookup#lookup.all x 2,081,357 ops/sec ±1.19% (83 runs sampled)
CacheableLookup#lookup.all.ADDRCONFIG x 1,913,955 ops/sec ±0.60% (89 runs sampled)
CacheableLookup#lookupAsync - zero TTL x 36.50 ops/sec ±11.21% (39 runs sampled)
CacheableLookup#lookup - zero TTL x 33.66 ops/sec ±7.57% (47 runs sampled)
dns#resolve4 x 40.31 ops/sec ±16.10% (49 runs sampled)
dns#lookup x 13,722 ops/sec ±20.69% (37 runs sampled)
dns#lookup.all x 30,343 ops/sec ±28.97% (47 runs sampled)
dns#lookup.all.ADDRCONFIG x 7,023 ops/sec ±15.86% (31 runs sampled)
CacheableLookup#lookupAsync x 4,095,922 ops/sec ±1.01% (84 runs sampled)
CacheableLookup#lookupAsync.all x 4,472,817 ops/sec ±0.67% (88 runs sampled)
CacheableLookup#lookupAsync.all.ADDRCONFIG x 3,713,702 ops/sec ±0.66% (85 runs sampled)
CacheableLookup#lookup x 3,332,170 ops/sec ±0.50% (82 runs sampled)
CacheableLookup#lookup.all x 3,303,159 ops/sec ±0.61% (84 runs sampled)
CacheableLookup#lookup.all.ADDRCONFIG x 2,851,815 ops/sec ±0.82% (84 runs sampled)
CacheableLookup#lookupAsync - zero TTL x 226 ops/sec ±3.55% (56 runs sampled)
CacheableLookup#lookup - zero TTL x 228 ops/sec ±2.48% (62 runs sampled)
dns#resolve4 x 346 ops/sec ±3.58% (55 runs sampled)
dns#lookup x 20,368 ops/sec ±38.31% (53 runs sampled)
dns#lookup.all x 13,529 ops/sec ±31.35% (29 runs sampled)
dns#lookup.all.ADDRCONFIG x 6,211 ops/sec ±22.92% (26 runs sampled)
Fastest is CacheableLookup#lookupAsync.all
```

Expand Down
2 changes: 1 addition & 1 deletion benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const Benchmark = require('benchmark');
const CacheableLookup = require('.');

const cacheable = new CacheableLookup();
const notCacheable = new CacheableLookup({maxTtl: 0});
const notCacheable = new CacheableLookup({maxTtl: 0, customHostsPath: false});
const suite = new Benchmark.Suite();

const options = {
Expand Down
70 changes: 70 additions & 0 deletions create-hosts-resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';
const {stat, readFile} = require('fs').promises;

const isWindows = process.platform === 'win32';
const hostsPath = isWindows ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/hosts';

const fileOptions = {
encoding: 'utf8'
};

const create = (customHostsPath = hostsPath) => {
if (customHostsPath === false) {
return {
hosts: {},
updateHosts: () => {}
};
}

let lastModifiedTime;
let localHosts = {};

const ipAndHost = /^\s*(?<address>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?<hostname>[a-zA-Z0-9-.]{1,63})/;

const updateHosts = async () => {
const {mtimeMs} = await stat(customHostsPath);

if (mtimeMs === lastModifiedTime) {
return localHosts;
}

lastModifiedTime = mtimeMs;

localHosts = {};

const lines = (await readFile(customHostsPath, fileOptions)).split('\n');
for (let line of lines) {
const commentIndex = line.indexOf('#');

if (commentIndex !== -1) {
line = line.slice(0, commentIndex);
}

const result = line.match(ipAndHost);

if (result) {
const {address, hostname} = result.groups;

localHosts[hostname] = [
{
address,
family: 4,
expires: Infinity,
ttl: Infinity
}
];
}
}

return localHosts;
};

return {
get hosts() {
return localHosts;
},
updateHosts
};
};

module.exports = create;
6 changes: 6 additions & 0 deletions hosts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# this is a comment
127.0.0.1 helloworld
127.0.0.1 foobar # this is a comment
# this is yet another comment 127.0.0.1 woofwoof
noiphere
127.0.0.1 # no host here
5 changes: 5 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface Options {
* @default new dns.promises.Resolver()
*/
resolver?: Resolver | AsyncResolver;
/**
* The full path to the `hosts` file.
* @default '/etc/hosts'
*/
customHostsPath?: string | false;
}

interface EntryObject {
Expand Down
54 changes: 38 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const {V4MAPPED, ADDRCONFIG, promises: dnsPromises} = require('dns');
const {promisify} = require('util');
const os = require('os');
const createHostsResolver = require('./create-hosts-resolver');

const {Resolver: AsyncResolver} = dnsPromises;

Expand Down Expand Up @@ -90,7 +91,7 @@ class TTLMap {
const ttl = {ttl: true};

class CacheableLookup {
constructor({maxTtl = Infinity, resolver} = {}) {
constructor({maxTtl = Infinity, resolver, customHostsPath} = {}) {
this.maxTtl = maxTtl;

this._cache = new TTLMap();
Expand All @@ -106,6 +107,9 @@ class CacheableLookup {

this._iface = getIfaceInfo();
this._tickLocked = false;
this._hostsResolver = createHostsResolver(customHostsPath);

this.tick();

this.lookup = this.lookup.bind(this);
this.lookupAsync = this.lookupAsync.bind(this);
Expand All @@ -126,7 +130,7 @@ class CacheableLookup {
}

// eslint-disable-next-line promise/prefer-await-to-then
this.lookupAsync(hostname, {...options, throwNotFound: true}).then(result => {
this.lookupAsync(hostname, options, true).then(result => {
if (options.all) {
callback(null, result);
} else {
Expand All @@ -135,7 +139,7 @@ class CacheableLookup {
}).catch(callback);
}

async lookupAsync(hostname, options = {}) {
async lookupAsync(hostname, options = {}, throwNotFound = undefined) {
let cached = await this.query(hostname);

if (options.family === 6) {
Expand All @@ -155,16 +159,17 @@ class CacheableLookup {
cached = cached.filter(entry => entry.family === 6 ? _iface.has6 : _iface.has4);
}

if (cached.length === 0 && options.throwNotFound) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;
if (cached.length === 0) {
if (throwNotFound || options.throwNotFound) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;

throw error;
}
throw error;
}

const now = Date.now();
cached = cached.filter(entry => entry.ttl === 0 || now < entry.expires);
return undefined;
}

if (options.all) {
return cached;
Expand All @@ -174,16 +179,31 @@ class CacheableLookup {
return cached[0];
}

if (cached.length === 0) {
return undefined;
}

return this._getEntry(cached);
}

async query(hostname) {
let cached = this._cache.get(hostname);
let cached = this._hostsResolver.hosts[hostname];

if (!cached) {
cached = this._cache.get(hostname);

if (cached) {
const now = Date.now();

for (let index = 0; index < cached.length;) {
const entry = cached[index];

if (now < entry.expires || entry.ttl === 0) {
cached.splice(index, 1);
} else {
index++;
}
}
}
}

if (!cached || cached.length === 0) {
cached = await this.queryAndCache(hostname);
}

Expand Down Expand Up @@ -246,6 +266,8 @@ class CacheableLookup {
}
}

this._hostsResolver.updateHosts();

this._tickLocked = true;

setTimeout(() => {
Expand Down
64 changes: 33 additions & 31 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import {expectType} from 'tsd';
import CacheableLookup, {EntryObject} from '.';

const cacheable = new CacheableLookup();

expectType<string[]>(cacheable.servers);

expectType<EntryObject>(await cacheable.lookupAsync('localhost', 4));
expectType<EntryObject>(await cacheable.lookupAsync('localhost', {all: false}));
expectType<ReadonlyArray<EntryObject>>(await cacheable.lookupAsync('localhost', {all: true}));

cacheable.lookup('localhost', 6, (error, address, family) => {
expectType<NodeJS.ErrnoException>(error);
expectType<string>(address);
expectType<4 | 6>(family);
});

cacheable.lookup('localhost', {all: false}, (error, address, family) => {
expectType<NodeJS.ErrnoException>(error);
expectType<string>(address);
expectType<4 | 6>(family);
});

cacheable.lookup('localhost', {all: true}, (error, results) => {
expectType<NodeJS.ErrnoException>(error);
expectType<ReadonlyArray<EntryObject>>(results);
});

expectType<ReadonlyArray<EntryObject>>(await cacheable.query('localhost'));
expectType<ReadonlyArray<EntryObject>>(await cacheable.queryAndCache('localhost'));

expectType<void>(cacheable.updateInterfaceInfo());
expectType<void>(cacheable.tick());
expectType<void>(cacheable.clear());
(async () => {
const cacheable = new CacheableLookup();

expectType<string[]>(cacheable.servers);

expectType<EntryObject>(await cacheable.lookupAsync('localhost', 4));
expectType<EntryObject>(await cacheable.lookupAsync('localhost', {all: false}));
expectType<ReadonlyArray<EntryObject>>(await cacheable.lookupAsync('localhost', {all: true}));

cacheable.lookup('localhost', 6, (error, address, family) => {
expectType<NodeJS.ErrnoException>(error);
expectType<string>(address);
expectType<4 | 6>(family);
});

cacheable.lookup('localhost', {all: false}, (error, address, family) => {
expectType<NodeJS.ErrnoException>(error);
expectType<string>(address);
expectType<4 | 6>(family);
});

cacheable.lookup('localhost', {all: true}, (error, results) => {
expectType<NodeJS.ErrnoException>(error);
expectType<ReadonlyArray<EntryObject>>(results);
});

expectType<ReadonlyArray<EntryObject>>(await cacheable.query('localhost'));
expectType<ReadonlyArray<EntryObject>>(await cacheable.queryAndCache('localhost'));

expectType<void>(cacheable.updateInterfaceInfo());
expectType<void>(cacheable.tick());
expectType<void>(cacheable.clear());
})();
58 changes: 58 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,61 @@ test.serial('double tick() has no effect', t => {

global.setTimeout = _setTimeout;
});

test('respects the `hosts` file', async t => {
const cacheable = new CacheableLookup({
customHostsPath: './hosts.txt'
});

await sleep(100);

const getAddress = async (hostname) => {
const result = await cacheable.lookupAsync(hostname);

if (result) {
t.is(result.family, 4);
t.is(result.ttl, Infinity);
t.is(result.expires, Infinity);

return result.address;
}

return result;
};

t.is(await getAddress('helloworld'), '127.0.0.1');
t.is(await getAddress('foobar'), '127.0.0.1');
t.is(await getAddress('woofwoof'), undefined);
t.is(await getAddress('noiphere'), undefined);
});

test('respects the `hosts` file #2', async t => {
const cacheable = new CacheableLookup({
customHostsPath: false,
resolver
});

await sleep(100);

const getAddress = async (hostname) => {
const result = await cacheable.lookupAsync(hostname);

if (result) {
t.is(result.family, 4);
t.is(result.ttl, Infinity);
t.is(result.expires, Infinity);

return result.address;
}

return result;
};

t.is(await getAddress('helloworld'), undefined);
t.is(await getAddress('foobar'), undefined);
t.is(await getAddress('woofwoof'), undefined);
t.is(await getAddress('noiphere'), undefined);

const {address} = await cacheable.lookupAsync('localhost');
t.is(address, '127.0.0.1');
});

0 comments on commit baea349

Please sign in to comment.