├── .gitignore ├── Dockerfile ├── README.md ├── api.js ├── dns.js ├── index.js ├── package.json ├── querymatcher.js ├── static.json ├── staticloop.js ├── stores └── mem.js ├── test ├── dns.js └── querymatcher.js ├── ttloop.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.swp 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4-slim 2 | RUN npm install -g rainbow-dns 3 | ENTRYPOINT ["rainbow-dns"] 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNS Server with http API 2 | 3 | :rainbow:-dns is a DNS server with an http API for populating it's records. Inspired by [skydns](https://github.com/skynetservices/skydns). 4 | 5 | ## Install 6 | 7 | npm install -g rainbow-dns 8 | 9 | ## Use 10 | 11 | rainbow-dns 12 | 13 | ## CLI Options 14 | 15 | --apihost // API host (default 127.0.0.1) 16 | --apiport // API port (default 8080) 17 | --dnshost // DNS host (default 127.0.0.1) 18 | --dnsport // DNS port (default 53) 19 | --ttl // Time To Live (default 300 -> seconds) 20 | --store // Records datastore (default mem -> memory) 21 | --domain // Domain (default random) 22 | --fwdhost // Forward host 23 | --fwdport // Forward port 24 | --static // Path to static records file 25 | --ipv4-for-ipv6 // Broken linux NODATA response handling crutch 26 | 27 | ### fwdhost 28 | 29 | By passing a ***\-\-fwdhost*** flag you can **forward** requests to another dns server if rainbow-dns don't have any matching records. 30 | 31 | rainbow-dns --fwdhost 8.8.8.8 32 | 33 | ### static 34 | 35 | By passing a ***\-\-static*** flag you can inject some **static records** from a **json** file. 36 | 37 | rainbow-dns --static ./static.json --domain dance.kiwi 38 | 39 | // Example static.json 40 | { 41 | "records" : [ 42 | { "name" : "break", "A" : [{"address" : "192.168.1.100"}] } 43 | { "name" : "popping", "CNAME" : [{"data" : "break.dance.kiwi"}] } 44 | ] 45 | } 46 | 47 | ### ipv4-for-ipv6 48 | 49 | Due to an [issue](https://github.com/asbjornenge/rainbow-dns/issues/5) with some recent linux distributions not properly handling (valid) [NODATA](https://www.ietf.org/rfc/rfc2308.txt) responses, you can set the **\-\-ipv4-for-ipv6** flag to include A records 50 | in response to AAAA requests and thereby working around this issue. 51 | 52 | **Symptom:** 53 | 54 | curl app.domain.com 55 | // unable to resolve hostname 56 | curl app.domain.com -4 57 | // 200 OK 58 | 59 | ## API 60 | 61 | GET / 62 | List all records 63 | PUT /{name} 64 | Add record name.domain 65 | DELETE /{name} 66 | Delete record name.domain 67 | 68 | // Valid json payload 69 | { 70 | "A" : [{"address" : "192.168.1.1"},{"address" : "192.168.10.1"}], 71 | "AAAA" : [{"address" : "2605:f8b0:4006:802:0:0:0:1010"}] 72 | } 73 | 74 | Rainbow-dns supports all **record types** listed **[here](https://github.com/tjfontaine/node-dns#resourcerecord)** provided that you include the **required properties**, with appropriate key and value, for the respective record type. Rainbow-dns will **not validate** your input and will only eject an error message upon requests if your record data is invalid. 75 | 76 | The payload for a **CNAME** record would look something like this: 77 | 78 | { 79 | "CNAME" : [{"data" : "elsewhere.domain.com"}] 80 | } 81 | 82 | **Defaults** (domain, ttl) can be included in the payload and thereby **overwritten** by PUTs. 83 | 84 | ## Example cURL 85 | 86 | curl -X PUT localhost:8080/database -d '{"A": [{"address" : "192.168.1.10"}], "ttl" : 999}' -H 'Content-Type: application/json' 87 | 88 | ## Example dig 89 | 90 | dig @localhost database.polychromatic.mo +short 91 | // 192.168.1.10 92 | dig @localhost polychromatic.mo 93 | // polychromatic.mo. 5 IN A 192.168.1.10 94 | dig @localhost "*.polychromatic.mo" 95 | // database.polychromatic.mo. 5 IN A 192.168.1.10 96 | 97 | ## Changelog 98 | 99 | ### 4.0.0 100 | 101 | * Fixed issue with case sensitive matching. `rainbow-dns`'s matching is now case insensitive :point_right: [rfc 4343](https://tools.ietf.org/html/rfc4343). Thanks to [@valentin2105](https://github.com/valentin2105) for catching this one! :rainbow: :tada: 102 | 103 | ### 3.0.1 104 | 105 | * Advertising recursion (setting ra header), fixes resolving on some platforms 106 | 107 | ### 3.0.0 108 | 109 | * Flexible record support (support any record supported by native-dns as long as you set the correct data) 110 | * Support for CNAME records :tada: 111 | * Renamed --ipv4-only -> --ipv4-for-ipv6 112 | 113 | ### 2.0.0 114 | 115 | * Removed default forward host - if no fwdhost is specified, empty results are returned 116 | * Added --ipv4-only crazy mode for Docker 117 | 118 | ### 1.2.1 119 | 120 | * New and improved query matcher 121 | * Groups now respond with the same name for all matches (!) 122 | 123 | ### 1.2.0 124 | 125 | * Added support for group responses 126 | 127 | ### 1.1.3 128 | 129 | * Fixed query match but no ipv4/ipv6 data bug 130 | 131 | ### 1.1.2 132 | 133 | * TTL sensitive interval for staticloop (with a minimum for 1s -> same as ttloop) 134 | 135 | ### 1.1.1 136 | 137 | * Support relative paths for --static 138 | 139 | ### 1.1.0 140 | 141 | * Added support for static records 142 | 143 | ### 1.0.0 144 | 145 | * Intial release :tada: 146 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var Hapi = require('hapi') 2 | var chalk = require('chalk') 3 | var rainbow = require('ansi-rainbow') 4 | var utils = require('./utils.js') 5 | 6 | var routes = [ 7 | { 8 | method: 'GET', 9 | path: '/', 10 | handler: function (request, reply) { 11 | request.store.list(function (err, all_records) { 12 | if (err) console.log('NEED ERROR HANDLING! GAAH!') 13 | reply(all_records) 14 | }) 15 | } 16 | }, 17 | { 18 | method: 'PUT', 19 | path: '/{name}', 20 | handler: function (request, reply) { 21 | var obj = utils.wrapDefaults(encodeURIComponent(request.params.name), request.payload, request.argv) 22 | request.store.set(obj.name, obj.payload, function (err, set_value) { 23 | if (err) console.log('NEED ERROR HANDLING! GAAH!') 24 | reply('OK.'); 25 | }) 26 | } 27 | }, 28 | { 29 | method: 'DELETE', 30 | path: '/{name}', 31 | handler: function (request, reply) { 32 | var obj = utils.wrapDefaults(encodeURIComponent(request.params.name), request.payload, request.argv) 33 | request.store.del(obj.name, function (err, set_value) { 34 | if (err) console.log('NEED ERROR HANDLING! GAAH!') 35 | reply(obj.name+' removed.'); 36 | }) 37 | } 38 | } 39 | ] 40 | 41 | module.exports = function (argv,store) { 42 | var server = new Hapi.Server(argv.apihost, argv.apiport) 43 | routes.forEach(function (route) { 44 | server.route(route) 45 | }) 46 | server.ext('onRequest', function (request, next) { 47 | request.store = store 48 | request.argv = argv 49 | next() 50 | }) 51 | server.realStart = server.start 52 | server.start = function () { 53 | this.realStart(function () { 54 | utils.displayServiceStatus('api',server.info.uri, true) 55 | }) 56 | } 57 | return server 58 | } 59 | -------------------------------------------------------------------------------- /dns.js: -------------------------------------------------------------------------------- 1 | var dns = require('native-dns') 2 | var consts = require('native-dns-packet').consts 3 | var utils = require('./utils') 4 | var queryMatcher = require('./querymatcher') 5 | 6 | var RainbowDns = function (argv, store) { 7 | this.argv = argv 8 | this.store = store 9 | this.server = dns.createServer() 10 | if (argv.fwdhost) this.fwdserver = { address: argv.fwdhost, port: argv.fwdport || 53, type: 'udp' } 11 | } 12 | RainbowDns.prototype.forward = function (request, response) { 13 | var req = dns.Request({ 14 | question : request, 15 | server : this.fwdserver, 16 | timeout : 1000 17 | }) 18 | req.on('message', function(err, answer) { 19 | response.answer = answer.answer 20 | response.authority = answer.authority 21 | response.additional = answer.additional 22 | response.edns_options = answer.edns_options 23 | response.header.ra = 1 24 | try { 25 | response.send() 26 | } catch(e) { 27 | req.cancel() 28 | console.log('ERROR: Error sending forward requrest: ',e) 29 | } 30 | }) 31 | req.on('timeout', function () { 32 | req.cancel() 33 | console.log('ERROR: Timeout in making forward request'); 34 | }); 35 | req.send() 36 | } 37 | RainbowDns.prototype.respond = function(request, response, results) { 38 | // TODO : Populate also Authority & Additional based on results .. ? 39 | // TODO : Should results be sorted? CNAME pre A ? 40 | 41 | results.forEach(function(resp) { 42 | response.answer.push(resp) 43 | }) 44 | 45 | // On some versions of glibc the resolver fails if response not advertised as recursive 46 | response.header.ra = 1 47 | 48 | // TODO : Being able to validate each record would be nice!! For imporved error logging 49 | try { 50 | response.send() 51 | } 52 | catch(e) { 53 | response.header.rcode = 2 // <- SERVERFAIL 54 | response.answer = [] 55 | response.send() 56 | console.log('ERROR: Unable to validate responses. \nSome mismatch between your store data and record requirements?\n',e) 57 | } 58 | } 59 | RainbowDns.prototype.handleRequest = function (request, response) { 60 | var _request = request.question[0] 61 | var query = _request.name 62 | var answer_types = this.filterTypes(this.pickAnswerTypes(_request.type)) 63 | this.queryStore(query, answer_types, function(results) { 64 | if (results.length == 0 && this.fwdserver) 65 | // FORWARD 66 | this.forward(_request, response) 67 | else 68 | // RESPOND 69 | this.respond(request, response, results) 70 | }.bind(this)) 71 | } 72 | RainbowDns.prototype.pickAnswerTypes = function(type) { 73 | return this.includeAnswerTypes(consts.QTYPE_TO_NAME[type]) 74 | } 75 | RainbowDns.prototype.includeAnswerTypes = function(queryType) { 76 | switch (queryType) { 77 | case 'A': 78 | return ['A','CNAME'] 79 | case 'AAAA': 80 | var types = ['AAAA','CNAME'] 81 | if (this.argv['ipv4-for-ipv6']) types.push('A') 82 | return types 83 | default: 84 | return [queryType] 85 | } 86 | } 87 | RainbowDns.prototype.filterTypes = function(responseTypes) { 88 | return responseTypes 89 | .filter(function(type) { 90 | return typeof dns[type] == 'function' 91 | }) 92 | } 93 | RainbowDns.prototype.queryStore = function(query, types, callback) { 94 | var results = [] 95 | this.store.list(function (err, records) { 96 | if (err) { console.log('ERROR: Unable to list data in store',err); process.exit(1) } 97 | types.forEach(function(recordtype) { 98 | queryMatcher(records, query, recordtype).forEach(function(record) { results.push(record) }) 99 | }) 100 | this.resolveCNAME(types, records, results) 101 | var _results = results.map(function(res) { return dns[res.type](res) }) 102 | if (typeof callback === 'function') callback(_results) 103 | }.bind(this)) 104 | } 105 | RainbowDns.prototype.resolveCNAME = function(types, records, results) { 106 | if (types.indexOf('CNAME') < 0) return 107 | if (types.indexOf('A') < 0 && types.indexOf('AAAA') < 0) return 108 | results.forEach(function(res) { 109 | if (res.type != 'CNAME') return 110 | if (results.filter(function(r) { return r.name == res.data }).length > 0) return 111 | if (types.indexOf('A') >= 0) queryMatcher(records, res.data, 'A').forEach(function(record) { results.push(record) }) 112 | if (types.indexOf('AAAA') >= 0) queryMatcher(records, res.data, 'AAAA').forEach(function(record) { results.push(record) }) 113 | }) 114 | } 115 | RainbowDns.prototype.start = function () { 116 | this.server.on('request', this.handleRequest.bind(this)) 117 | this.server.on('listening', function () { 118 | utils.displayServiceStatus('dns', 'udp://'+this.argv.dnshost+':'+this.argv.dnsport, true) 119 | }.bind(this)) 120 | this.server.on('close', function () { 121 | utils.displayErrorMessage('DNS socket unexpectedly closed', null, { exit : true }) 122 | }) 123 | this.server.on('error', function (err) { 124 | utils.displayErrorMessage('Unknown DNS error', err, { exit : true }) 125 | }) 126 | this.server.on('socketError', function (err) { 127 | utils.displayErrorMessage('DNS socket error occurred', err, { exit : true, hint : 'Port might be in use or you might not have permissions to bind to port. Try sudo?' }) 128 | }) 129 | this.server.serve(this.argv.dnsport, this.argv.dnshost) 130 | } 131 | 132 | module.exports = function (argv, store) { 133 | return new RainbowDns(argv, store) 134 | } 135 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv.slice(2), { 3 | default : { 4 | apihost : '127.0.0.1', 5 | apiport : 8080, 6 | dnshost : '127.0.0.1', 7 | dnsport : 53, 8 | store : 'mem', 9 | domain : require('random-domain')(), 10 | ttl : 300, 11 | } 12 | }) 13 | var utils = require('./utils') 14 | var api = require('./api') 15 | var dns = require('./dns') 16 | var ttloop = require('./ttloop') 17 | var staticloop = require('./staticloop') 18 | 19 | utils.displayVersionMaybe(argv) 20 | utils.displayHelpMaybe(argv) 21 | var store = utils.selectStore(argv) 22 | 23 | store.ready(function () { 24 | dns(argv, store).start() 25 | api(argv, store).start() 26 | ttloop(store).start() 27 | if (argv.static) staticloop(argv, store).start() 28 | utils.displayStartMessage(argv) 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rainbow-dns", 3 | "version": "3.0.1", 4 | "description": "DNS server with http API", 5 | "bin": "./index.js", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "mocha -R nyan -w --check-leaks" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/asbjornenge/rainbow-dns.git" 13 | }, 14 | "keywords": [ 15 | "dns", 16 | "server", 17 | "api" 18 | ], 19 | "author": "Asbjorn Enge ", 20 | "license": "BSD", 21 | "bugs": { 22 | "url": "https://github.com/asbjornenge/rainbow-dns/issues" 23 | }, 24 | "homepage": "https://github.com/asbjornenge/rainbow-dns", 25 | "dependencies": { 26 | "ansi-rainbow": "0.0.8", 27 | "chalk": "^1.0.0", 28 | "hapi": "^6.8.1", 29 | "minimist": "^1.1.0", 30 | "native-dns": "^0.6.1", 31 | "native-dns-packet": "^0.1.1", 32 | "random-domain": "^1.0.2", 33 | "transducers.js": "^0.2.1" 34 | }, 35 | "devDependencies": { 36 | "mocha": "^3.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /querymatcher.js: -------------------------------------------------------------------------------- 1 | var T = require('transducers.js') 2 | 3 | var onlySimilar = function(item, index) { 4 | var record = item[0] 5 | return record.toLowerCase().indexOf(this.query.toLowerCase()) >= 0 6 | } 7 | var onlyRecordType = function(item, index) { 8 | return item[1][this.recordType] != undefined 9 | } 10 | var intoResponses = function(item, index) { 11 | var record = item[0] 12 | var data = item[1] 13 | var records = data[this.recordType].map(function(store_data) { 14 | return { 15 | name : record.toLowerCase(), 16 | type : this.recordType, 17 | store_data : store_data, 18 | ttl : data.ttl, 19 | } 20 | }.bind(this)) 21 | return [ record, records ] 22 | } 23 | var intoGroups = function(item, index) { 24 | var record_name = item[0] 25 | var records = item[1] 26 | if (!this.wildcard && record_name != this.query) records.forEach(function(record) { 27 | record.name = this.query.toLowerCase() 28 | }.bind(this)) 29 | return item 30 | } 31 | var transformer = { 32 | init : function() { 33 | return [] 34 | }, 35 | step : function(result, x) { 36 | x[1].forEach(function(record) { 37 | var store_data = typeof record.store_data == 'object' ? record.store_data : {} 38 | Object.keys(store_data).forEach(function(key) { 39 | record[key] = record.store_data[key] 40 | }) 41 | delete record.store_data 42 | result.push(record) 43 | }) 44 | return result 45 | }, 46 | result : function(result) { 47 | return result 48 | } 49 | } 50 | 51 | /** This is where the magic happens **/ 52 | var matcher = function(records, query, type, wildcard) { 53 | var filterAndMap = T.compose( 54 | T.filter(onlySimilar.bind({ query : query })), 55 | T.filter(onlyRecordType.bind({ recordType : type })), 56 | T.map(intoResponses.bind({ recordType : type })), 57 | T.map(intoGroups.bind({ query : query, wildcard : wildcard })) 58 | ) 59 | return T.transduce(records, filterAndMap, transformer) 60 | } 61 | 62 | module.exports = function(records, query, type) { 63 | var wildcard = false 64 | if (query[0] == '*') { query = query.split('*.')[1]; wildcard = true } 65 | if (!query) return [] 66 | return matcher(records, query, type, wildcard) 67 | } 68 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "records" : [ 3 | { 4 | "name" : "dns", 5 | "A" : [{ "address" : "192.168.10.101" }], 6 | "AAAA" : [{ "address" : "2605:f8b0:4006:802:0:0:0:1010"}] 7 | }, 8 | { 9 | "name" : "yolo", 10 | "CNAME" : [{ "data" : "dns.dance.kiwi"}] 11 | }, 12 | { 13 | "name" : "yoLo", 14 | "CNAME" : [{ "data" : "dns.upperCasedance.kiwi"}] 15 | }, 16 | { 17 | "name" : "no6", 18 | "A" : [{ "address" : "192.168.10.101" }] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /staticloop.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | 4 | var pathToStatic = function (static_param) { 5 | if (static_param[0] != '/') static_param = process.cwd()+'/'+static_param 6 | return path.resolve(static_param) 7 | } 8 | 9 | var StaticLoop = function(argv, store) { 10 | this.argv = argv 11 | this.store = store 12 | } 13 | StaticLoop.prototype.start = function () { 14 | this.populate() 15 | this.interval = setInterval(this.populate.bind(this), this.intervalMillis()) 16 | } 17 | StaticLoop.prototype.populate = function () { 18 | var records = require(pathToStatic(this.argv.static)).records 19 | records.forEach(function (record) { 20 | var obj = utils.wrapDefaults(record.name, record, this.argv) 21 | this.store.set(obj.name, obj.payload, function (err, set_value) { 22 | if (err) console.log('NEED ERROR HANDLING! GAAH!') 23 | }) 24 | }.bind(this)) 25 | } 26 | StaticLoop.prototype.intervalMillis = function () { 27 | var interval = (this.argv.ttl*1000)-(this.argv.ttl*100) 28 | return interval > 1000 ? interval : 1000 29 | } 30 | StaticLoop.prototype.stop = function () { 31 | clearInterval(this.interval) 32 | } 33 | module.exports = function (argv, store) { 34 | return new StaticLoop(argv, store) 35 | } 36 | -------------------------------------------------------------------------------- /stores/mem.js: -------------------------------------------------------------------------------- 1 | function MemStore(config) { 2 | this.data = {} 3 | } 4 | MemStore.prototype.ready = function(callback) { 5 | callback() 6 | } 7 | MemStore.prototype.get = function(key, callback) { 8 | if (typeof callback === 'function') callback(null, this.data[key]) 9 | } 10 | MemStore.prototype.set = function (key, value, callback) { 11 | this.data[key] = value 12 | if (typeof callback === 'function') callback(null, this.data[key]) 13 | } 14 | MemStore.prototype.del = function (key, callback) { 15 | delete this.data[key] 16 | if (typeof callback === 'function') callback(null) 17 | } 18 | MemStore.prototype.list = function (callback) { 19 | if (typeof callback === 'function') callback(null, this.data) 20 | } 21 | module.exports = function (config) { 22 | return new MemStore(config) 23 | } 24 | -------------------------------------------------------------------------------- /test/dns.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var dns = require('../dns') 3 | var memstore = require('../stores/mem') 4 | 5 | describe('Dns', function() { 6 | 7 | it('should return appropriate answer types for A records', function() { 8 | var d = dns({}, memstore()) 9 | var types = d.includeAnswerTypes('A') 10 | assert(types.indexOf('A') >= 0) 11 | assert(types.indexOf('CNAME') >= 0) 12 | }) 13 | 14 | it('should return appropriate answer types for AAAA records', function() { 15 | var d = dns({}, memstore()) 16 | var types = d.includeAnswerTypes('AAAA') 17 | assert(types.indexOf('AAAA') >= 0) 18 | assert(types.indexOf('CNAME') >= 0) 19 | }) 20 | 21 | it('should return same answer types non handled queries', function() { 22 | var d = dns({}, memstore()) 23 | var types = d.includeAnswerTypes('YEAH') 24 | assert(types.length == 1) 25 | assert(types.indexOf('YEAH') >= 0) 26 | }) 27 | 28 | it('can filter non-existing response types', function() { 29 | var d = dns({}, memstore()) 30 | var filteredAnswerTypes = d.filterTypes(['A','CNAME','YOLO']) 31 | assert(filteredAnswerTypes.length == 2) 32 | }) 33 | 34 | it('should return appropriate response object from queryStore', function(done) { 35 | var s = memstore() 36 | s.set('break.dance.kiwi', { 37 | 'A' : [{'address':'1.2.3.4'}] 38 | }) 39 | s.set('yolo.dance.kiwi', { 40 | 'CNAME' : [{'data':'break.dance.kiwi'}] 41 | }) 42 | var d = dns({}, s) 43 | d.queryStore('yolo.dance.kiwi', ['A', 'CNAME'], function(results) { 44 | assert(results.length == 2) // <- Enough to check this? 45 | assert(results[0].type == 5) 46 | assert(results[0].name == 'yolo.dance.kiwi') 47 | assert(results[0].data == 'break.dance.kiwi') 48 | assert(results[1].type == 1) 49 | assert(results[1].name == 'break.dance.kiwi') 50 | assert(results[1].address == '1.2.3.4') 51 | done() 52 | }) 53 | }) 54 | 55 | }) -------------------------------------------------------------------------------- /test/querymatcher.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var queryMatcher = require('../querymatcher') 3 | 4 | var records = { 5 | 'a.lol.test.domain.com' : { 6 | A : [{'address' : '1.1.1.1'},{'address':'2.2.2.2'}], 7 | AAAA : [{'address' : '2605:f8b0:4006:802:0:0:0:1010'}], 8 | CNAME : [{'data' : 'yolo'}], 9 | ttl : 30 10 | }, 11 | 'b.lol.test.domain.com' : { 12 | A : [{'address':'2.2.2.2'},{'address':'3.3.3.3'}], 13 | ttl : 30 14 | }, 15 | 'a.ehm.test.domain.com' : { 16 | A : [{'address':'4.4.4.4'}], 17 | ttl : 30 18 | }, 19 | 'broken.lol.test.domain.com' : { 20 | ttl : 30 21 | }, 22 | 'test2.another.com' : { 23 | A : [{'address':'5.5.5.5'},{'address':'6.6.6.6'}], 24 | ttl : 30 25 | }, 26 | 'test.upperCase.com' : { 27 | A : [{'address':'5.5.5.5'}], 28 | ttl : 30 29 | } 30 | } 31 | 32 | describe('Matcher', function() { 33 | 34 | // TODO: TEST speed of matching before (with current match function) and after 35 | 36 | it('Should match direct queries and return all IPs', function() { 37 | var results = queryMatcher(records, 'a.lol.test.domain.com', 'A') 38 | assert(results.length == 2) 39 | }) 40 | 41 | it('Should support group queries', function() { 42 | var results = queryMatcher(records, 'test.domain.com', 'A') 43 | assert(results.length > 1) 44 | }) 45 | 46 | it('Should mutate record names to match query for groups', function() { 47 | var results = queryMatcher(records, 'test.domain.com', 'A') 48 | assert(results[0].name == 'test.domain.com' ) 49 | }) 50 | 51 | it('Should support wildcard queries', function() { 52 | var results = queryMatcher(records, '*.lol.test.domain.com', 'A') 53 | assert(results.length == 4) 54 | }) 55 | 56 | it('Should not mutate record names for wildcard queries', function() { 57 | var results = queryMatcher(records, '*.lol.test.domain.com', 'A') 58 | assert(results[0].name.indexOf('lol.test.domain.com') > 0) 59 | }) 60 | 61 | it('Should support AAAA queries too', function() { 62 | var results = queryMatcher(records, 'test.domain.com', 'AAAA') 63 | assert(results.length == 1) 64 | }) 65 | 66 | it('Should support CNAME queries too', function() { 67 | var results = queryMatcher(records, 'test.domain.com', 'CNAME') 68 | assert(results.length == 1) 69 | }) 70 | 71 | it('Should not be case sensitive', function() { 72 | var queryUpResults = queryMatcher(records, 'Another.com', 'A') 73 | assert(queryUpResults.length > 0) 74 | var queryDownResults = queryMatcher(records, 'uppercase.com', 'A') 75 | assert(queryDownResults.length == 1) 76 | }) 77 | 78 | }) 79 | -------------------------------------------------------------------------------- /ttloop.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | 3 | var Ttloop = function(store) { 4 | this.store = store 5 | } 6 | Ttloop.prototype.start = function () { 7 | this.interval = setInterval(this.check.bind(this), 1000) 8 | } 9 | Ttloop.prototype.check = function () { 10 | var now = utils.getUnixTime() 11 | this.store.list(function (err, records) { 12 | if (err) { console.log(err); this.stop(); process.exit(1) } 13 | Object.keys(records).forEach(function (record) { 14 | var r = records[record] 15 | if (r.time+r.ttl < now) this.store.del(record) 16 | }.bind(this)) 17 | }.bind(this)) 18 | } 19 | Ttloop.prototype.stop = function () { 20 | clearInterval(this.interval) 21 | } 22 | module.exports = function (store) { 23 | return new Ttloop(store) 24 | } 25 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var rainbow = require('ansi-rainbow') 3 | 4 | var stores = { 5 | mem : require('./stores/mem') 6 | } 7 | 8 | module.exports = { 9 | 10 | displayVersionMaybe : function (argv) { 11 | if (!argv.v && !argv.version) return 12 | var pkg = require('./package.json') 13 | console.log(rainbow.r(pkg.name)+chalk.red(' ❤ ')+chalk.green('v'+pkg.version)) 14 | process.exit(0) 15 | }, 16 | 17 | displayHelpMaybe : function (argv) { 18 | if (!argv.h && !argv.help) return 19 | var pkg = require('./package.json') 20 | console.log(chalk.cyan('help coming soon!')+chalk.red(' ❤ ')) 21 | process.exit(0) 22 | }, 23 | 24 | selectStore : function (argv) { 25 | if (stores[argv.store] == undefined) { console.log(chalk.red('ERROR ')+'no such datastore '+argv.store); process.exit(1) } 26 | return stores[argv.store]() 27 | }, 28 | 29 | displayStartMessage : function (argv) { 30 | // console.log(rainbow.r('nameserver ')+chalk.bgBlue(argv.nameserver)) 31 | this.displayServiceStatus('domain', argv.domain) 32 | // this.displayServiceStatus('store', argv.store) 33 | // console.log(rainbow.r('ttl ')+chalk.bgBlue(argv.ttl)) 34 | }, 35 | 36 | displayServiceStatus : function (service, meta, check) { 37 | console.log(rainbow.r(this.fillSpaces(service,6))+' '+this.fillSpaces(meta,21)+' '+(check ? chalk.green('✔') : '')) 38 | }, 39 | 40 | displayErrorMessage : function (msg, err, props) { 41 | console.log(chalk.red('ERROR: ')+msg, err) 42 | if (props.hint) console.log(chalk.cyan('HINT: ')+props.hint) 43 | if (props.exit) process.exit(1) 44 | }, 45 | 46 | fillSpaces : function (word, len) { 47 | while(word.length < len) { 48 | word = word+' ' 49 | } 50 | return word 51 | }, 52 | 53 | wrapDefaults : function (name, payload, argv) { 54 | name = name+'.'+(payload.domain || argv.domain) 55 | if (!payload.domain) payload.domain = argv.domain 56 | if (!payload.ttl) payload.ttl = argv.ttl 57 | payload.time = this.getUnixTime() 58 | return { 59 | name : name, 60 | payload : payload 61 | } 62 | }, 63 | 64 | getUnixTime : function () { 65 | return Math.floor(new Date().getTime() / 1000) 66 | }, 67 | 68 | reverse_map : function(src) { 69 | var dst = {}, 70 | k; 71 | 72 | for (k in src) { 73 | if (src.hasOwnProperty(k)) { 74 | dst[src[k]] = k; 75 | } 76 | } 77 | return dst; 78 | } 79 | 80 | } 81 | --------------------------------------------------------------------------------