├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── sample.js ├── lib ├── cache.js └── index.js ├── package.json └── test ├── index.js ├── testcache.js └── testcoverage.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | artifacts 4 | .nyc_output 5 | .DS_Store 6 | .DS_Store? 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | - "11" 7 | before_install: npm install -g npm@latest-2 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Yahoo! Inc. All rights reserved. 2 | 3 | Redistribution and use of this software in source and binary forms, 4 | with or without modification, are permitted provided that the following 5 | conditions are met: 6 | 7 | * Redistributions of source code must retain the above 8 | copyright notice, this list of conditions and the 9 | following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the 13 | following disclaimer in the documentation and/or other 14 | materials provided with the distribution. 15 | 16 | * Neither the name of Yahoo! Inc. nor the names of its 17 | contributors may be used to endorse or promote products 18 | derived from this software without specific prior 19 | written permission of Yahoo! Inc. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ARCHIVED 3 | 4 | dnscache for Node 5 | =================== 6 | 7 | This module wraps the [dns](http://nodejs.org/api/dns.html) module methods and provide a caching layer in between. 8 | Every call to a dns method is first looked into the local cache, in case of cache hit the value from cache is returned, in case of cache miss the original dns call is made 9 | and the return value is cached in the local cache. 10 | 11 | It is very similar to GOF Proxy design pattern providing a Cache Proxy. 12 | 13 | The goal of this module is to cache the most used/most recent dns calls, to avoid the network delay and improve the performance. 14 | 15 | Once this module is enabled, all the subsequent calls to `require('dns')` are wrapped too. 16 | 17 | **NOTE:** There are situations where the built-in `dns` functions would throw, rather than call back with an error. Due to the fact that asynchronous caching mechanisms are supported, all errors for these functions will be passed as the first argument to the callback. 18 | 19 | Installation 20 | ------------ 21 | 22 | `npm install dnscache` 23 | 24 | Usage 25 | ----- 26 | 27 | ```javascript 28 | var dns = require('dns'), 29 | dnscache = require('dnscache')({ 30 | "enable" : true, 31 | "ttl" : 300, 32 | "cachesize" : 1000 33 | }); 34 | 35 | //to use the cached dns either of dnscache or dns can be called. 36 | //all the methods of dns are wrapped, this one just shows lookup on an example 37 | 38 | //will call the wrapped dns 39 | dnscache.lookup('www.yahoo.com', function(err, result) { 40 | //do something with result 41 | }); 42 | 43 | //will call the wrapped dns 44 | dns.lookup('www.google.com', function(err, result) { 45 | //do something with result 46 | }); 47 | 48 | 49 | ``` 50 | 51 | Configuration 52 | ------------- 53 | 54 | * `enable` - Whether dnscache is enabled or not, defaults to `false`. 55 | * `ttl` - ttl in seconds for cache-entries. Default: `300` 56 | * `cachesize` - number of cache entries, defaults to `1000` 57 | * `cache` - If a custom cache needs to be used instead of the supplied cache implementation. Only for Advanced Usage. Custom Cache needs to have same interface for `get` and `set`. 58 | 59 | 60 | Advanced Caching 61 | ---------------- 62 | 63 | If you want to use a different cache mechanism (ex: `mdbm`, `redis`), you only need to create an object similar to this: 64 | 65 | ```javascript 66 | var Cache = function(config) { 67 | 68 | this.set = function(key, value, callback) {}; 69 | 70 | this.get = function(key, callback) {}; 71 | }; 72 | ``` 73 | 74 | 75 | Build Status 76 | ------------ 77 | 78 | [![Build Status](https://secure.travis-ci.org/yahoo/dnscache.png?branch=master)](http://travis-ci.org/yahoo/dnscache) 79 | 80 | 81 | Node Badge 82 | ---------- 83 | 84 | [![NPM](https://nodei.co/npm/dnscache.png)](https://nodei.co/npm/dnscache/) 85 | -------------------------------------------------------------------------------- /examples/sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved. 3 | * Copyrights licensed under the New BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | */ 6 | 7 | var dns = require('dns'), 8 | dnscache = require('../lib/index.js')({"enable" : true, "ttl" : 300, "cachesize" : 1000}), 9 | now = Date.now(), now1; 10 | //to use the cached dns either of dnscahe or dns can be called 11 | dnscache.lookup('www.yahoo.com', function(err, result) { 12 | console.log('lookup time to www.yahoo.com without cache: ', Date.now() - now); 13 | now = Date.now(); 14 | dnscache.lookup('www.yahoo.com', function(err, result) { 15 | console.log('lookup time to www.yahoo.com with cache: ', Date.now() - now); 16 | console.log(typeof dnscache.internalCache); 17 | }); 18 | }); 19 | 20 | now1 = Date.now(); 21 | //dns methods are wrapped too with the cached dns 22 | dns.lookup('www.google.com', function(err, result) { 23 | console.log('lookup time to www.google.com without cache: ', Date.now() - now1); 24 | now1 = Date.now(); 25 | dns.lookup('www.google.com', function(err, result) { 26 | console.log('lookup time to www.google.com with cache: ', Date.now() - now1); 27 | console.log(typeof dns.internalCache); 28 | }); 29 | }); 30 | 31 | 32 | /* Sample output on console.log 33 | * lookup time to www.yahoo.com without cache: 6 34 | * lookup time to www.yahoo.com with cache: 1 35 | * object 36 | * lookup time to www.google.com without cache: 8 37 | * lookup time to www.google.com with cache: 1 38 | * object 39 | */ -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved. 3 | * Copyrights licensed under the New BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | */ 6 | 7 | var CacheObject = function (conf) { 8 | conf = conf || {}; 9 | conf.ttl = parseInt(conf.ttl, 10) || 300; //0 is not permissible 10 | conf.cachesize = parseInt(conf.cachesize, 10) || 1000; //0 is not permissible 11 | 12 | this.ttl = conf.ttl * 1000; 13 | this.max = conf.cachesize; 14 | 15 | this.count = 0; 16 | this.data = {}; 17 | var next = require('asap'); 18 | 19 | this.set = function (key, value, callback) { 20 | var self = this; 21 | next(function () { 22 | if (self.data[key]) { 23 | if (self.data[key].newer) { 24 | if (self.data[key].older) { 25 | self.data[key].newer.older = self.data[key].older; 26 | self.data[key].older.newer = self.data[key].newer; 27 | } else { 28 | self.tail = self.data[key].newer; 29 | delete self.tail.older; 30 | } 31 | 32 | self.data[key].older = self.head; 33 | self.head.newer = self.data[key]; 34 | delete self.data[key].newer; 35 | self.head = self.data[key]; 36 | } 37 | 38 | self.head.val = value; 39 | self.head.hit = 0; 40 | self.head.ts = Date.now(); 41 | } else { 42 | // key is not exist 43 | self.data[key] = { 44 | "key" : key, 45 | "val" : value, 46 | "hit" : 0, 47 | "ts" : Date.now() 48 | }; 49 | 50 | if (!self.head) { 51 | // cache is empty 52 | self.head = self.data[key]; 53 | self.tail = self.data[key]; 54 | } else { 55 | // insert the new entry to the front 56 | self.head.newer = self.data[key]; 57 | self.data[key].older = self.head; 58 | self.head = self.data[key]; 59 | } 60 | 61 | if (self.count >= self.max) { 62 | // remove the tail 63 | var temp = self.tail; 64 | self.tail = self.tail.newer; 65 | delete self.tail.next; 66 | delete self.data[temp.key]; 67 | } else { 68 | self.count = self.count + 1; 69 | } 70 | } 71 | /* jshint -W030 */ 72 | callback && callback(null, value); 73 | }); 74 | }; 75 | 76 | this.get = function (key, callback) { 77 | var self = this; 78 | if (!callback) { 79 | throw('cache.get callback is required.'); 80 | } 81 | 82 | next(function () { 83 | if (!self.data[key]) { 84 | return callback(null, undefined); 85 | } 86 | var value; 87 | if (conf.ttl !== 0 && (Date.now() - self.data[key].ts) >= self.ttl) { 88 | if (self.data[key].newer) { 89 | if (self.data[key].older) { 90 | // in the middle of the list 91 | self.data[key].newer.older = self.data[key].older; 92 | self.data[key].older.newer = self.data[key].newer; 93 | } else { 94 | // tail 95 | self.tail = self.data[key].newer; 96 | delete self.tail.older; 97 | } 98 | } else { 99 | // the first item 100 | if (self.data[key].older) { 101 | self.head = self.data[key].older; 102 | delete self.head.newer; 103 | } else { 104 | // 1 items 105 | delete self.head; 106 | delete self.tail; 107 | } 108 | } 109 | 110 | delete self.data[key]; 111 | self.count = self.count - 1; 112 | } else { 113 | self.data[key].hit = self.data[key].hit + 1; 114 | value = self.data[key].val; 115 | } 116 | callback(null, value); 117 | }); 118 | }; 119 | }; 120 | 121 | module.exports = CacheObject; 122 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved. 3 | * Copyrights licensed under the New BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | */ 6 | 7 | var CacheObject = require('./cache.js'), 8 | deepCopy = require('lodash.clone'), 9 | dns = require('dns'); 10 | 11 | /* 12 | * Stores the list of callbacks waiting for a dns call. 13 | */ 14 | var callbackList = {}; 15 | 16 | // original function storage 17 | var backup_object = { 18 | lookup : dns.lookup, 19 | resolve : dns.resolve, 20 | resolve4 : dns.resolve4, 21 | resolve6 : dns.resolve6, 22 | resolveMx : dns.resolveMx, 23 | resolveTxt : dns.resolveTxt, 24 | resolveSrv : dns.resolveSrv, 25 | resolveNs : dns.resolveNs, 26 | resolveCname : dns.resolveCname, 27 | reverse : dns.reverse 28 | }; 29 | 30 | /* 31 | * Make a deep copy of the supplied object. This function reliably copies only 32 | * what is valid for a JSON object, array, or other element. 33 | */ 34 | var deepCopy = function (o) { 35 | var newArr, ix, newObj, prop; 36 | 37 | if (!o || typeof o !== 'object') { 38 | return o; 39 | } 40 | 41 | if (Array.isArray(o)) { 42 | newArr = []; 43 | for (ix = 0; ix < o.length; ix += 1) { 44 | newArr.push(deepCopy(o[ix])); 45 | } 46 | return newArr; 47 | } else { 48 | newObj = {}; 49 | for (prop in o) { 50 | /* istanbul ignore else */ 51 | if (o.hasOwnProperty(prop)) { 52 | newObj[prop] = deepCopy(o[prop]); 53 | } 54 | } 55 | return newObj; 56 | } 57 | }; 58 | 59 | 60 | // original function storage 61 | var EnhanceDns = function (conf) { 62 | conf = conf || {}; 63 | conf.ttl = parseInt(conf.ttl, 10) || 300; //0 is not allowed ie it ttl is set to 0, it will take the default 64 | conf.cachesize = parseInt(conf.cachesize, 10); //0 is allowed but it will disable the caching 65 | 66 | if (isNaN(conf.cachesize)) { 67 | conf.cachesize = 1000; //set default cache size to 1000 records max 68 | } 69 | 70 | if(!conf.enable || conf.cachesize <= 0) { 71 | delete dns.internalCache; 72 | for(var key in backup_object) { 73 | /* istanbul ignore else */ 74 | if (backup_object.hasOwnProperty(key)) { 75 | dns[key] = backup_object[key]; 76 | } 77 | } 78 | return dns; 79 | } 80 | 81 | // insert cache object to the instance 82 | if(!dns.internalCache) { 83 | dns.internalCache = conf.cache ? new conf.cache(conf) : new CacheObject(conf); 84 | callbackList = {}; 85 | } 86 | 87 | var cache = dns.internalCache; 88 | 89 | // override dns.lookup method 90 | dns.lookup = function (domain, options, callback) { 91 | var family = 0; 92 | var hints = 0; 93 | var all = false; 94 | if (arguments.length === 2) { 95 | callback = options; 96 | options = family; 97 | } else if (typeof options === 'object') { 98 | if (options.family) { 99 | family = +options.family; 100 | if (family !== 4 && family !== 6) { 101 | callback(new Error('invalid argument: `family` must be 4 or 6')); 102 | return; 103 | } 104 | } 105 | /*istanbul ignore next - "hints" require node 0.12+*/ 106 | if (options.hints) { 107 | hints = +options.hints; 108 | } 109 | all = (options.all === true); 110 | } else if (options) { 111 | family = +options; 112 | if (family !== 4 && family !== 6) { 113 | callback(new Error('invalid argument: `family` must be 4 or 6')); 114 | return; 115 | } 116 | } 117 | 118 | var key = 'lookup_' + domain + '_' + family + '_' + hints + '_' + all; 119 | cache.get(key, function (error, record) { 120 | if (record) { 121 | /*istanbul ignore next - "all" option require node 4+*/ 122 | if (Array.isArray(record)) { 123 | return callback(error, record); 124 | } 125 | return callback(error, record.address, record.family); 126 | } 127 | 128 | var list = callbackList[key] = callbackList[key] || []; 129 | list.push(callback); 130 | 131 | if (list.length > 1) { return; } 132 | 133 | try{ 134 | backup_object.lookup(domain, options, function (err, address, family_r) { 135 | if (err) { 136 | list.forEach(function (cb) { cb(err); }); 137 | delete callbackList[key]; 138 | return; 139 | } 140 | var value; 141 | /*istanbul ignore next - "all" option require node 4+*/ 142 | if (Array.isArray(address)) { 143 | value = deepCopy(address); 144 | } else { 145 | value = { 146 | 'address' : address, 147 | 'family' : family_r 148 | }; 149 | } 150 | cache.set(key, value, function () { 151 | list.forEach(function (cb) { cb(err, address, family_r); }); 152 | delete callbackList[key]; 153 | }); 154 | }); 155 | } catch (err) { 156 | /*istanbul ignore next - doesn't throw in node 0.10*/ 157 | callback(err); 158 | } 159 | }); 160 | }; 161 | 162 | // override dns.resolve method 163 | dns.resolve = function (domain, type, callback) { 164 | var type_new, callback_new; 165 | 166 | if (typeof type === 'string') { 167 | type_new = type; 168 | callback_new = callback; 169 | } else { 170 | type_new = "A"; 171 | callback_new = type; 172 | } 173 | 174 | var key = 'resolve_' + domain + '_' + type_new; 175 | cache.get(key, function (error, record) { 176 | if (record) { 177 | return callback_new(error, deepCopy(record), true); 178 | } 179 | 180 | var list = callbackList[key] = callbackList[key] || []; 181 | list.push(callback_new); 182 | 183 | if (list.length > 1) { return; } 184 | 185 | try { 186 | backup_object.resolve(domain, type_new, function (err, addresses) { 187 | if (err) { 188 | list.forEach(function (cb) { cb(err); }); 189 | delete callbackList[key]; 190 | return; 191 | } 192 | cache.set(key, addresses, function () { 193 | list.forEach(function (cb) { cb(err, deepCopy(addresses), false); }); 194 | delete callbackList[key]; 195 | }); 196 | }); 197 | } catch (err) { 198 | /*istanbul ignore next - doesn't throw in node 0.10*/ 199 | callback_new(err); 200 | } 201 | }); 202 | }; 203 | 204 | // override dns.resolve4 method 205 | dns.resolve4 = function (domain, callback) { 206 | var key = 'resolve4_' + domain; 207 | cache.get(key, function (error, record) { 208 | if (record) { 209 | return callback(error, deepCopy(record)); 210 | } 211 | 212 | var list = callbackList[key] = callbackList[key] || []; 213 | list.push(callback); 214 | 215 | if (list.length > 1) { return; } 216 | 217 | try { 218 | backup_object.resolve4(domain, function (err, addresses) { 219 | if (err) { 220 | list.forEach(function (cb) { cb(err); }); 221 | delete callbackList[key]; 222 | return; 223 | } 224 | cache.set(key, addresses, function () { 225 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 226 | delete callbackList[key]; 227 | }); 228 | }); 229 | } catch (err) { 230 | /*istanbul ignore next - doesn't throw in node 0.10*/ 231 | callback(err); 232 | } 233 | }); 234 | }; 235 | 236 | // override dns.resolve6 method 237 | dns.resolve6 = function (domain, callback) { 238 | var key = 'resolve6_' + domain; 239 | cache.get(key, function (error, record) { 240 | if (record) { 241 | return callback(error, deepCopy(record)); 242 | } 243 | 244 | var list = callbackList[key] = callbackList[key] || []; 245 | list.push(callback); 246 | 247 | if (list.length > 1) { return; } 248 | 249 | try { 250 | backup_object.resolve6(domain, function (err, addresses) { 251 | if (err) { 252 | list.forEach(function (cb) { cb(err); }); 253 | delete callbackList[key]; 254 | return; 255 | } 256 | cache.set(key, addresses, function () { 257 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 258 | delete callbackList[key]; 259 | }); 260 | }); 261 | } catch (err) { 262 | /*istanbul ignore next - doesn't throw in node 0.10*/ 263 | callback(err); 264 | } 265 | }); 266 | }; 267 | 268 | // override dns.resolveMx method 269 | dns.resolveMx = function (domain, callback) { 270 | var key = 'resolveMx_' + domain; 271 | cache.get(key, function (error, record) { 272 | if (record) { 273 | return callback(error, deepCopy(record)); 274 | } 275 | 276 | var list = callbackList[key] = callbackList[key] || []; 277 | list.push(callback); 278 | 279 | if (list.length > 1) { return; } 280 | 281 | try { 282 | backup_object.resolveMx(domain, function (err, addresses) { 283 | if (err) { 284 | list.forEach(function (cb) { cb(err); }); 285 | delete callbackList[key]; 286 | return; 287 | } 288 | cache.set(key, addresses, function () { 289 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 290 | delete callbackList[key]; 291 | }); 292 | }); 293 | } catch (err) { 294 | /*istanbul ignore next - doesn't throw in node 0.10*/ 295 | callback(err); 296 | } 297 | }); 298 | }; 299 | 300 | // override dns.resolveTxt method 301 | dns.resolveTxt = function (domain, callback) { 302 | var key = 'resolveTxt_' + domain; 303 | cache.get(key, function (error, record) { 304 | if (record) { 305 | return callback(error, deepCopy(record)); 306 | } 307 | 308 | var list = callbackList[key] = callbackList[key] || []; 309 | list.push(callback); 310 | 311 | if (list.length > 1) { return; } 312 | 313 | try { 314 | backup_object.resolveTxt(domain, function (err, addresses) { 315 | if (err) { 316 | list.forEach(function (cb) { cb(err); }); 317 | delete callbackList[key]; 318 | return; 319 | } 320 | cache.set(key, addresses, function () { 321 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 322 | delete callbackList[key]; 323 | }); 324 | }); 325 | } catch (err) { 326 | /*istanbul ignore next - doesn't throw in node 0.10*/ 327 | callback(err); 328 | } 329 | }); 330 | }; 331 | 332 | // override dns.resolveSrv method 333 | dns.resolveSrv = function (domain, callback) { 334 | var key = 'resolveSrv_' + domain; 335 | cache.get(key, function (error, record) { 336 | if (record) { 337 | return callback(error, deepCopy(record)); 338 | } 339 | 340 | var list = callbackList[key] = callbackList[key] || []; 341 | list.push(callback); 342 | 343 | /* istanbul ignore if */ 344 | if (list.length > 1) { return; } 345 | 346 | try { 347 | backup_object.resolveSrv(domain, function (err, addresses) { 348 | if (err) { 349 | list.forEach(function (cb) { cb(err); }); 350 | delete callbackList[key]; 351 | return; 352 | } 353 | cache.set(key, addresses, function () { 354 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 355 | delete callbackList[key]; 356 | }); 357 | }); 358 | } catch (err) { 359 | /*istanbul ignore next - doesn't throw in node 0.10*/ 360 | callback(err); 361 | } 362 | }); 363 | }; 364 | 365 | // override dns.resolveNs method 366 | dns.resolveNs = function (domain, callback) { 367 | var key = 'resolveNs_' + domain; 368 | cache.get(key, function (error, record) { 369 | if (record) { 370 | return callback(error, deepCopy(record)); 371 | } 372 | 373 | var list = callbackList[key] = callbackList[key] || []; 374 | list.push(callback); 375 | 376 | if (list.length > 1) { return; } 377 | 378 | try { 379 | backup_object.resolveNs(domain, function (err, addresses) { 380 | if (err) { 381 | list.forEach(function (cb) { cb(err); }); 382 | delete callbackList[key]; 383 | return; 384 | } 385 | cache.set(key, addresses, function () { 386 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 387 | delete callbackList[key]; 388 | }); 389 | }); 390 | } catch (err) { 391 | /*istanbul ignore next - doesn't throw in node 0.10*/ 392 | callback(err); 393 | } 394 | }); 395 | }; 396 | 397 | // override dns.resolveCname method 398 | dns.resolveCname = function (domain, callback) { 399 | var key = 'resolveCname_' + domain; 400 | cache.get(key, function (error, record) { 401 | if (record) { 402 | return callback(error, deepCopy(record)); 403 | } 404 | 405 | var list = callbackList[key] = callbackList[key] || []; 406 | list.push(callback); 407 | 408 | if (list.length > 1) { return; } 409 | 410 | try { 411 | backup_object.resolveCname(domain, function (err, addresses) { 412 | if (err) { 413 | list.forEach(function (cb) { cb(err); }); 414 | delete callbackList[key]; 415 | return; 416 | } 417 | cache.set(key, addresses, function () { 418 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 419 | delete callbackList[key]; 420 | }); 421 | }); 422 | } catch (err) { 423 | /*istanbul ignore next - doesn't throw in node 0.10*/ 424 | callback(err); 425 | } 426 | }); 427 | }; 428 | 429 | // override dns.reverse method 430 | dns.reverse = function (ip, callback) { 431 | var key = 'reverse_' + ip; 432 | cache.get(key, function (error, record) { 433 | if (record) { 434 | return callback(error, deepCopy(record)); 435 | } 436 | 437 | var list = callbackList[key] = callbackList[key] || []; 438 | list.push(callback); 439 | 440 | if (list.length > 1) { return; } 441 | 442 | try { 443 | backup_object.reverse(ip, function (err, addresses) { 444 | if (err) { 445 | list.forEach(function (cb) { cb(err); }); 446 | delete callbackList[key]; 447 | return; 448 | } 449 | cache.set(key, addresses, function () { 450 | list.forEach(function (cb) { cb(err, deepCopy(addresses)); }); 451 | delete callbackList[key]; 452 | }); 453 | }); 454 | } catch (err) { 455 | /*istanbul ignore next - doesn't throw in node 0.10*/ 456 | callback(err); 457 | } 458 | }); 459 | }; 460 | return dns; 461 | }; 462 | 463 | module.exports = function(conf) { 464 | return new EnhanceDns(conf); 465 | }; 466 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dnscache", 3 | "description": "dnscache for Node", 4 | "author": "Vinit Sacheti ", 5 | "version": "1.0.2", 6 | "keywords": [ 7 | "dnscache", 8 | "dns" 9 | ], 10 | "main": "./lib/index.js", 11 | "scripts": { 12 | "pretest": "jshint --config ./node_modules/yui-lint/jshint.json ./lib/ ./test/", 13 | "test": "nyc mocha ./test/*.js", 14 | "posttest": "nyc check-coverage --lines 100 --functions 100 --branches 100 && nyc report --reporter=lcov" 15 | }, 16 | "bugs": { 17 | "url": "http://github.com/yahoo/dnscache/issues" 18 | }, 19 | "license": "BSD", 20 | "repository": { 21 | "type": "git", 22 | "url": "http://github.com/yahoo/dnscache.git" 23 | }, 24 | "devDependencies": { 25 | "async": "^2.6.2", 26 | "jshint": "^2.10.2", 27 | "mocha": "^6.1.4", 28 | "nyc": "^14.0.0", 29 | "yui-lint": "^0.2.0" 30 | }, 31 | "dependencies": { 32 | "asap": "^2.0.6", 33 | "lodash.clone": "^4.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | /* 3 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved. 4 | * Copyrights licensed under the New BSD License. 5 | * See the accompanying LICENSE file for terms. 6 | */ 7 | 8 | var assert = require('assert'), 9 | async = require('async'), 10 | mod = require('../lib/index.js')({ 11 | enable: true, 12 | ttl: 300, 13 | cachesize: 1000 14 | }); 15 | 16 | var dns = require('dns'); 17 | var methods = [dns.lookup, dns.resolve, dns.resolve4, dns.resolve6, dns.resolveMx,dns.resolveTxt, 18 | dns.resolveSrv, dns.resolveNs, dns.resolveCname, dns.reverse]; 19 | var params = ["www.yahoo.com", "www.google.com", "www.google.com", "ipv6.google.com", "yahoo.com", 20 | "google.com", "www.yahoo.com", "yahoo.com", "www.yahoo.com", "173.236.27.26"]; 21 | var prefix = ['lookup_', 'resolve_', 'resolve4_', 'resolve6_', 'resolveMx_', 'resolveTxt_', 22 | 'resolveSrv_', 'resolveNs_', 'resolveCname_', 'reverse_']; 23 | var suffix = ['_0_0_false', '_A', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none']; 24 | 25 | // node v4+ dns.lookup support "all" option. 26 | var node_support_lookup_all = process.versions.node.split('.')[0] >= 4; 27 | // node v0.12+ dns.lookup support "hints" option. 28 | var node_support_lookup_hints = process.versions.node.split('.')[0] > 0 || process.versions.node.split('.')[1] >= 12; 29 | 30 | describe('dnscache main test suite', function() { 31 | this.timeout(10000); //dns queries are slow.. 32 | 33 | it('should export an Object', function () { 34 | assert.equal(typeof mod, 'object'); 35 | }); 36 | 37 | it('should verify internal cache is create for each call', function (done) { 38 | var index = 0; 39 | async.eachSeries(methods, function(method, cb) { 40 | method(params[index], function(err, result) { 41 | ++index; 42 | cb(err, result); 43 | }); 44 | }, function () { 45 | assert.ok(dns.internalCache); 46 | methods.forEach(function(name, index) { 47 | var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; 48 | assert.ok(dns.internalCache.data[key], 'entry not there for ' + key); 49 | assert.equal(dns.internalCache.data[key].hit, 0, 'hit should be 0 for ' + key); 50 | }); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('verify hits are incremented', function (done) { 56 | var index = 0; 57 | async.eachSeries(methods, function(method, cb) { 58 | method(params[index], function(err, result) { 59 | ++index; 60 | cb(err, result); 61 | }); 62 | }, function () { 63 | assert.ok(dns.internalCache); 64 | methods.forEach(function(name, index) { 65 | var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; 66 | assert.ok(dns.internalCache.data[key], 'entry not there for ' + key); 67 | assert.equal(dns.internalCache.data[key].hit, 1, 'hit should be 1 for ' + key); 68 | }); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('require again and verify cache is same as before', function (done) { 74 | require('../lib/index.js')({ 75 | enable: true, 76 | ttl: 300, 77 | cachesize: 1000 78 | }); 79 | assert.ok(dns.internalCache); 80 | methods.forEach(function(name, index) { 81 | var key = suffix[index] !== 'none' ? prefix[index] + params[index] + suffix[index] : prefix[index] + params[index]; 82 | assert.ok(dns.internalCache.data[key], 'entry not there for ' + key); 83 | assert.equal(dns.internalCache.data[key].hit, 1, 'hit should be 1 for ' + key); 84 | }); 85 | done(); 86 | }); 87 | 88 | it('should verify family4/family6 cache is created for local addresses', function (done) { 89 | dns.lookup('127.0.0.1', 4, function() { 90 | dns.lookup('::1', 6, function() { 91 | dns.lookup('127.0.0.1', { family: 4, hints: dns.ADDRCONFIG }, function() { 92 | dns.lookup('127.0.0.1', { hints: dns.ADDRCONFIG }, function() { 93 | dns.lookup('::1', { family: 6, hints: dns.ADDRCONFIG }, function() { 94 | assert.ok(dns.internalCache); 95 | var hit = node_support_lookup_hints ? 0 : 1; 96 | assert.equal(dns.internalCache.data['lookup_127.0.0.1_4_0_false'].hit, hit, 'hit should be ' + hit + ' for family4'); 97 | assert.equal(dns.internalCache.data['lookup_::1_6_0_false'].hit, hit, 'hit should be ' + hit + ' for family6'); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | }); 105 | 106 | it('should error if the underlying dns method throws', function(done) { 107 | var errors = []; 108 | async.each(methods, function(method, cb) { 109 | method([], function(err) { 110 | errors.push(err); 111 | cb(null); 112 | }); 113 | }, function (err) { 114 | assert.ok(!err); 115 | assert.ok(Array.isArray(errors)); 116 | assert.ok(errors.length > 0); 117 | errors.forEach(function(e) { 118 | if (e) { //one node 0.10 method doens't throw 119 | assert.ok((e instanceof Error)); 120 | } 121 | }); 122 | done(); 123 | }); 124 | }); 125 | 126 | it('should error on invalid reverse lookup', function(done) { 127 | // from the TEST-NET-1 block, as specified in https://tools.ietf.org/html/rfc5737 128 | dns.reverse('192.0.2.0', function(err) { 129 | assert.ok((err instanceof Error)); 130 | done(); 131 | }); 132 | }); 133 | 134 | it('should error on invalid family', function(done) { 135 | dns.lookup('127.0.0.1', 7, function(err) { 136 | assert.ok((err instanceof Error)); 137 | done(); 138 | }); 139 | }); 140 | 141 | it('should error on invalid family Object', function() { 142 | dns.lookup('127.0.0.1', { family: 7 }, function(err) { 143 | assert.ok((err instanceof Error)); 144 | }); 145 | }); 146 | 147 | it('should create resolve cache with type', function (done) { 148 | dns.resolve('www.yahoo.com', 'A', function(err, result) { 149 | assert.ok(dns.internalCache); 150 | assert.ok(result); 151 | assert.equal(dns.internalCache.data['resolve_www.yahoo.com_A'].hit, 0, 'hit should be 0 for resolve'); 152 | done(); 153 | }); 154 | }); 155 | 156 | it('not create a cache from an error in a lookup', function (done) { 157 | var index = 0; 158 | async.eachSeries(methods, function(method, cb) { 159 | method('someerrordata', function(err) { 160 | ++index; 161 | cb(null, err); 162 | }); 163 | }, function () { 164 | methods.forEach(function(name, index) { 165 | var key = suffix[index] !== 'none' ? prefix[index] + 'someerrordata' + suffix[index] : prefix[index] + 'someerrordata'; 166 | assert.equal(dns.internalCache.data[key], undefined, 'entry should not there for ' + key); 167 | }); 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should not cache by default', function(done) { 173 | //if created from other tests 174 | if (require('dns').internalCache) { 175 | delete require('dns').internalCache; 176 | } 177 | var testee = require('../lib/index.js')(); 178 | testee.lookup('127.0.0.1', function() { 179 | assert.ok(!dns.internalCache); 180 | assert.ok(!require('dns').internalCache); 181 | done(); 182 | }); 183 | }); 184 | 185 | it('should not cache if enabled: false', function(done) { 186 | //if created from other tests 187 | if (require('dns').internalCache) { 188 | delete require('dns').internalCache; 189 | } 190 | var testee = require('../lib/index.js')({ 191 | enable: false 192 | }); 193 | 194 | testee.lookup('127.0.0.1', function() { 195 | assert.ok(!testee.internalCache); 196 | assert.ok(!require('dns').internalCache); 197 | done(); 198 | }); 199 | }); 200 | 201 | it('should not cache if cachsize is 0', function(done) { 202 | //if created from other tests 203 | if (require('dns').internalCache) { 204 | delete require('dns').internalCache; 205 | } 206 | var testee = require('../lib/index.js')({ 207 | enable: true, 208 | cachesize: 0 209 | }); 210 | testee.lookup('127.0.0.1', function() { 211 | assert.ok(!testee.internalCache); 212 | assert.ok(!require('dns').internalCache); 213 | done(); 214 | }); 215 | }); 216 | 217 | it('should create a cache with default settings', function(done) { 218 | //if created from other tests 219 | if (require('dns').internalCache) { 220 | delete require('dns').internalCache; 221 | } 222 | var conf = { 223 | enable: true 224 | }, 225 | testee = require('../lib/index.js')(conf); 226 | 227 | testee.lookup('127.0.0.1', function() { 228 | //verify cache is created 229 | assert.ok(testee.internalCache); 230 | assert.equal(testee.internalCache.data['lookup_127.0.0.1_0_0_false'].hit, 0, 'hit should be 0 for family4'); 231 | assert.ok(dns.internalCache); 232 | assert.equal(dns.internalCache.data['lookup_127.0.0.1_0_0_false'].hit, 0, 'hit should be 0 for family4'); 233 | 234 | //verify default values 235 | assert.ok(conf); 236 | assert.equal(conf.ttl, 300); 237 | assert.equal(conf.cachesize, 1000); 238 | done(); 239 | }); 240 | }); 241 | 242 | // lookup's all option require node v4+. 243 | if (node_support_lookup_all) { 244 | it('should return array if lookup all', function(done) { 245 | //if created from other tests 246 | if (require('dns').internalCache) { 247 | delete require('dns').internalCache; 248 | } 249 | var conf = { 250 | enable: true 251 | }, 252 | testee = require('../lib/index.js')(conf); 253 | dns.lookup('127.0.0.1', {all: true}, function(err, addresses) { 254 | assert.ok(Array.isArray(addresses)); 255 | assert.equal(testee.internalCache.data['lookup_127.0.0.1_0_0_true'].hit, 0, 'hit should be 0'); 256 | dns.lookup('127.0.0.1', {all: true}, function(err, addresses) { 257 | assert.ok(Array.isArray(addresses)); 258 | assert.equal(testee.internalCache.data['lookup_127.0.0.1_0_0_true'].hit, 1, 'hit should be 1'); 259 | done(); 260 | }); 261 | }); 262 | }); 263 | 264 | it('should return an array copy if lookup all', function(done) { 265 | //if created from other tests 266 | if (require('dns').internalCache) { 267 | delete require('dns').internalCache; 268 | } 269 | var conf = { 270 | enable: true 271 | }, 272 | testee = require('../lib/index.js')(conf); 273 | dns.lookup('127.0.0.1', {all: true}, function(err, addresses) { 274 | assert.ok(Array.isArray(addresses)); 275 | addresses.pop(); 276 | assert.equal(testee.internalCache.data['lookup_127.0.0.1_0_0_true'].val.length, 1, 'length should be 1'); 277 | done(); 278 | }); 279 | }); 280 | } 281 | }); 282 | -------------------------------------------------------------------------------- /test/testcache.js: -------------------------------------------------------------------------------- 1 | /*jshint newcap: false */ 2 | /*global describe, it*/ 3 | /* 4 | * Copyright (c) 2013, Yahoo! Inc. All rights reserved. 5 | * Copyrights licensed under the New BSD License. 6 | * See the accompanying LICENSE file for terms. 7 | */ 8 | 9 | var assert = require('assert'), 10 | async = require('async'), 11 | Mod_cache = require('../lib/cache.js'); 12 | 13 | describe('caching tests', function() { 14 | 15 | it('should return a Cache Object with defaults', function() { 16 | var conf = {}; 17 | new Mod_cache(conf); 18 | assert.ok(conf); 19 | assert.equal(conf.ttl, 300); 20 | assert.equal(conf.cachesize, 1000); 21 | }); 22 | 23 | it('should return a Cache Object with defaults without config', function() { 24 | var mod = new Mod_cache(); 25 | assert.equal(mod.ttl, 300 * 1000); 26 | assert.equal(mod.max, 1000); 27 | }); 28 | 29 | it('should cache entries for lru', function(done) { 30 | var CacheObject = new Mod_cache({"ttl" : 300, "cachesize" : 5}), 31 | array = Array.apply(null, Array(5)).map(function(v, k) { return k; }); 32 | 33 | async.each(array, function (name, callback) { 34 | CacheObject.set(name, name, function () { 35 | callback(null); 36 | }); 37 | }, function () { 38 | CacheObject.get(0, function (err0) { 39 | CacheObject.get(1, function (err1, data1) { 40 | CacheObject.get('unknownkey', function (err2, data2) { 41 | assert.equal(true, data1 !== undefined && data2 === undefined); 42 | assert.equal(null, err0); 43 | assert.equal(null, err1); 44 | assert.equal(null, err2); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | }); 51 | 52 | it('should update multiple keys', function(done) { 53 | var CacheObject = new Mod_cache({ 54 | ttl: 300, 55 | cachesize: 5 56 | }); 57 | 58 | CacheObject.set(1, 1, function () { 59 | CacheObject.set(2, 2, function () { 60 | CacheObject.set(3, 30, function () { 61 | CacheObject.set(3, 31, function () { 62 | CacheObject.set(2, 4, function () { 63 | CacheObject.set(2, 5, function () { 64 | CacheObject.set(1, 6, function () { 65 | assert.equal(CacheObject.count, 3); 66 | assert.equal(CacheObject.tail.key, 3); 67 | assert.equal(CacheObject.tail.val, 31); 68 | assert.equal(CacheObject.head.key, 1); 69 | assert.equal(CacheObject.head.val, 6); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | }); 75 | }); 76 | }); 77 | }); 78 | }); 79 | 80 | it('should get a key', function(done) { 81 | var CacheObject = new Mod_cache({ 82 | ttl: 300, 83 | cachesize: 5 84 | }); 85 | CacheObject.set(1, 1, function () { 86 | CacheObject.set(1, 2, function () { 87 | CacheObject.get(1, function (err, rec) { 88 | assert.equal(rec, 2); 89 | done(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | it('should allow to see hit stats', function(done) { 96 | var CacheObject = new Mod_cache({ 97 | ttl: 300, 98 | cachesize: 5 99 | }); 100 | 101 | CacheObject.set(1, 1, function () { 102 | CacheObject.get(1, function () { 103 | CacheObject.get(1, function () { 104 | assert.equal(CacheObject.data['1'].hit, 2); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | }); 110 | 111 | it('should expire a single item', function(done) { 112 | var CacheObject = new Mod_cache({ 113 | ttl: 1, 114 | cachesize: 5 115 | }), noop = function() {}; 116 | CacheObject.set(1, 1); 117 | CacheObject.get(1, noop); 118 | CacheObject.get(1, noop); 119 | setTimeout(function(){ 120 | CacheObject.get(1, function () { 121 | assert.equal(undefined, CacheObject.data['1']); 122 | done(); 123 | }); 124 | }, 1200); 125 | }); 126 | 127 | it('should not expire a key', function(done) { 128 | var CacheObject = new Mod_cache({ 129 | ttl: 0, 130 | cachesize: 5 131 | }); 132 | CacheObject.set(2, 2); 133 | CacheObject.set(1, 1); 134 | setTimeout(function(){ 135 | CacheObject.get(1, function () { 136 | assert.ok(CacheObject.data['1']); 137 | assert.equal(CacheObject.data['1'].val, 1); 138 | done(); 139 | }); 140 | }, 1200); 141 | }); 142 | 143 | it('should expire head via ttl', function(done) { 144 | var CacheObject = new Mod_cache({ 145 | ttl: 1, 146 | cachesize: 5 147 | }); 148 | CacheObject.set(2, 2); 149 | CacheObject.set(1, 1); 150 | setTimeout(function(){ 151 | CacheObject.get(1, function () { 152 | assert.equal(undefined, CacheObject.data['1']); 153 | done(); 154 | }); 155 | }, 1200); 156 | }); 157 | 158 | it('should expire tail via ttl', function(done) { 159 | var CacheObject = new Mod_cache({ 160 | ttl: 1, 161 | cachesize: 5 162 | }); 163 | CacheObject.set(1, 1); 164 | CacheObject.set(2, 2); 165 | setTimeout(function(){ 166 | CacheObject.get(1, function () { 167 | assert.equal(undefined, CacheObject.data['1']); 168 | done(); 169 | }); 170 | }, 1200); 171 | }); 172 | 173 | it('should expire middle via ttl', function(done) { 174 | var CacheObject = new Mod_cache({ 175 | ttl: 1, 176 | cachesize: 5 177 | }); 178 | CacheObject.set(3, 3); 179 | CacheObject.set(1, 1); 180 | CacheObject.set(2, 2); 181 | setTimeout(function(){ 182 | CacheObject.get(1, function () { 183 | assert.equal(undefined, CacheObject.data['1']); 184 | done(); 185 | }); 186 | }, 1200); 187 | }); 188 | 189 | it('should throw if cache.get does not get a callback', function() { 190 | var CacheObject = new Mod_cache({ 191 | ttl: 1, 192 | cachesize: 5 193 | }); 194 | 195 | assert.throws(function() { 196 | CacheObject.get(1); 197 | }, /callback is required/); 198 | }); 199 | 200 | it('should remove items from cache when cache limit is hit', function(done) { 201 | var CacheObject = new Mod_cache({ 202 | ttl: 1, 203 | cachesize: 2 204 | }); 205 | 206 | CacheObject.set(1, 1, function() { 207 | assert.equal(CacheObject.count, 1); 208 | CacheObject.set(2, 1, function() { 209 | assert.equal(CacheObject.count, 2); 210 | CacheObject.set(3, 1, function() { 211 | assert.equal(CacheObject.count, 2); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | }); 217 | 218 | it('should not error when calling cache.get on an expired key twice in the same tick', function(done) { 219 | var CacheObject = new Mod_cache({ 220 | ttl: 1, 221 | cachesize: 5 222 | }); 223 | CacheObject.set(1, 1); 224 | setTimeout(function() { 225 | CacheObject.get(1, Function.prototype); 226 | CacheObject.get(1, function() { 227 | done(); 228 | }); 229 | }, 1200); 230 | }); 231 | 232 | }); 233 | -------------------------------------------------------------------------------- /test/testcoverage.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | 3 | var globalOptions = { 4 | enable: true, 5 | ttl: 300, 6 | cachesize: 1000 7 | }; 8 | var assert = require("assert"); 9 | var async = require("async"); 10 | var dns = require('../lib/index.js')(globalOptions); 11 | 12 | function prepare(options) { 13 | if (!options) { 14 | options = globalOptions; 15 | } 16 | //if created from other tests 17 | if (dns.internalCache) { 18 | delete dns.internalCache; 19 | } 20 | dns = require("../lib/index.js")(options); 21 | return dns; 22 | } 23 | 24 | describe('dnscache additional tests for full coverage', function() { 25 | this.timeout(10000); //dns queries are slow.. 26 | 27 | it("should convert family string to number", function(done) { 28 | prepare(); 29 | dns.lookup("127.0.0.1", "4", function() { 30 | dns.lookup("127.0.0.1", "6", function() { 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | it("coverage: options is undefined", function(done) { 37 | prepare(); 38 | dns.lookup("127.0.0.1", undefined, function() { 39 | done(); 40 | }); 41 | }); 42 | 43 | it("coverage: custom cache object", function(done) { 44 | var cacheClass = require("../lib/cache.js"); 45 | prepare({enable: true, cache: cacheClass}); 46 | done(); 47 | }); 48 | 49 | it("coverage: simultaneous requests", function(done) { 50 | prepare(); 51 | var methods = [ 52 | ["lookup", "127.0.0.1"], 53 | ["resolve", "localhost"], 54 | ["resolve4", "localhost"], 55 | ["resolve6", "localhost"], 56 | ["resolveMx", "yahoo.com"], 57 | ["resolveTxt", "yahoo.com"], 58 | ["resolveNs", "yahoo.com"], 59 | ["resolveCname", "www.yahoo.com"], 60 | ["reverse", "1.1.1.1"] 61 | ]; 62 | 63 | async.eachSeries(methods, function(method, itemDone) { 64 | async.times(2, function(i, callback) { 65 | dns[method[0]](method[1], function(err, result) { 66 | callback(null, result); 67 | }); 68 | }, function(err, results) { 69 | (assert.deepStrictEqual || assert.deepEqual)(results[0], results[1], "expected same result from cached query"); 70 | itemDone(null); 71 | }); 72 | }, function() { 73 | done(); 74 | }); 75 | }); 76 | }); 77 | --------------------------------------------------------------------------------