├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── docs └── index.md ├── examples ├── app.js ├── cluster.js ├── noanswer.js └── reflect.js ├── lib ├── errors.js ├── index.js ├── protocol.js ├── query.js ├── records │ ├── a.js │ ├── aaaa.js │ ├── cname.js │ ├── mx.js │ ├── ns.js │ ├── soa.js │ ├── srv.js │ └── txt.js ├── server.js └── validators.js ├── package.json └── test ├── dig.js ├── dnsbuffer.js ├── helper.js ├── named.test.js ├── protocol.test.js ├── query.test.js ├── records.test.js └── validator.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swmp 3 | node_modules 4 | npm-debug.log 5 | tmp 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Trevor Orsztynowicz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODEUNIT := ./node_modules/.bin/nodeunit 2 | BUNYAN := ./node_modules/.bin/bunyan 3 | NPM := $(shell which npm) 4 | 5 | .PHONY: setup 6 | setup: $(NPM) 7 | $(NPM) install 8 | 9 | .PHONY: test 10 | test: $(NODEUNIT) 11 | $(NODEUNIT) test/*.test.js $(BUNYAN) 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-named - DNS Server in Node.js 2 | 3 | Node-named is a lightweight DNS server written in pure javascript. It has 4 | limited support for the DNS spec, but aims to implement all of the *common* 5 | functionality that is in use today. 6 | 7 | ** This project is not actively maintained ** 8 | I've received a lot of great PRs for this project, but I don't have the capacity to actively maintain this library at the moment. I feel strongly about maintaining backwards compatibility for people who rely on it, so any PRs would also need to adhere to keeping the API sane, or contribute to some improvement in performance. 9 | 10 | 11 | 12 | ## Creating a DNS Server 13 | ```javascript 14 | var named = require('./lib/index'); 15 | var server = named.createServer(); 16 | var ttl = 300; 17 | 18 | server.listen(9999, '127.0.0.1', function() { 19 | console.log('DNS server started on port 9999'); 20 | }); 21 | 22 | server.on('query', function(query) { 23 | var domain = query.name(); 24 | console.log('DNS Query: %s', domain) 25 | var target = new named.SOARecord(domain, {serial: 12345}); 26 | query.addAnswer(domain, target, ttl); 27 | server.send(query); 28 | }); 29 | ``` 30 | ## Creating DNS Records 31 | 32 | node-named provides helper functions for creating DNS records. 33 | The records are available under 'named.record.NAME' where NAME is one 34 | of ['A', 'AAAA', 'CNAME', 'SOA', 'MX', 'NS', 'TXT, 'SRV']. It is important to 35 | remember that these DNS records are not permanently added to the server. 36 | They only exist for the length of the particular request. After that, they are 37 | destroyed. This means you have to create your own lookup mechanism. 38 | ```javascript 39 | var named = require('node-named'); 40 | 41 | var soaRecord = new named.SOARecord('example.com', {serial: 201205150000}); 42 | console.log(soaRecord); 43 | ``` 44 | ### Supported Record Types 45 | 46 | The following record types are supported 47 | 48 | * A (ipv4) 49 | * AAAA (ipv6) 50 | * CNAME (aliases) 51 | * SOA (start of authority) 52 | * MX (mail server records) 53 | * NS (nameserver entries) 54 | * TXT (arbitrary text entries) 55 | * SRV (service discovery) 56 | 57 | ## Logging 58 | 59 | node-named uses [http://github.com/trentm/node-bunyan](bunyan) for logging. 60 | It's a lot nicer to use if you npm install bunyan and put the bunyan tool in 61 | your path. Otherwise, you will end up with JSON formatted log output by default. 62 | 63 | ### Replacing the default logger 64 | 65 | You can pass in an alternate logger if you wish. If you do not, then it will use 66 | bunyan by default. Your logger must expose the functions 'info', 'debug', 67 | 'warn', 'trace', 'error', and 'notice'. 68 | 69 | ### TODO 70 | 71 | * Better record validation 72 | * Create DNS client for query recursor 73 | * Add support for PTR records 74 | * Add support for TCP AXFR requests 75 | 76 | ## Tell me even more... 77 | 78 | When DNS was designed it was designed prior 79 | to the web existing, so many of the features in the RFC are either never used, 80 | or were never implemented. This server aims to be RFC compliant, but does not 81 | implement any other protocol other than INET (the one we're all used to), and 82 | only supports a handful of record types (the ones that are in use on a regular 83 | basis). 84 | 85 | ## Looking up Records 86 | 87 | There are a few handy ways to lookup DNS records in node. 88 | https://github.com/LCMApps/dns-lookup-cache 89 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Add better message compression using 'reference pointers' instead of 4 | additional nsName entries. 5 | * Add / build Query recursor (most record types possible using c-ares) 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API | node-named 3 | --- 4 | 5 | # node-named 6 | 7 | node-named is a lightweight DNS server written in javascript. It implements 8 | commonly used functionality from a variety of DNS RFC specifications. Unlike 9 | many other DNS servers node-named does not attempt to manage DNS records for 10 | you. You simply get a request, build your response based on whatever criteria 11 | you desire, and then send that response back to the client. 12 | 13 | ## Seriously? 14 | 15 | Actually this is quite useful. Both BIND and PowerDNS assume that records never 16 | change, and are not designed to be manipulated using an API or have elegant 17 | pluggable storage mechanisms. This DNS server is good for creating services 18 | where your records may change frequently, or you would like to access records 19 | stored in a central system using a mechanism of your choosing. 20 | 21 | 22 | # Installation 23 | 24 | $ npm install named 25 | 26 | # Server API 27 | 28 | var named = require('./lib/index'); 29 | var server = named.createServer(); 30 | 31 | server.listen(9999, '127.0.0.1', function() { 32 | console.log('DNS server started on port 9999'); 33 | }); 34 | 35 | server.on('query', function(query) { 36 | var domain = query.name(); 37 | var target = new named.SOARecord(domain, {serial: 12345}); 38 | // 300 is the ttl for this record 39 | query.addAnswer(domain, target, 300); 40 | server.send(query); 41 | }); 42 | 43 | Hit this DNS server with `dig` to see some results. Because we are only 44 | handling DNS responses for one record type (SOA or 'Start of Authority'), that 45 | is the response we will see, regardless of the type we make a request for. Dig 46 | is nice about this. 47 | 48 | $ dig @localhost -p9999 example.com SOA 49 | 50 | ; <<>> DiG 9.7.3-P3 <<>> @localhost -p9999 example.com SOA 51 | ; (3 servers found) 52 | ;; global options: +cmd 53 | ;; Got answer: 54 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32739 55 | ;; flags: qr rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 56 | ;; WARNING: recursion requested but not available 57 | 58 | ;; QUESTION SECTION: 59 | ;example.com. IN SOA 60 | 61 | ;; ANSWER SECTION: 62 | example.com. 5 IN SOA example.com. hostmaster.example.com. 12345 10 10 10 10 63 | 64 | ;; Query time: 10 msec 65 | ;; SERVER: ::1#9999(::1) 66 | ;; WHEN: Wed May 23 19:24:09 2012 67 | ;; MSG SIZE rcvd: 109 68 | 69 | 70 | ## Named API 71 | 72 | ### named.createServer([options]) 73 | 74 | Create a new named server. 75 | 76 | options is an object which may specify: 77 | 78 | - log: an optional bunyan logger 79 | - name: an optional name used to identify the server 80 | 81 | Here is an example a named server listening on port 53 82 | 83 | var named = require('named'); 84 | 85 | var server = named.createServer({ 86 | name: 'named0' 87 | }); 88 | 89 | server.listen(53); 90 | 91 | ## Class: named.Server 92 | 93 | ### server.listen(port, [host], [onListen]) 94 | 95 | Start accepting connections on the specified `port` and `host`. 96 | If the host is ommited then it will listen on all IPv4 and IPv6 interfaces. 97 | 98 | This function is asyncronous. Whenever the server is litening a `listen` event 99 | will be emitted. The last parameter `onListen` will be executed but not attached 100 | to the listen event, when the server is listening. 101 | 102 | ### server.send(queryResponse) 103 | 104 | Sends a `queryResponse` message. The queryResponse includes information about 105 | the client in the object itself. The `send` function will encode the message 106 | and send the response to the appropriate client. Unsolicited DNS messages are 107 | not permitted. This function should only be used within the `query` event. 108 | 109 | **Note** If you do not add any answers to your query, then the `send()` method 110 | will send a 'null-response' DNS message. This is the equivalent of an HTTP 404. 111 | 112 | ### server.close(onClose) 113 | 114 | Stops listening and closes the socket. `onClose` is an optional callback that 115 | will be attached to the underlying socket `close` event. 116 | 117 | ### Event: 'listening' 118 | 119 | `function() { }` 120 | 121 | Emitted once, when the server starts listening on the specified `port` and 122 | `host` 123 | 124 | ### Event: 'query' 125 | 126 | `function (query) { }` 127 | 128 | Emitted each time there is valid request. `query` is an instance of 129 | `named.Query` 130 | 131 | ### Event: 'clientError' 132 | 133 | `function (error) { }` 134 | 135 | Emitted when there is an invalid DNS request. This may be caused by a bad UDP 136 | datagram, or some other malformed DNS request. Parser errors are not included 137 | here. 138 | 139 | `error` is an instance of `named.DnsError` 140 | 141 | ### Event: 'uncaughtException' 142 | 143 | `function (error) { }` 144 | 145 | Emitted when there is an uncaught exception somewhere in the protocol stack. 146 | 147 | `error` is an instance of `named.DnsError` 148 | 149 | ### Event: 'after' 150 | 151 | `function (query, bytes)` 152 | 153 | Emitted after a `query` is sent to a client. This can be used for logging 154 | purposes. 155 | 156 | `query` is an instance of `named.Query` 157 | `bytes` is the number of bytes sent over the wire to the client 158 | 159 | ## Class: named.Query 160 | 161 | A query message is emitted by the `query` event. Query messages include all of 162 | the information about the query from the Client, including the client details. 163 | Because DNS queries are UDP based, the entire query itself is echoed back onto 164 | the wire, with the answer appended to its appropriate 'answer' fields. Several 165 | headers are changed, but the query is the same. 166 | 167 | For this reason, you 'reflect' the modified query back to the client. Prior to 168 | doing this you can check the 'Question' and 'Type' of question and perform an 169 | appropriate lookup & generate an appropriate response. 170 | 171 | ### query.name() 172 | 173 | Returns a string containing the query question name. This may be a hostname, but 174 | depends on the type 175 | 176 | ### query.type() 177 | 178 | Returns a string containing the type code for the query question. 179 | 180 | ### query.answers() 181 | 182 | Returns an array of answers that have been added to the query 183 | 184 | ### query.addAnswer(name, record, ttl) 185 | 186 | Add an instances of `named.Record` to the query. 187 | Name is the name you want to respond with (in 99.99% of cases, the 188 | query.name()), record is the record instance, and type is the type of record you 189 | are responding with. In most cases this will be what the query.type() returns, 190 | but for instances like an 'A' or 'AAAA' request you may elect to respond with a 191 | CNAME record. 192 | 193 | ### query.operation() 194 | 195 | Returns the type of operation that the client is requesting. In almost all cases 196 | this will be 'query'. Valid operations are 'query', 'status', 'notify', and 197 | 'update'. 198 | 199 | ### query.encode() 200 | 201 | Encodes the query and stores the results as a `buffer` in the query itself. 202 | This function should never need to be invoked, as the `server.send` function 203 | will automatically encode a query prior to being sent to the client. 204 | 205 | 206 | ## Records 207 | 208 | A DNS query is a question posed to a server about a record for a specific 209 | domain. The questions are for specific 'types' of records. Of the types listed 210 | in all of the DNS RFCs only some are still in use, and even fewer are 211 | frequently used. Each type of request has an appropriate response, each of 212 | which have different formats. These response formats are known as 213 | "Resource Records" or for the sake of named, just 'Records'. 214 | 215 | All records in named are created using the `new` keyword. 216 | 217 | ### named.SOARecord(domain, [options]) 218 | 219 | Create a DNS 'Start of Authority' record 220 | 221 | Options: 222 | 223 | - `admin`: The DNS name formatted email address of the administrator for this 224 | domain. Defaults to 'hostmaster.[domain]' 225 | - `serial`: The serial number of this domain. Defaults to 0 226 | - `refresh`: The refresh interval for this domain. Defaults to 10 227 | - `retry`: The retry interval for this domain. Defaults to 10 228 | - `expire`: The expire interval for this domain. Defaults to 10 229 | - `ttl`: The default time-to-live for records in this domain. Defaults to 10 230 | 231 | ### named.ARecord(ipv4Addr) 232 | 233 | Create an IPv4 resource record 234 | `ipv4Addr` must be a valid IPv4 address (string). 235 | 236 | ### named.AAAARecord(ipv6Addr) 237 | 238 | Create an IPv6 resource record. 239 | `ipv6Addr` must be a valid IPv6 address (string). 240 | 241 | ### named.CNAMERecord(target) 242 | 243 | Create an Alias record. When these records are sent to the client, the client 244 | will often make an additional request for the alias itself. 245 | 246 | ### named.MXRecord(exchange, options) 247 | 248 | Create a Mail Server record. A client making this request will often make an 249 | additional request for the entries in these records. 250 | `exchange` is the name of the mailserver that handles mail for this domain. 251 | 252 | Options: 253 | - `ttl`: The time-to-live for this particular mail server record 254 | - `priority`: The priority of this mailserver over other mailservers. You may 255 | have multiple mail servers. Lowest priority server is selected by client. 256 | 257 | ### named.SRVRecord(target, port, options) 258 | 259 | Create a Server Resource record. 260 | `target` is the name of the server that handles this particular resource name 261 | `port` is the tcp/udp port where the service may be reached 262 | 263 | Options: 264 | - `weight`: Used by the client for selecting between multiple results. Higher 265 | weight wins. Default is 10 266 | - `priority`: Used by the client for selecting between mutiple results. Higher 267 | priortiy wins. Default is 10 268 | 269 | ### named.TXTRecord(target) 270 | 271 | Create a text resource record. 272 | `target` can be any text up to 500 bytes in length 273 | 274 | ## Class: named.Record 275 | 276 | ### record.valid() 277 | 278 | This function will ensure that the data you used to create the record is in fact 279 | valid. 280 | 281 | ## DnsError 282 | 283 | DnsErrors rae objects that consist of: 284 | - `code`: A unique error number 285 | - `name`: the name of the error 286 | 287 | DnsErrors are: 288 | 289 | - `NoError` 290 | - `ProtocolError` 291 | - `CannotProcessError` 292 | - `NoNameError` 293 | - `NotImplementedError` 294 | - `RefusedError` 295 | - `ExceptionError` 296 | 297 | ## Class: named.DnsError 298 | 299 | ### error.message() 300 | 301 | Returns the message that was passed in to the error. The message is a string, 302 | and can be used for logging purposes 303 | 304 | ## Server Properties 305 | 306 | 307 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | var server = named.createServer(); 3 | 4 | server.listen(9999, '127.0.0.1', function() { 5 | console.log('DNS server started on port 9999'); 6 | }); 7 | 8 | console.log(named.SoaRecord); 9 | 10 | server.on('query', function(query) { 11 | var domain = query.name(); 12 | var record = new named.SOARecord(domain, {serial: 12345, ttl: 300}); 13 | query.addAnswer(domain, record, 300); 14 | server.send(query); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/cluster.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib/index'); 2 | var cluster = require('cluster'); 3 | 4 | /** 5 | * 6 | *
7 | * Example that uses cluster (http://nodejs.org/api/cluster.html) to 8 | * spin up multiple workers to handle requests. 9 | *
10 | * 11 | *
12 | * You can test it like this: dig @localhost -p 9999 goodtimes.com 13 | *
14 | * 15 | *
16 | * Or using dnsperf: 17 | *
18 | * 19 | *
20 |  * $ echo "goodtimes.com A" > /tmp/f
21 |  * $ dnsperf -s localhost -p 9999 -d /tmp/f -l 300
22 |  * 
23 | * 24 | *
25 | * Unfortunately the surprise is that more workers (4) run slower (3711 qps), 26 | * than a single worker (4084 qps). 27 | *
28 | * 29 | * @author Brian Hammond 30 | * 31 | */ 32 | var ClusterDns = function() { 33 | 34 | /* lame config */ 35 | this.PORT = 9999; 36 | this.LISTEN = '127.0.0.1'; 37 | this.SCALING = 1; 38 | 39 | this.master = function() { 40 | var numCPUs = require('os').cpus().length; 41 | var workers = numCPUs * this.SCALING; 42 | workers = 1; 43 | 44 | console.log( 'there are numCPUs:' + numCPUs + ', starting ' + workers + ' workers' ); 45 | 46 | for (var i = 0; i < workers ; i++) { 47 | cluster.fork(); 48 | } 49 | 50 | cluster.on('exit', function(worker, code, signal) { 51 | console.log('worker ' + worker.process.pid + ' died'); 52 | }); 53 | } 54 | 55 | this.randumb = function() { 56 | var r = function() { return Math.floor( Math.random() * 252 + 1 ) }; 57 | return r() + '.' + r() + '.' + r() + '.' + r(); 58 | }; 59 | 60 | this.friendo = function() { 61 | var thiz = this; 62 | var server = named.createServer(); 63 | 64 | var port = this.PORT; 65 | var listen = this.LISTEN; 66 | 67 | server.listen( port, listen, function() { 68 | console.log( 'DNS worker started on ' + listen + ':' + port + ', pid:' + cluster.worker.process.pid ); 69 | }); 70 | 71 | server.on('query', function(query) { 72 | var domain = query.name(); 73 | var ttl = 0; 74 | query.addAnswer( domain, new named.SOARecord(domain, {serial: 12345}, ttl ) ); 75 | query.addAnswer( domain, new named.ARecord( thiz.randumb(), ttl ) ); 76 | server.send(query); 77 | }); 78 | }; 79 | 80 | this.run = function() { 81 | if ( cluster.isMaster ) { 82 | this.master(); 83 | } else { 84 | this.friendo(); 85 | } 86 | }; 87 | }; 88 | 89 | new ClusterDns().run(); 90 | -------------------------------------------------------------------------------- /examples/noanswer.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | var server = named.createServer(); 3 | 4 | server.listen(9999, '127.0.0.1', function() { 5 | console.log('DNS server started on port 9999'); 6 | }); 7 | 8 | server.on('query', function(query) { 9 | var domain = query.name() 10 | var type = query.type(); 11 | console.log('DNS Query: (%s) %s', type, domain); 12 | // If we do not add any answers to the query then the 13 | // result will be a 'null-answer' message. This is how 14 | // you send a "404" to a DNS client 15 | server.send(query); 16 | }); 17 | 18 | server.on('clientError', function(error) { 19 | console.log("there was a clientError: %s", error); 20 | }); 21 | 22 | server.on('uncaughtException', function(error) { 23 | console.log("there was an excepton: %s", error.message()); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/reflect.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | var server = named.createServer(); 3 | 4 | server.listen(9999, '127.0.0.1', function() { 5 | console.log('DNS server started on port 9999'); 6 | }); 7 | 8 | server.on('query', function(query) { 9 | var domain = query.name() 10 | var type = query.type(); 11 | console.log('DNS Query: (%s) %s', type, domain); 12 | switch (type) { 13 | case 'A': 14 | var record = new named.ARecord('127.0.0.1'); 15 | query.addAnswer(domain, record, 300); 16 | break; 17 | case 'AAAA': 18 | var record = new named.AAAARecord('::1'); 19 | query.addAnswer(domain, record, 300); 20 | break; 21 | case 'CNAME': 22 | var record = new named.CNAMERecord('cname.example.com'); 23 | query.addAnswer(domain, record, 300); 24 | break; 25 | case 'MX': 26 | var record = new named.MXRecord('smtp.example.com'); 27 | query.addAnswer(domain, record, 300); 28 | break; 29 | case 'SOA': 30 | var record = new named.SOARecord('example.com'); 31 | query.addAnswer(domain, record, 300); 32 | break; 33 | case 'SRV': 34 | var record = new named.SRVRecord('sip.example.com', 5060); 35 | query.addAnswer(domain, record, 300); 36 | break; 37 | case 'TXT': 38 | var record = new named.TXTRecord('hello world'); 39 | query.addAnswer(domain, record, 300); 40 | break; 41 | } 42 | server.send(query); 43 | }); 44 | 45 | server.on('clientError', function(error) { 46 | console.log("there was a clientError: %s", error); 47 | }); 48 | 49 | server.on('uncaughtException', function(error) { 50 | console.log("there was an excepton: %s", error.message()); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This excellent error creator concept was borrowed from Mark Cavage 4 | https://github.com/mcavage/node-ldapjs/blob/master/lib/errors/index.js 5 | 6 | */ 7 | 8 | 9 | var util = require('util'); 10 | 11 | var CODES = { 12 | DNS_NO_ERROR: 0, 13 | DNS_PROTOCOL_ERROR: 1, 14 | DNS_CANNOT_PROCESS: 2, 15 | DNS_NO_NAME: 3, 16 | DNS_NOT_IMPLEMENTED: 4, 17 | DNS_REFUSED: 5, 18 | DNS_EXCEPTION: 6 19 | } 20 | 21 | var ERRORS = []; 22 | 23 | function DnsError(name, code, msg, caller) { 24 | if (Error.captureStackTrace) 25 | Error.captureStackTrace(this, caller || DnsError); 26 | 27 | this.code = code; 28 | this.name = name; 29 | 30 | this.message = function() { 31 | return msg || name; 32 | } 33 | } 34 | 35 | util.inherits(DnsError, Error); 36 | 37 | 38 | module.exports = {}; 39 | module.exports.DnsError = DnsError; 40 | 41 | Object.keys(CODES).forEach(function (code) { 42 | module.exports[code] = CODES[code]; 43 | 44 | if (CODES[code] === 0) 45 | return; 46 | 47 | var err = '', msg = ''; 48 | var pieces = code.split('_').slice(1); 49 | for (var i in pieces) { 50 | var lc = pieces[i].toLowerCase(); 51 | var key = lc.charAt(0).toUpperCase() + lc.slice(1); 52 | err += key; 53 | msg += key + (( i + 1 ) < pieces.length ? ' ' : ''); 54 | } 55 | 56 | if (!/\w+Error$/.test(err)) 57 | err += 'Error'; 58 | 59 | module.exports[err] = function(message, caller) { 60 | DnsError.call(this, 61 | err, 62 | CODES[code], 63 | message || msg, 64 | caller || module.exports[err]); 65 | 66 | }; 67 | module.exports[err].constructor = module.exports[err]; 68 | util.inherits(module.exports[err], DnsError); 69 | 70 | ERRORS[CODES[code]] = { 71 | err: err, 72 | message: msg 73 | } 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var bunyan = require('bunyan'); 5 | 6 | var Server = require('./server'); 7 | var Query = require('./query'); 8 | var Protocol = require('./protocol'); 9 | 10 | 11 | 12 | ////--- Globals 13 | 14 | var BUNYAN_SERIALIZERS = { 15 | err: bunyan.stdSerializers.err, 16 | query: function serializeQuery(q) { 17 | var out = { 18 | domain: q.name(), 19 | operation: q.operation(), 20 | type: q.type() 21 | }; 22 | return (out); 23 | } 24 | }; 25 | 26 | 27 | 28 | ///--- Exports 29 | module.exports = { 30 | 31 | createServer: function createServer(options) { 32 | options = options || {}; 33 | if (typeof (options) !== 'object') 34 | throw new TypeError('options (object) required'); 35 | 36 | 37 | var opts = { 38 | name: options.name || 'named', 39 | log: options.log || bunyan.createLogger({ 40 | name: 'named', 41 | level: 'warn', 42 | serializers: BUNYAN_SERIALIZERS 43 | }) 44 | }; 45 | return (new Server(opts)); 46 | }, 47 | 48 | Query: Query, 49 | 50 | Protocol: Protocol, 51 | 52 | bunyan: { serializers: BUNYAN_SERIALIZERS } 53 | 54 | }; 55 | 56 | // Export all the record types at the top-level 57 | var subdir = path.join(__dirname, 'records'); 58 | fs.readdirSync(subdir).forEach(function (f) { 59 | var name = path.basename(f); 60 | if (/\w+\.js/.test(name)) { 61 | var k = name.split('.').shift().toUpperCase() + 'Record'; 62 | module.exports[k] = require(path.join(subdir, f)); 63 | } 64 | }); 65 | // [ 66 | // 'A', 67 | // 'MX', 68 | // 'SOA', 69 | // 'SRV', 70 | // 'TXT', 71 | // 'AAAA', 72 | // 'CNAME' 73 | // ].forEach(function (r) { 74 | // var lcr = r.toLowerCase(); 75 | // var k = lcr.charAt(0).toUpperCase() + lcr.slice(1) + 'Record'; 76 | // module.exports[k] = require(path.join(__dirname, 'records/' + lcr))[r]; 77 | // }); 78 | -------------------------------------------------------------------------------- /lib/protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Protocol 4 | 5 | Stores protocol definitions and their primitives as well as any other 6 | associated protocol constants 7 | 8 | ## References 9 | 10 | http://tools.ietf.org/html/rfc1035 11 | http://tools.ietf.org/html/rfc4408 12 | http://tools.ietf.org/html/rfc2782 13 | http://tools.ietf.org/html/rfc3596 14 | 15 | ## Notes 16 | 17 | * Even though RFC1035 says that questions should support multiple queries, the 18 | reality is *nobody* does this. MS DNS doesn't support it and apparently BIND 19 | doesn't support it as well. That implies no client side tools do either - so 20 | we will not worry about that complication. 21 | 22 | * DNS Extensions have been proposed, but another case of chicken-and-egg. 23 | These extensions make it _possible_ to have DNS queries over 512 bytes in 24 | length, but because it is not universally supported, nobody does it. 25 | 26 | */ 27 | 28 | 29 | var ipaddr = require('ipaddr.js'); 30 | 31 | QCLASS_IN = 0x01, // the internet 32 | QCLASS_CS = 0x02, // obsolete 33 | QCLASS_CH = 0x03, // chaos class. yes this actually exists 34 | QCLASS_HS = 0x04, // Hesiod 35 | DNS_ENOERR = 0x00, // No error 36 | DNS_EFORMAT = 0x01, // Formatting Error 37 | DNS_ESERVER = 0x02, // server it unable to process 38 | DNS_ENONAME = 0x03, // name does not exist 39 | DNS_ENOTIMP = 0x04, // feature not implemented on this server 40 | DNS_EREFUSE = 0x05, // refused for policy reasons 41 | 42 | Formats = {}; 43 | 44 | Formats.answer = { 45 | name: { type: '_nsName' }, 46 | rtype: { type: 'UInt16BE' }, 47 | rclass: { type: 'UInt16BE' }, 48 | rttl: { type: 'UInt32BE' }, 49 | rdata: { type: '_nsData' }, // rdlength is prepended to this field 50 | }; 51 | 52 | Formats.question = { 53 | name: { type: '_nsName' }, 54 | type: { type: 'UInt16BE' }, 55 | qclass: { type: 'UInt16BE' } 56 | }; 57 | 58 | Formats.header = { 59 | id: { type: 'UInt16BE' }, 60 | flags: { type: '_nsFlags' }, 61 | qdCount: { type: 'UInt16BE' }, 62 | anCount: { type: 'UInt16BE' }, 63 | nsCount: { type: 'UInt16BE' }, 64 | srCount: { type: 'UInt16BE' }, 65 | }; 66 | 67 | Formats.soa = { 68 | host: { type: '_nsName' }, 69 | admin: { type: '_nsName' }, 70 | serial: { type: 'UInt32BE' }, 71 | refresh: { type: 'UInt32BE' }, 72 | retry: { type: 'UInt32BE' }, 73 | expire: { type: 'UInt32BE' }, 74 | ttl: { type: 'UInt32BE' } 75 | }; 76 | 77 | Formats.mx = { 78 | priority: { type: 'UInt16BE' }, 79 | exchange: { type: '_nsName' } 80 | }; 81 | 82 | Formats.txt = { 83 | text: { type: '_nsText' } 84 | }; 85 | 86 | Formats.srv = { 87 | priority: { type: 'UInt16BE' }, 88 | weight: { type: 'UInt16BE' }, 89 | port: { type: 'UInt16BE' }, 90 | target: { type: '_nsName' } 91 | }; 92 | 93 | Formats.queryMessage = { 94 | header: { type: { format: 'header' } }, 95 | question: { type: { format: 'question' } } 96 | }; 97 | 98 | Formats.answerMessage = { 99 | header: { type: { format: 'header' } }, 100 | question: { type: { format: 'question' } }, 101 | answers: { type: '_nsAnswers' } 102 | }; 103 | 104 | 105 | // turns a dotted-decimal address into a UInt32 106 | function parseIPv4(addr) { 107 | if (typeof(addr) !== 'string') 108 | throw new TypeError('addr (string) is required'); 109 | 110 | var octets = addr.split(/\./).map(function (octet) { 111 | return (parseInt(octet, 10)); 112 | }); 113 | if (octets.length !== 4) 114 | throw new TypeError('valid IP address required'); 115 | 116 | var uint32 = ((octets[0] * Math.pow(256, 3)) + 117 | (octets[1] * Math.pow(256, 2)) + 118 | (octets[2] * 256) + octets[3]); 119 | return (uint32); 120 | } 121 | 122 | 123 | function parseIPv6(addr) { 124 | if (typeof(addr) !== 'string') 125 | throw new TypeError('addr (string) is required'); 126 | 127 | var addr; 128 | try { 129 | addr = ipaddr.parse(addr); 130 | } catch (e) { 131 | return false; 132 | } 133 | 134 | return addr.parts; 135 | 136 | } 137 | 138 | 139 | // each of these serializers are functions which accept a value to serialize 140 | // and must returns the serialized value as a buffer 141 | var serializers = { 142 | 'UInt32BE': { 143 | encoder: function(v) { 144 | var b = new Buffer(4); 145 | b.writeUInt32BE(v, 0); 146 | return b; 147 | }, 148 | decoder: function(v, p) { 149 | return v.readUInt32BE(v, p); 150 | } 151 | }, 152 | 'UInt16BE': { 153 | encoder: function(v) { 154 | var b = new Buffer(2); 155 | b.writeUInt16BE(v, 0); 156 | return b; 157 | }, 158 | decoder: function(v, p) { 159 | var res = v.readUInt16BE(p); 160 | return { val: res, len: 2 }; 161 | } 162 | }, 163 | '_nsAnswers': { 164 | encoder: function(v) { 165 | var s = 0, p = 0, answers = []; 166 | for (i in v) { 167 | var r = encode(v[i], 'answer'); 168 | answers.push(r); 169 | s = s + r.length; 170 | } 171 | b = new Buffer(s); 172 | for (n in answers) { 173 | answers[n].copy(b, p); 174 | p = p + answers[n].length; 175 | } 176 | return b; 177 | } 178 | }, 179 | '_nsFlags': { 180 | encoder: function(v) { 181 | if (typeof(v) !== 'object') { 182 | throw new TypeError("flag must be an object"); 183 | } 184 | var b = new Buffer(2); 185 | var f = 0x0000; 186 | f = f | (v.qr << 15); 187 | f = f | (v.opcode << 11); 188 | f = f | (v.aa << 10); 189 | f = f | (v.tc << 9); 190 | f = f | (v.rd << 8); 191 | f = f | (v.ra << 7); 192 | f = f | (v.z << 6); 193 | f = f | (v.ad << 5); 194 | f = f | (v.cd << 4); 195 | f = f | v.rcode; 196 | b.writeUInt16BE(f, 0); 197 | return b; 198 | }, 199 | decoder: function(v, p) { 200 | var flags, f; 201 | flags = v.readUInt16BE(p); 202 | f = { 203 | qr: (( flags & 0x8000 )) ? true : false, 204 | opcode: (( flags & 0x7800 )), 205 | aa: (( flags & 0x0400 )) ? true : false, 206 | tc: (( flags & 0x0200 )) ? true : false, 207 | rd: (( flags & 0x0100 )) ? true : false, 208 | ra: (( flags & 0x0080 )) ? true : false, 209 | z: (( flags & 0x0040 )) ? true : false, 210 | ad: (( flags & 0x0020 )) ? true : false, 211 | cd: (( flags & 0x0010 )) ? true : false, 212 | rcode: (( flags & 0x000F )) 213 | }; 214 | return { val: f, len: 2 }; 215 | } 216 | }, 217 | '_nsIP4': { 218 | encoder: function(v) { 219 | var a, b; 220 | a = parseIPv4(v); 221 | b = new Buffer(4); 222 | b.writeUInt32BE(a, 0); 223 | return b; 224 | } 225 | }, 226 | '_nsIP6': { 227 | encoder: function(v) { 228 | var a, b, i = 0; 229 | a = parseIPv6(v); 230 | b = new Buffer(16); 231 | for (var i=0; i<8; i++) { 232 | b.writeUInt16BE(a[i], i * 2); 233 | } 234 | return b; 235 | } 236 | }, 237 | '_nsName': { 238 | encoder: function(v) { 239 | if (typeof(v) !== 'string') 240 | throw new TypeError('name (string) is required') 241 | var n = v.split(/\./); 242 | 243 | var b = new Buffer(n.toString().length + 2); 244 | var o = 0; //offset 245 | 246 | for (var i = 0; i < n.length; i++) { 247 | var l = n[i].length; 248 | b[o] = l; 249 | b.write(n[i], ++o, l, 'utf8'); 250 | o += l; 251 | } 252 | b[o] = 0x00; 253 | 254 | return b; 255 | }, 256 | decoder: function(v, p) { 257 | var rle, start = p, name = []; 258 | 259 | rlen = v.readUInt8(p); 260 | while (rlen != 0x00) { 261 | p++; 262 | var t = v.slice(p, p + rlen); 263 | name.push(t.toString()); 264 | p = p + rlen; 265 | rlen = v.readUInt8(p); 266 | } 267 | 268 | return { val: name.join('.'), len: (p - start + 1) }; 269 | } 270 | }, 271 | '_nsText': { 272 | encoder: function(v) { 273 | var b; 274 | b = new Buffer(v.length + 1); 275 | b.writeUInt8(v.length, 0); 276 | b.write(v, 1); 277 | return b; 278 | } 279 | }, 280 | '_nsData': { 281 | encoder: function(v, t) { 282 | var r, b, l; 283 | // TODO with the new queryTypes layout this could probably be mostly 284 | // eliminated 285 | 286 | switch(t) { 287 | case queryTypes['A']: 288 | r = serializers['_nsIP4'].encoder(v.target); 289 | break; 290 | case queryTypes['CNAME']: 291 | r = serializers['_nsName'].encoder(v.target); 292 | break; 293 | case queryTypes['NS']: 294 | r = serializers['_nsName'].encoder(v.target); 295 | break; 296 | case queryTypes['SOA']: 297 | r = encode(v, 'soa'); 298 | break; 299 | case queryTypes['MX']: 300 | r = encode(v, 'mx'); 301 | break; 302 | case queryTypes['TXT']: 303 | r = serializers['_nsText'].encoder(v.target); 304 | break; 305 | case queryTypes['AAAA']: 306 | r = serializers['_nsIP6'].encoder(v.target); 307 | break; 308 | case queryTypes['SRV']: 309 | r = encode(v, 'srv'); 310 | break; 311 | default: 312 | throw new Error('unrecognized nsdata type'); 313 | break; 314 | } 315 | 316 | l = r.length; 317 | b = new Buffer(l + 2); 318 | b.writeUInt16BE(l, 0); 319 | r.copy(b, 2); 320 | return b; 321 | } 322 | }, 323 | }; 324 | 325 | function encode(obj, format) { 326 | var size = 0, pos = 0, fmt, field, type, result, encoder, results = []; 327 | 328 | fmt = Formats[format]; 329 | 330 | for (f in fmt) { 331 | var type, decoder, res; 332 | type = fmt[f].type; 333 | 334 | if (typeof(type) === 'string') { 335 | //XXX I dont like this 336 | if (type == '_nsData') { 337 | res = serializers['_nsData'].encoder(obj[f], obj['rtype']); 338 | } 339 | else { 340 | res = serializers[type].encoder(obj[f]); 341 | } 342 | } 343 | else if (typeof(type) === 'object') { 344 | reftype = type.format; 345 | res = encode(obj[f], reftype); 346 | } 347 | else { 348 | throw new TypeError('invalid type'); 349 | } 350 | 351 | results.push(res); 352 | size = size + res.length; 353 | 354 | } 355 | 356 | result = new Buffer(size); 357 | 358 | for (i in results) { 359 | var buf = results[i]; 360 | buf.copy(result, pos); 361 | pos = pos + buf.length; 362 | } 363 | 364 | return result; 365 | } 366 | 367 | function decode(raw, format, pos) { 368 | var size = 0, fmt, field, type, decoder, result = {} 369 | 370 | if (!pos) pos = 0; 371 | fmt = Formats[format]; 372 | 373 | for (var f in fmt) { 374 | var type, decoder, res; 375 | type = fmt[f].type; 376 | 377 | // if the type is a string its a reference to a serializer 378 | // if the type is an object its a nested format and we call decode again 379 | // with the appropriate offset 380 | 381 | if (typeof(type) === 'string') { 382 | res = serializers[type].decoder(raw, pos); 383 | } 384 | else if (typeof(type) === 'object') { 385 | reftype = type.format; 386 | res = decode(raw, reftype, pos); 387 | } 388 | else { 389 | throw new TypeError('invalid type'); 390 | } 391 | 392 | pos += res.len; 393 | result[f] = res.val; 394 | 395 | } 396 | 397 | return {val: result, len: pos}; 398 | } 399 | 400 | 401 | var queryTypes = { 402 | A : 0x01, // ipv4 address 403 | NS : 0x02, // nameserver 404 | MD : 0x03, // obsolete 405 | MF : 0x04, // obsolete 406 | CNAME : 0x05, // alias 407 | SOA : 0x06, // start of authority 408 | MB : 0x07, // experimental 409 | MG : 0x08, // experimental 410 | MR : 0x09, // experimental 411 | NULL : 0x0A, // experimental null RR 412 | WKS : 0x0B, // service description 413 | PTR : 0x0C, // reverse entry (inaddr.arpa) 414 | HINFO : 0x0D, // host information 415 | MINFO : 0x0E, // mailbox or mail list information 416 | MX : 0x0F, // mail exchange 417 | TXT : 0x10, // text strings 418 | AAAA : 0x1C, // ipv6 address 419 | SRV : 0x21, // srv records 420 | AXFR : 0xFC, // request to transfer entire zone 421 | MAILA : 0xFE, // request for mailbox related records 422 | MAILB : 0xFD, // request for mail agent RRs 423 | ANY : 0xFF, // any class 424 | 0x01 : 'A' , // ipv4 address 425 | 0x02 : 'NS', // nameserver 426 | 0x03 : 'MD', // obsolete 427 | 0x04 : 'MF', // obsolete 428 | 0x05 : 'CNAME',// alias 429 | 0x06 : 'SOA', // start of authority 430 | 0x07 : 'MB', // experimental 431 | 0x08 : 'MG', // experimental 432 | 0x09 : 'MR', // experimental 433 | 0x0A : 'NULL', // experimental null RR 434 | 0x0B : 'WKS', // service description 435 | 0x0C : 'PTR', // reverse entry (inaddr.arpa) 436 | 0x0D : 'HINFO',// host information 437 | 0x0E : 'MINFO',// mailbox or mail list information 438 | 0x0F : 'MX', // mail exchange 439 | 0x10 : 'TXT', // text strings 440 | 0x1C : 'AAAA', // ipv6 address 441 | 0x21 : 'SRV', // srv records 442 | 0xFC : 'AXFR', // request to transfer entire zone 443 | 0xFE : 'MAILA',// request for mailbox related records 444 | 0xFD : 'MAILB',// request for mail agent RRs 445 | 0xFF : 'ANY', // any class 446 | } 447 | 448 | module.exports = { 449 | DNS_ENOERR : 0x00, // No error 450 | DNS_EFORMAT : 0x01, // Formatting Error 451 | DNS_ESERVER : 0x02, // server it unable to process 452 | DNS_ENONAME : 0x03, // name does not exist 453 | DNS_ENOTIMP : 0x04, // feature not implemented on this server 454 | DNS_EREFUSE : 0x05, // refused for policy reasons 455 | encode: encode, 456 | decode: decode, 457 | queryTypes: queryTypes 458 | } 459 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | var protocol = require('./protocol'); 2 | var ipaddr = require('ipaddr.js'); 3 | var protocol = require('./protocol'); 4 | var queryTypes = protocol.queryTypes 5 | 6 | 7 | function Query(arg) { 8 | if (typeof(arg) !== 'object') 9 | throw new TypeError('arg (object) is missing'); 10 | 11 | var self = this; 12 | 13 | this.id = arg.id; 14 | this._truncated = false; 15 | this._authoritative = false; // set on response 16 | this._recursionAvailable = false; // set on response 17 | this._responseCode = 0; 18 | this._qdCount = arg.qdCount; 19 | this._anCount = arg.anCount || 0; 20 | this._nsCount = arg.nsCount || 0; 21 | this._srCount = arg.srCount || 0; 22 | this._flags = arg.flags; 23 | this._question = arg.question; 24 | this._answers = []; 25 | 26 | this._raw = null; 27 | this._client = null; 28 | } 29 | 30 | 31 | Query.prototype.answers = function answers() { 32 | return this._answers.map(function(r) { 33 | return { 34 | name: r.name, 35 | type: queryTypes[r.rtype], 36 | record: r.rdata, 37 | ttl: r.rttl 38 | }; 39 | }); 40 | } 41 | 42 | Query.prototype.name = function name() { 43 | return this._question.name; 44 | } 45 | 46 | Query.prototype.type = function type() { 47 | return queryTypes[this._question.type]; 48 | } 49 | 50 | Query.prototype.operation = function operation() { 51 | switch (this._flags.opcode) { 52 | case 0: 53 | return 'query'; 54 | case 2: 55 | return 'status'; 56 | case 4: 57 | return 'notify'; 58 | case 5: 59 | return 'update'; 60 | default: 61 | throw new Error('invalid operation %d', this._flags.opcode); 62 | } 63 | }; 64 | 65 | Query.prototype.encode = function encode() { 66 | var header, question, answer, rSize, rBuffer; 67 | 68 | // TODO get rid of this intermediate format (or justify it) 69 | var toPack = { 70 | header: { 71 | id: this.id, 72 | flags: this._flags, 73 | qdCount: this._qdCount, 74 | anCount: this._anCount, 75 | nsCount: this._nsCount, 76 | srCount: this._srCount 77 | }, 78 | question: this._question, 79 | answers: this._answers, 80 | authority: this._authority, 81 | additional: this._additional 82 | } 83 | 84 | var encoded = protocol.encode(toPack, 'answerMessage'); 85 | 86 | this._raw = { 87 | buf: encoded, 88 | len: encoded.length 89 | }; 90 | }; 91 | 92 | Query.prototype.addAnswer = function(name, record, ttl) { 93 | if (typeof (name) !== 'string') 94 | throw new TypeError('name (string) required'); 95 | if (typeof (record) !== 'object') 96 | throw new TypeError('record (Record) required'); 97 | if (ttl !== undefined && typeof (ttl) !== 'number') 98 | throw new TypeError('ttl (number) required'); 99 | 100 | if (!queryTypes.hasOwnProperty(record._type)) 101 | throw new Error('unknown queryType: ' + record._type); 102 | 103 | var answer = { 104 | name: name, 105 | rtype: queryTypes[record._type], 106 | rclass: 1, // INET 107 | rttl: ttl || 5, 108 | rdata: record 109 | }; 110 | 111 | // Note: 112 | // 113 | // You can only have multiple answers in certain circumstances in no 114 | // circumstance can you mix different answer types other than 'A' with 115 | // 'AAAA' unless they are in the 'additional' section. 116 | // 117 | // There are also restrictions on what you can answer with depending on 118 | // the question. 119 | // 120 | // We will not attempt to enforce that here at the moment. 121 | // 122 | 123 | this._answers.push(answer); 124 | this._anCount++; 125 | }; 126 | 127 | function parseQuery(raw, src) { 128 | var dobj, b = raw.buf; 129 | 130 | dobj = protocol.decode(b, 'queryMessage'); 131 | 132 | if (!dobj.val) 133 | return null; 134 | 135 | // TODO get rid of this intermediate format (or justify it) 136 | var d = dobj.val; 137 | var res = { 138 | id: d.header.id, 139 | flags: d.header.flags, 140 | qdCount: d.header.qdCount, 141 | anCount: d.header.anCount, 142 | nsCount: d.header.nsCount, 143 | srCount: d.header.srCount, 144 | question: d.question, //XXX 145 | src: src, 146 | raw: raw 147 | } 148 | 149 | return (res); 150 | } 151 | 152 | function createQuery(req) { 153 | var query = new Query(req); 154 | query._raw = req.raw; 155 | query._client = req.src; 156 | return (query); 157 | } 158 | 159 | module.exports = { 160 | createQuery: createQuery, 161 | parse: parseQuery, 162 | } 163 | -------------------------------------------------------------------------------- /lib/records/a.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | function A(target) { 4 | if (typeof (target) !== 'string') 5 | throw new TypeError('IPv4Addr (string) is required'); 6 | 7 | this.target = target; 8 | this._type = 'A'; 9 | } 10 | module.exports = A; 11 | 12 | 13 | A.prototype.valid = function valid() { 14 | var self = this, model = {}; 15 | model = { 16 | target: validators.IPv4 17 | }; 18 | return validators.validate(self, model); 19 | } 20 | -------------------------------------------------------------------------------- /lib/records/aaaa.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | function AAAA(target) { 4 | if (typeof (target) !== 'string') 5 | throw new TypeError('IPv6Addr (string) is required'); 6 | 7 | this.target = target; 8 | this._type = 'AAAA'; 9 | } 10 | module.exports = AAAA; 11 | 12 | AAAA.prototype.valid = function valid() { 13 | var self = this, model = {}; 14 | model = { 15 | target: validators.IPv6 16 | }; 17 | return validators.validate(self, model); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/records/cname.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | 4 | 5 | function CNAME(target) { 6 | if (typeof(target) !== 'string') 7 | throw new TypeError('target (string) is required'); 8 | 9 | this.target = target; 10 | this._type = 'CNAME'; 11 | } 12 | module.exports = CNAME; 13 | 14 | 15 | CNAME.prototype.valid = function valid() { 16 | var self = this, model = {}; 17 | model = { 18 | target: validators.nsName 19 | }; 20 | return validators.validate(self, model); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/records/mx.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | 4 | function MX(exchange, opts) { 5 | if (typeof (exchange) !== 'string') 6 | throw new TypeError('exchange (string) is required'); 7 | 8 | if (!opts) 9 | opts = {}; 10 | 11 | var defaults = { 12 | priority: 0, 13 | ttl: 600, 14 | }; 15 | 16 | for (key in defaults) { 17 | if (key in opts) continue; 18 | opts[key] = defaults[key]; 19 | } 20 | 21 | this.exchange = exchange; 22 | this.ttl = opts.ttl; 23 | this.priority = opts.priority; 24 | this._type = 'MX'; 25 | } 26 | module.exports = MX; 27 | 28 | 29 | MX.prototype.valid = function valid() { 30 | var self = this, model = {}; 31 | model = { 32 | exchange: validators.nsName, 33 | ttl: validators.UInt32BE, 34 | priority: validators.UInt16BE 35 | }; 36 | return validators.validate(self, model); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/records/ns.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | 4 | 5 | function NS(target) { 6 | if (typeof(target) !== 'string') 7 | throw new TypeError('target (string) is required'); 8 | 9 | this.target = target; 10 | this._type = 'NS'; 11 | } 12 | module.exports = NS; 13 | 14 | 15 | NS.prototype.valid = function valid() { 16 | var self = this, model = {}; 17 | model = { 18 | target: validators.nsName 19 | }; 20 | return validators.validate(self, model); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/records/soa.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | function SOA(host, opts) { 4 | if (typeof(host) !== 'string') 5 | throw new TypeError('host (string) is required'); 6 | 7 | if (!opts) 8 | opts = {}; 9 | 10 | var defaults = { 11 | admin: 'hostmaster.' + host, 12 | serial: 0, 13 | refresh: 10, 14 | retry: 10, 15 | expire: 10, 16 | ttl: 10 17 | }; 18 | 19 | for (key in defaults) { 20 | if (key in opts) continue; 21 | opts[key] = defaults[key]; 22 | } 23 | 24 | this.host = host; 25 | this.admin = opts.admin; 26 | this.serial = opts.serial; 27 | this.refresh = opts.refresh; 28 | this.retry = opts.retry; 29 | this.expire = opts.expire; 30 | this.ttl = opts.ttl; 31 | this._type = 'SOA'; 32 | } 33 | module.exports = SOA; 34 | 35 | 36 | SOA.prototype.valid = function SOA() { 37 | var self = this, model = {}; 38 | 39 | model = { 40 | host: validators.nsName, 41 | admin: validators.nsName, 42 | serial: validators.UInt32BE, 43 | refresh: validators.UInt32BE, 44 | retry: validators.UInt32BE, 45 | expire: validators.UInt32BE, 46 | ttl: validators.UInt32BE 47 | }; 48 | 49 | return validators.validate(self, model); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/records/srv.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | function SRV(target, port, opts) { 4 | if (typeof(target) !== 'string') 5 | throw new TypeError('host (string) is required'); 6 | if (!port) 7 | throw new TypeError('port (integer) is required'); //XXX 8 | 9 | if (!opts) 10 | opts = {}; 11 | 12 | var defaults = { 13 | priority: 0, 14 | weight: 10, 15 | }; 16 | 17 | for (key in defaults) { 18 | if (key in opts) continue; 19 | opts[key] = defaults[key]; 20 | } 21 | 22 | this.target = target; 23 | this.port = port; 24 | this.weight = opts.weight; 25 | this.priority = opts.priority; 26 | this._type = 'SRV'; 27 | } 28 | module.exports = SRV; 29 | 30 | 31 | SRV.prototype.valid = function SRV() { 32 | var self = this, model = {}; 33 | model = { 34 | target: validators.nsText, // XXX 35 | port: validators.UInt16BE, 36 | weight: validators.UInt16BE, 37 | priority: validators.UInt16BE 38 | }; 39 | return validators.validate(self, model); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/records/txt.js: -------------------------------------------------------------------------------- 1 | var validators = require('../validators'); 2 | 3 | function TXT(target) { 4 | if (typeof(target) !== 'string') 5 | throw new TypeError('target (string) is required'); 6 | 7 | this.target = target; 8 | this._type = 'TXT'; 9 | } 10 | module.exports = TXT; 11 | 12 | 13 | TXT.prototype.valid = function TXT() { 14 | var self = this, model = {}; 15 | model = { 16 | target: validators.nsText 17 | }; 18 | return validators.validate(self, model); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var dgram = require('dgram'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var util = require('util'); 5 | 6 | var Query = require('./query'); 7 | var DnsError = require('./errors'); 8 | 9 | 10 | 11 | ///--- Globals 12 | 13 | var sprintf = util.format; 14 | 15 | var ExceptionError = DnsError.ExceptionError; 16 | var ProtocolError = DnsError.ProtocolError; 17 | 18 | 19 | 20 | ///--- API 21 | 22 | function Server(options) { 23 | if (typeof(options) !== 'object') 24 | throw new TypeError('options (object) is required'); 25 | 26 | this._log = options.log.child({component: 'agent'}, true); 27 | this._name = options.name || "named"; 28 | this._socket = null; 29 | 30 | } 31 | util.inherits(Server, EventEmitter); 32 | 33 | 34 | Server.prototype.close = function close(callback) { 35 | if (typeof (callback) === 'function') 36 | this._socket.once('close', callback); 37 | 38 | this._socket.close(); 39 | }; 40 | 41 | 42 | Server.prototype.listen = function listen(port, address, callback) { 43 | if (!port) 44 | throw new TypeError('port (number) is required'); 45 | 46 | if (typeof (address) === 'function' || !address) { 47 | callback = address; 48 | address = '0.0.0.0'; 49 | } 50 | 51 | var self = this; 52 | 53 | this._socket = dgram.createSocket('udp6'); 54 | this._socket.once('listening', function () { 55 | self.emit('listening'); 56 | if (typeof (callback) === 'function') 57 | process.nextTick(callback); 58 | }); 59 | this._socket.on('close', function onSocketClose() { 60 | self.emit('close'); 61 | }); 62 | this._socket.on('error', function onSocketError(err) { 63 | self.emit('error', err); 64 | }); 65 | this._socket.on('message', function (buffer, rinfo) { 66 | var decoded; 67 | var query; 68 | var raw = { 69 | buf: buffer, 70 | len: rinfo.size 71 | }; 72 | 73 | var src = { 74 | family: 'udp6', 75 | address: rinfo.address, 76 | port: rinfo.port 77 | }; 78 | 79 | try { 80 | decoded = Query.parse(raw, src); 81 | query = Query.createQuery(decoded); 82 | } catch (e) { 83 | self.emit('clientError', 84 | new ProtocolError('invalid DNS datagram')); 85 | } 86 | 87 | if (query === undefined || query === null) { 88 | return; 89 | } 90 | 91 | query.respond = function respond() { 92 | self.send(query); 93 | }; 94 | 95 | try { 96 | self.emit('query', query); 97 | } catch (e) { 98 | self._log.warn({ 99 | err: e 100 | }, 'query handler threw an uncaughtException'); 101 | self.emit('uncaughtException', e); 102 | } 103 | }); 104 | this._socket.bind(port, address); 105 | }; 106 | 107 | 108 | Server.prototype.send = function send(res) { 109 | assert.ok(res); 110 | 111 | try { 112 | res._flags.qr = 1; // replace with function 113 | res.encode(); 114 | } catch (e) { 115 | this._log.trace({err: e}, 'send: uncaughtException'); 116 | var err = new ExceptionError('unable to encode response'); 117 | this.emit('uncaughtException', err); 118 | return false; 119 | } 120 | 121 | var addr = res._client.address; 122 | var buf = res._raw.buf; 123 | var len = res._raw.len; 124 | var port = res._client.port; 125 | var self = this; 126 | 127 | this._log.trace({ 128 | adddress: addr, 129 | port: port, 130 | len: len 131 | }, 'send: writing DNS message to socket'); 132 | 133 | this._socket.send(buf, 0, len, port, addr, function (err, bytes) { 134 | if (err) { 135 | self._log.warn({ 136 | adddress: addr, 137 | port: port, 138 | err: err 139 | }, 'send: unable to send response'); 140 | self.emit('error', new ExceptionError(err.message)); 141 | } else { 142 | self._log.trace({ 143 | adddress: addr, 144 | port: port 145 | }, 'send: DNS response sent'); 146 | self.emit('after', res, bytes); 147 | } 148 | }); 149 | }; 150 | 151 | 152 | Server.prototype.toString = function toString() { 153 | var str = '[object named.Server <'; 154 | str += sprintf('name=%s, ', this._name); 155 | str += sprintf('socket=%j', this._socket ? this._socket.address() : {}); 156 | str += '>]'; 157 | return (str); 158 | }; 159 | 160 | 161 | 162 | ///--- Exports 163 | 164 | module.exports = Server; 165 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | var ipaddr = require('ipaddr.js'); 2 | var net = require('net'); 3 | 4 | module.exports = { 5 | nsName: function(v) { 6 | // hostname regex per RFC1123 7 | var reg =/^([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])(\.([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]))*$/i; 8 | if (typeof(v) !== 'string') 9 | return false; 10 | if (v.length > 255) 11 | return false; 12 | 13 | if (reg.test(v)) { 14 | return true; 15 | } 16 | else { 17 | return false; 18 | } 19 | }, 20 | UInt32BE: function(v) { 21 | if (typeof(v) === 'number') { 22 | var n = parseInt(v); 23 | if (n !== NaN && n < 4294967295) { 24 | return true; 25 | } 26 | else { 27 | return false; 28 | } 29 | } 30 | else { 31 | return false; 32 | } 33 | }, 34 | UInt16BE: function(v) { 35 | if (typeof(v) === 'number') { 36 | var n = parseInt(v); 37 | if (n !== NaN && n < 65535) { 38 | return true; 39 | } 40 | else { 41 | return false; 42 | } 43 | } 44 | else { 45 | return false; 46 | } 47 | }, 48 | nsText: function(v) { 49 | if (typeof(v) === 'string') { 50 | if (v.length < 256) 51 | return true; 52 | } 53 | else { 54 | return false; 55 | } 56 | }, 57 | IPv4: function(v) { 58 | return net.isIPv4(v); 59 | }, 60 | IPv6: function(v) { 61 | return net.isIPv6(v); 62 | }, 63 | validate: function(obj, model) { 64 | var result = true; 65 | for (v in model) { 66 | valid = model[v](obj[v]); 67 | if (!valid) { 68 | valid = false; 69 | break; 70 | } 71 | } 72 | return result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "named", 3 | "description": "DNS server library for node.js", 4 | "version": "0.0.1", 5 | "author": "trevoro ", 6 | "contributors": [ 7 | "Mark Cavage" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/trevoro/node-named.git" 12 | }, 13 | "main": "lib/index.js", 14 | "engines": { 15 | "node": ">=0.6" 16 | }, 17 | "dependencies": { 18 | "bunyan": "0.7.0", 19 | "ipaddr.js": "0.1.1" 20 | }, 21 | "devDependencies": { 22 | "nodeunit": "0.9.0" 23 | }, 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /test/dig.js: -------------------------------------------------------------------------------- 1 | // Quick and dirty 'dig' wrapper 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | var sprintf = require('util').format; 6 | 7 | 8 | 9 | ///--- Globals 10 | 11 | var DIG = 'dig'; 12 | 13 | 14 | 15 | ///--- Helpers 16 | 17 | function parseAnswer(tokens) { 18 | var t = tokens.filter(function (v) { 19 | return (v !== '' ? v : undefined); 20 | }); 21 | 22 | var r = { 23 | name: t[0], 24 | ttl: parseInt(t[1], 10), 25 | type: t[3], 26 | target: t[4] 27 | } 28 | 29 | return (r); 30 | } 31 | 32 | 33 | function parseDig(output) { 34 | var lines = output.split(/\n/); 35 | var section = 'header'; 36 | 37 | var results = { 38 | question: null, 39 | answers: [], 40 | additional: [], 41 | authority: [] 42 | }; 43 | 44 | lines.forEach(function (l) { 45 | if (l === '') { 46 | section = undefined; 47 | } else if (/^;; QUESTION SECTION:/.test(l)) { 48 | section = 'question'; 49 | } else if (/^;; ANSWER SECTION:/.test(l)) { 50 | section = 'answer'; 51 | } else if (/^;; ADDITIONAL SECTION:/.test(l)) { 52 | section = 'additional'; 53 | } else if (/^;; AUTHORITY SECTION:/.test(l)) { 54 | section = 'authority'; 55 | } 56 | 57 | if (section === 'question') { 58 | if (/^;([A-Za-z0-9])*\./.test(l)) { 59 | results.question = 60 | l.match(/([A-Za-z0-9_\-\.])+/)[0]; 61 | } 62 | } 63 | 64 | if (section === 'answer') { 65 | if (/^([_A-Za-z0-9])+/.test(l)) { 66 | var tokens = l.match(/(.*)/)[0].split(/\t/); 67 | var answer = parseAnswer(tokens); 68 | if (answer) 69 | results.answers.push(answer); 70 | } 71 | } 72 | }); 73 | 74 | return (results); 75 | } 76 | 77 | 78 | 79 | ///--- API 80 | 81 | function dig(name, type, options, callback) { 82 | if (typeof (name) !== 'string') 83 | throw new TypeError('name (string) is required'); 84 | if (typeof (type) !== 'string') 85 | throw new TypeError('type (string) is required'); 86 | if (typeof (options) === 'function') { 87 | callback = options; 88 | options = {}; 89 | } 90 | 91 | type = type.toUpperCase(); 92 | 93 | var opts = '' 94 | if (options.server) 95 | opts += ' @' + options.server; 96 | if (options.port) 97 | opts += ' -p ' + options.port; 98 | 99 | var cmd = sprintf('dig %s -t %s %s +time=1 +retry=0', opts, type, name); 100 | exec(cmd, function (err, stdout, stderr) { 101 | if (err) 102 | return (callback(err)); 103 | 104 | 105 | return (callback(null, parseDig(stdout))); 106 | }); 107 | } 108 | 109 | 110 | 111 | ///--- Exports 112 | 113 | module.exports = dig; 114 | -------------------------------------------------------------------------------- /test/dnsbuffer.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This file contains a bunch of sample DNS queries generated against namedjs 4 | using the 'dig' utility, as its the only tool that supports alternative ports 5 | when doing a query. 6 | 7 | When this module is loaded it will return an object containing an array of 8 | samples that you can use to test serializers, protocol generators, create a 9 | raw DNS client, etc. 10 | 11 | Each sample is an object with an 'id', 'description', 'raw', and 'data'. 12 | The ID is used so that adding and removing samples out of order will not affect 13 | external references to them in tests. 14 | 15 | The data is put through an encoder that will turn this string into a raw 16 | buffer. That way, samples may be loaded from file that can be read by a (mortal) 17 | human being. 18 | 19 | When the sample is encoded it places a "raw" value in the object. If you have one 20 | there it will be over-written. 21 | 22 | */ 23 | 24 | var samples = [ 25 | { 26 | id: 0, 27 | description: 'query ns1.joyent.dev (A)', 28 | data: "0f 34 01 00 00 01 00 00 00 00 00 00 03 6e 73 31 06 6a 6f 79 65 " + 29 | "6e 74 03 64 65 76 00 00 01 00 01", 30 | type: 'queryMessage' 31 | }, 32 | { 33 | id: 1, 34 | description: 'query ns1.joyent.dev (AAAA)', 35 | data: "b9 dd 01 00 00 01 00 00 00 00 00 00 03 6e 73 31 06 6a 6f 79 65 " + 36 | "6e 74 03 64 65 76 00 00 1c 00 01", 37 | type: 'queryMessage' 38 | } 39 | ]; 40 | 41 | function encode(data) { 42 | var tokens, buffer, pos = 0; 43 | 44 | if (typeof(data) !== 'string') 45 | throw new TypeError('data (string) is required'); 46 | 47 | tokens = data.split(/\s/); 48 | buffer = new Buffer(tokens.length); 49 | 50 | for (i in tokens) { 51 | var t = '0x' + tokens[i]; 52 | var v = parseInt(t); 53 | buffer.writeInt8(v, pos++, true); 54 | } 55 | return buffer; 56 | } 57 | 58 | function encodeSamples(samples) { 59 | var sample, results = []; 60 | for (i in samples) { 61 | sample = samples[i]; 62 | sample.raw = encode(sample.data); 63 | results.push(sample); 64 | } 65 | return results; 66 | } 67 | 68 | function equalBuffers(b1, b2) { 69 | if (b1.length !== b2.length) { 70 | return false; 71 | } 72 | 73 | var l = b1.length; 74 | while (l--) { 75 | var one = b1.readUInt8(l); 76 | var two = b2.readUInt8(l); 77 | if (one !== two) { 78 | return false; 79 | } 80 | } 81 | return true; 82 | } 83 | 84 | module.exports = { 85 | samples: encodeSamples(samples), 86 | equalBuffers: equalBuffers 87 | } 88 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Mark Cavage. All rights reserved. 2 | // 3 | // Just a simple wrapper over nodeunit's exports syntax. Also exposes 4 | // a common logger for all tests. 5 | // 6 | 7 | var bunyan = require('bunyan'); 8 | var named = require('../lib'); 9 | 10 | 11 | ///--- Exports 12 | 13 | module.exports = { 14 | 15 | after: function after(teardown) { 16 | module.parent.exports.tearDown = teardown; 17 | }, 18 | 19 | before: function before(setup) { 20 | module.parent.exports.setUp = setup; 21 | }, 22 | 23 | test: function test(name, tester) { 24 | module.parent.exports[name] = function _(t) { 25 | var _done = false; 26 | t.end = function end() { 27 | if (!_done) { 28 | _done = true; 29 | t.done(); 30 | } 31 | }; 32 | t.notOk = function notOk(ok, message) { 33 | return (t.ok(!ok, message)); 34 | }; 35 | return (tester(t)); 36 | }; 37 | }, 38 | 39 | 40 | getLog: function (name, stream) { 41 | return (bunyan.createLogger({ 42 | level: (process.env.LOG_LEVEL || 'info'), 43 | name: name || process.argv[1], 44 | serializers: named.bunyan.serializers, 45 | stream: stream || process.stdout, 46 | src: true 47 | })); 48 | }, 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /test/named.test.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | 3 | var dig = require('./dig'); 4 | 5 | if (require.cache[__dirname + '/helper.js']) 6 | delete require.cache[__dirname + '/helper.js'] 7 | var helper = require('./helper'); 8 | 9 | 10 | ///--- Globals 11 | 12 | var test = helper.test; 13 | var before = helper.before; 14 | var after = helper.after; 15 | 16 | var options = {port: 9999, server: '::1'}; 17 | 18 | 19 | 20 | ///--- Tests 21 | 22 | before(function (callback) { 23 | this.server = named.createServer({ 24 | log: helper.getLog('server') 25 | }); 26 | 27 | this.server.on('query', function (query) { 28 | var domain = query.name() 29 | var type = query.type(); 30 | 31 | switch (type) { 32 | case 'A': 33 | var record = new named.ARecord('127.0.0.1'); 34 | query.addAnswer(domain, record); 35 | break; 36 | case 'AAAA': 37 | var record = new named.AAAARecord('::1'); 38 | query.addAnswer(domain, record); 39 | break; 40 | case 'CNAME': 41 | var record = new named.CNAMERecord('cname.example.com'); 42 | query.addAnswer(domain, record); 43 | break; 44 | case 'NS': 45 | var record = new named.NSRecord('ns.example.com'); 46 | query.addAnswer(domain, record); 47 | break; 48 | case 'MX': 49 | var record = new named.MXRecord('smtp.example.com'); 50 | query.addAnswer(domain, record); 51 | break; 52 | case 'SOA': 53 | var record = new named.SOARecord('example.com'); 54 | query.addAnswer(domain, record); 55 | break; 56 | case 'SRV': 57 | var record = new named.SRVRecord('sip.example.com', 5060); 58 | query.addAnswer(domain, record); 59 | break; 60 | case 'TXT': 61 | var record = new named.TXTRecord('hello world'); 62 | query.addAnswer(domain, record); 63 | break; 64 | } 65 | query.respond(); 66 | }); 67 | 68 | this.server.listen(options.port, options.server, function() { 69 | process.nextTick(callback); 70 | }); 71 | }); 72 | 73 | 74 | after(function (callback) { 75 | this.server.close(callback); 76 | }); 77 | 78 | 79 | test('listen and close (port only)', function (t) { 80 | // don't conflict with the server made in 'before' 81 | var server = named.createServer(); 82 | server.listen(1153, function () { 83 | process.nextTick(function () { 84 | server.close(function () { 85 | t.end(); 86 | }) 87 | }); 88 | }); 89 | }); 90 | 91 | 92 | test('listen and close (port and ::1)', function(t) { 93 | var server = named.createServer(); 94 | server.listen(String(1153), '::1', function() { 95 | process.nextTick(function () { 96 | server.close(function () { 97 | t.end(); 98 | }) 99 | }); 100 | }); 101 | }); 102 | 103 | 104 | test('answer query: example.com (A)', function (t) { 105 | dig('example.com', 'A', options, function (err, results) { 106 | t.ifError(err); 107 | t.deepEqual(results.answers, [{ 108 | name: 'example.com.', 109 | ttl: 5, type: 'A', 110 | target: '127.0.0.1' 111 | }]); 112 | t.end(); 113 | }); 114 | }); 115 | 116 | 117 | test('answer query: example.com (AAAA)', function (t) { 118 | dig('example.com', 'AAAA', options, function (err, results) { 119 | t.ifError(err); 120 | t.deepEqual(results.answers, [{ 121 | name: 'example.com.', 122 | ttl: 5, type: 'AAAA', 123 | target: '::1' 124 | }]); 125 | t.end(); 126 | }); 127 | }); 128 | 129 | 130 | test('answer query: example.com (CNAME)', function (t) { 131 | dig('www.example.com', 'CNAME', options, function (err, results) { 132 | t.ifError(err); 133 | t.deepEqual(results.answers, [{ 134 | name: 'www.example.com.', 135 | ttl: 5, 136 | type: 'CNAME', 137 | target: 'cname.example.com.' 138 | }]); 139 | t.end(); 140 | }); 141 | }); 142 | 143 | test('answer query: example.com (NS)', function (t) { 144 | dig('example.com', 'NS', options, function (err, results) { 145 | t.ifError(err); 146 | t.deepEqual(results.answers, [{ 147 | name: 'example.com.', 148 | ttl: 5, 149 | type: 'NS', 150 | target: 'ns.example.com.' 151 | }]); 152 | t.end(); 153 | }); 154 | }); 155 | 156 | 157 | test('answer query: example.com (MX)', function (t) { 158 | dig('example.com', 'MX', options, function (err, results) { 159 | t.ifError(err); 160 | t.deepEqual(results.answers, [{ 161 | name: 'example.com.', 162 | ttl: 5, 163 | type: 'MX', 164 | target: '0 smtp.example.com.' 165 | }]); 166 | t.end(); 167 | }); 168 | }); 169 | 170 | 171 | test('answer query: example.com (SOA)', function (t) { 172 | dig('example.com', 'SOA', options, function (err, results) { 173 | t.ifError(err); 174 | t.deepEqual(results.answers, [{ 175 | name: 'example.com.', 176 | ttl: 5, 177 | type: 'SOA', 178 | target: 'example.com. hostmaster.example.com. 0 10 10 10 10' 179 | }]); 180 | t.end(); 181 | }); 182 | }); 183 | 184 | 185 | test('answer query: example.com (SRV)', function (t) { 186 | dig('_sip._tcp.example.com', 'SRV', options, function (err, results) { 187 | t.ifError(err); 188 | t.deepEqual(results.answers, [{ 189 | name: '_sip._tcp.example.com.', 190 | ttl: 5, 191 | type: 'SRV', 192 | target: '0 10 5060 sip.example.com.' 193 | }]); 194 | t.end(); 195 | }); 196 | }); 197 | 198 | 199 | test('answer query: example.com (TXT)', function (t) { 200 | dig('example.com', 'TXT', options, function (err, results) { 201 | t.ifError(err); 202 | t.deepEqual(results.answers, [{ 203 | name: 'example.com.', 204 | ttl: 5, 205 | type: 'TXT', 206 | target: '"hello world"' 207 | }]); 208 | t.end(); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /test/protocol.test.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | 3 | if (require.cache[__dirname + '/helper.js']) 4 | delete require.cache[__dirname + '/helper.js'] 5 | var helper = require('./helper'); 6 | 7 | 8 | var dnsBuffer = require('./dnsbuffer'); 9 | 10 | var test = helper.test; 11 | var protocol = named.Protocol; 12 | 13 | for (var i in dnsBuffer.samples) { 14 | var sample = dnsBuffer.samples[i]; 15 | test('protocol decode/encode: ' + sample.description, function(t) { 16 | decoded = protocol.decode(sample.raw, sample.type); 17 | encoded = protocol.encode(decoded.val, sample.type); 18 | if (dnsBuffer.equalBuffers(encoded, sample.raw)) { 19 | t.ok(true, 'encoder cycle passed'); 20 | } 21 | else { 22 | t.ok(false, 'encoder cycle failed'); 23 | } 24 | t.end(); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | var dnsBuffer = require('./dnsbuffer'); 3 | 4 | if (require.cache[__dirname + '/helper.js']) 5 | delete require.cache[__dirname + '/helper.js']; 6 | var helper = require('./helper'); 7 | 8 | var test = helper.test; 9 | var before = helper.before; 10 | var after = helper.after; 11 | 12 | var raw, src; 13 | 14 | before(function(callback) { 15 | try { 16 | raw = { 17 | buf: dnsBuffer.samples[0].raw, 18 | len: dnsBuffer.samples[0].length 19 | } 20 | src = { 21 | family: 'udp6', 22 | address: '127.0.0.1', 23 | port: 23456 24 | } 25 | 26 | process.nextTick(callback); 27 | } 28 | catch (e) { 29 | console.error(e.stack); 30 | process.exit(1); 31 | } 32 | }); 33 | 34 | 35 | test('decode a query datagram', function(t) { 36 | var query = named.Query.parse(raw, src); 37 | t.end(); 38 | }); 39 | 40 | test('create a new query object', function(t) { 41 | var decoded = named.Query.parse(raw, src); 42 | var query = named.Query.createQuery(decoded); 43 | t.end(); 44 | }); 45 | 46 | test('encode an null-response query object', function(t) { 47 | var decoded = named.Query.parse(raw, src); 48 | var query = named.Query.createQuery(decoded); 49 | query.encode(); 50 | var ok = dnsBuffer.samples[0].raw; 51 | t.deepEqual(query._raw.buf, ok); 52 | t.end(); 53 | }); 54 | 55 | // TODO test adding a record 56 | // TODO test name response 57 | // TODO test answers response 58 | -------------------------------------------------------------------------------- /test/records.test.js: -------------------------------------------------------------------------------- 1 | var named = require('../lib'); 2 | 3 | if (require.cache[__dirname + '/helper.js']) 4 | delete require.cache[__dirname + '/helper.js'] 5 | var helper = require('./helper'); 6 | 7 | var test = helper.test; 8 | 9 | var testRecord = function(record, t) { 10 | if (!record) 11 | t.ok(false, 'record could not be created'); 12 | 13 | if (record && record.valid()) { 14 | t.ok(true, 'valid record created'); 15 | } 16 | else { 17 | t.ok(false, 'record was not valid'); 18 | } 19 | 20 | t.end() 21 | } 22 | 23 | test('create a valid record (A)', function(t) { 24 | var record = new named.ARecord('127.0.0.1'); 25 | testRecord(record, t); 26 | }); 27 | 28 | test('create a valid record (AAAA)', function(t) { 29 | var record = new named.AAAARecord('::1'); 30 | testRecord(record, t); 31 | }); 32 | 33 | test('create a valid record (CNAME)', function(t) { 34 | var record = new named.CNAMERecord('alias.example.com'); 35 | testRecord(record, t); 36 | }); 37 | 38 | test('create a valid record (NS)', function(t) { 39 | var record = new named.NSRecord('ns.example.com'); 40 | testRecord(record, t); 41 | }); 42 | 43 | test('create a valid record (MX)', function(t) { 44 | var record = new named.MXRecord('smtp.example.com'); 45 | testRecord(record, t); 46 | }); 47 | 48 | test('create a valid record (SOA)', function(t) { 49 | var record = new named.SOARecord('example.com'); 50 | testRecord(record, t); 51 | }); 52 | 53 | test('create a valid record (SRV)', function(t) { 54 | var record = new named.SRVRecord('_sip._udp.example.com', 5060); 55 | testRecord(record, t); 56 | }); 57 | 58 | test('create a valid record (TXT)', function(t) { 59 | var record = new named.TXTRecord('hello world'); 60 | testRecord(record, t); 61 | }); 62 | -------------------------------------------------------------------------------- /test/validator.test.js: -------------------------------------------------------------------------------- 1 | var validators = require('../lib/validators'); 2 | 3 | if (require.cache[__dirname + '/helper.js']) 4 | delete require.cache[__dirname + '/helper.js'] 5 | var helper = require('./helper'); 6 | 7 | var test = helper.test; 8 | 9 | var toTest = { 10 | nsName: [ 11 | [ 'example.com', true ], 12 | [ '0example.com', true ], 13 | [ '_example.com', false ], 14 | [ '0_example.com', false ], 15 | [ '-example.com', false ], 16 | [ '0-example.com', true ], 17 | [ 'example-one.com', true ], 18 | [ 'example-111.com', true ], 19 | [ 'Example-111.com', true ], 20 | [ 'a name with spaces', false ], 21 | ], 22 | UInt32BE: [ 23 | [ 'hello', false ], 24 | [ '12345', true ], 25 | [ 4294967296, false ], 26 | [ 10, true ] 27 | ], 28 | UInt16BE: [ 29 | [ 'hello', false ], 30 | [ '12345', true ], 31 | [ 65536, false ], 32 | [ 10, true ] 33 | ], 34 | nsText: [ 35 | [ 'hello world', true ], 36 | ] 37 | }; 38 | 39 | test('testing validator (nsName)', function(t) { 40 | var k = 'nsName'; 41 | for (var i in toTest.k) { 42 | var s = toTest.k[i][0]; 43 | var ok = toTest.k[i][1]; 44 | var result = validators.k(s); 45 | t.equal(result, ok); 46 | } 47 | t.end(); 48 | }); 49 | 50 | test('testing validator (UInt32BE)', function(t) { 51 | var k = 'UInt32BE'; 52 | for (var i in toTest.k) { 53 | var s = toTest.k[i][0]; 54 | var ok = toTest.k[i][1]; 55 | var result = validators.k(s); 56 | t.equal(result, ok); 57 | } 58 | t.end(); 59 | }); 60 | 61 | test('testing validator (UInt16BE)', function(t) { 62 | var k = 'UInt16BE'; 63 | for (var i in toTest.k) { 64 | var s = toTest.k[i][0]; 65 | var ok = toTest.k[i][1]; 66 | var result = validators.k(s); 67 | t.equal(result, ok); 68 | } 69 | t.end(); 70 | }); 71 | 72 | test('testing validator (nsText)', function(t) { 73 | var k = 'nsText'; 74 | for (var i in toTest.k) { 75 | var s = toTest.k[i][0]; 76 | var ok = toTest.k[i][1]; 77 | var result = validators.k(s); 78 | t.equal(result, ok); 79 | } 80 | t.end(); 81 | }); 82 | --------------------------------------------------------------------------------