From cd6397cc45789149f30a7fd4917cfbc6b3f5c442 Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Thu, 7 Jan 2010 01:28:15 +0000 Subject: [PATCH] Add support for MX, TXT, and SRV records in DNS module. --- doc/api.txt | 36 +++++- lib/dns.js | 39 +++++++ src/node_dns.cc | 183 ++++++++++++++++++++++++++++++ test/mjsunit/disabled/test-dns.js | 29 ----- test/mjsunit/test-dns.js | 119 +++++++++++++++++++ 5 files changed, 373 insertions(+), 33 deletions(-) delete mode 100644 test/mjsunit/disabled/test-dns.js create mode 100644 test/mjsunit/test-dns.js diff --git a/doc/api.txt b/doc/api.txt index 1b576524b26..27cc1f14b6c 100644 --- a/doc/api.txt +++ b/doc/api.txt @@ -1470,22 +1470,50 @@ resolution.addErrback(function (code, msg) { }); ------------------------------------------------------------------------- ++dns.resolve(domain, rrtype = 'A')+:: -+dns.resolve4(domain)+:: - -Resolves a domain (e.g. +"google.com"+) into an array of IPv4 addresses (e.g. -+["74.125.79.104", "74.125.79.105", "74.125.79.106"]+). +Resolves a domain (e.g. +"google.com"+) into an array of the record types +specified by rrtype. Valid rrtypes are +A+ (IPV4 addresses), +AAAA+ (IPV6 +addresses), +MX+ (mail exchange records), +TXT+ (text records), +SRV+ +(SRV records), and +PTR+ (used for reverse IP lookups). This function returns a promise. - on success: returns +addresses, ttl, cname+. +ttl+ (time-to-live) is an integer specifying the number of seconds this result is valid for. +cname+ is the canonical name for the query. + The type of each item in +addresses+ is determined by the record type, and + described in the documentation for the corresponding lookup methods below. - on error: returns +code, msg+. +code+ is one of the error codes listed below and +msg+ is a string describing the error in English. ++dns.resolve4(domain)+:: + +The same as +dns.resolve()+, but only for IPv4 queries (+A+ records). ++addresses+ is an array of IPv4 addresses (e.g. +["74.125.79.104", +"74.125.79.105", "74.125.79.106"]+). + +dns.resolve6(domain)+:: The same as +dns.resolve4()+ except for IPv6 queries (an +AAAA+ query). ++dns.resolveMx(domain)+:: + +The same as +dns.resolve()+, but only for mail exchange queries (+MX+ records). ++addresses+ is an array of MX records, each with a priority and an exchange +attribute (e.g. +[{"priority": 10, "exchange": "mx.example.com"},...]+). + ++dns.resolveTxt(domain)+:: + +The same as +dns.resolve()+, but only for text queries (+TXT+ records). ++addresses+ is an array of the text records available for +domain+ (e.g., ++["v=spf1 ip4:0.0.0.0 ~all"]+). + ++dns.resolveSrv(domain)+:: + +The same as +dns.resolve()+, but only for service records (+SRV+ records). ++addresses+ is an array of the SRV records available for +domain+. Properties +of SRV records are priority, weight, port, and name (e.g., +[{"priority": 10, +{"weight": 5, "port": 21223, "name": "service.example.com"}, ...]+). + +dns.reverse(ip)+:: Reverse resolves an ip address to an array of domain names. diff --git a/lib/dns.js b/lib/dns.js index bfbfdf45c0f..7aedab377c2 100644 --- a/lib/dns.js +++ b/lib/dns.js @@ -10,6 +10,18 @@ function callback (promise) { } } +exports.resolve = function (domain, type) { + type = (type || 'a').toUpperCase(); + + var resolveFunc = resolveMap[type]; + + if (typeof(resolveFunc) == 'function') { + return resolveFunc(domain); + } else { + return undefined; + } +} + exports.resolve4 = function (domain) { var promise = new events.Promise(); process.dns.resolve4(domain, callback(promise)); @@ -22,6 +34,24 @@ exports.resolve6 = function (domain) { return promise; }; +exports.resolveMx = function (domain) { + var promise = new process.Promise(); + process.dns.resolveMx(domain, callback(promise)); + return promise; +}; + +exports.resolveTxt = function (domain) { + var promise = new process.Promise(); + process.dns.resolveTxt(domain, callback(promise)); + return promise; +}; + +exports.resolveSrv = function (domain) { + var promise = new process.Promise(); + process.dns.resolveSrv(domain, callback(promise)); + return promise; +} + exports.reverse = function (ip) { var promise = new events.Promise(); process.dns.reverse(ip, callback(promise)); @@ -47,3 +77,12 @@ exports.NOMEM = process.dns.NOMEM; // the query is malformed. exports.BADQUERY = process.dns.BADQUERY; + +resolveMap = { + 'A': exports.resolve4, + 'AAAA': exports.resolve6, + 'MX': exports.resolveMx, + 'TXT': exports.resolveTxt, + 'SRV': exports.resolveSrv, + 'PTR': exports.reverse, +}; diff --git a/src/node_dns.cc b/src/node_dns.cc index 1dadab268f5..8bf78f66845 100644 --- a/src/node_dns.cc +++ b/src/node_dns.cc @@ -20,6 +20,11 @@ static ev_io io_watcher; static ev_timer timer_watcher; static Persistent errno_symbol; +static Persistent exchange_symbol; +static Persistent priority_symbol; +static Persistent weight_symbol; +static Persistent port_symbol; +static Persistent name_symbol; static inline Persistent* cb_persist(const Local &v) { Persistent *fn = new Persistent(); @@ -174,6 +179,145 @@ static void AfterResolveA6(struct dns_ctx *ctx, cb_destroy(cb); } +static void AfterResolveMX(struct dns_ctx *ctx, + struct dns_rr_mx *result, + void *data) { + assert(ctx == &dns_defctx); + + HandleScope scope; + + Persistent *cb = cb_unwrap(data); + + if (result == NULL) { + ResolveError(cb); + cb_destroy(cb); + return; + } + + /* canonical name */ + Local cname = String::New(result->dnsmx_cname); + + /* Time-To-Live (TTL) value */ + Local ttl = Integer::New(result->dnsmx_ttl); + + Local exchanges = Array::New(result->dnsmx_nrr); + for (int i = 0; i < result->dnsmx_nrr; i++) { + HandleScope loop_scope; + + Local exchange = Object::New(); + + struct dns_mx *mx = &(result->dnsmx_mx[i]); + exchange->Set(exchange_symbol, String::New(mx->name)); + exchange->Set(priority_symbol, Integer::New(mx->priority)); + + exchanges->Set(Integer::New(i), exchange); + } + + Local argv[3] = { exchanges, ttl, cname }; + + TryCatch try_catch; + + (*cb)->Call(Context::GetCurrent()->Global(), 3, argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + cb_destroy(cb); +} + +static void AfterResolveTXT(struct dns_ctx *ctx, + struct dns_rr_txt *result, + void *data) { + assert(ctx == &dns_defctx); + + HandleScope scope; + + Persistent *cb = cb_unwrap(data); + + if (result == NULL) { + ResolveError(cb); + cb_destroy(cb); + return; + } + + /* canonical name */ + Local cname = String::New(result->dnstxt_cname); + + /* Time-To-Live (TTL) value */ + Local ttl = Integer::New(result->dnstxt_ttl); + + Local records = Array::New(result->dnstxt_nrr); + for (int i = 0; i < result->dnstxt_nrr; i++) { + HandleScope loop_scope; + + struct dns_txt *record = &(result->dnstxt_txt[i]); + const char *txt = (const char *)record->txt; + records->Set(Integer::New(i), String::New(txt)); + } + + Local argv[3] = { records, ttl, cname }; + + TryCatch try_catch; + + (*cb)->Call(Context::GetCurrent()->Global(), 3, argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + cb_destroy(cb); +} + +static void AfterResolveSRV(struct dns_ctx *ctx, + struct dns_rr_srv *result, + void *data) { + assert(ctx == &dns_defctx); + + HandleScope scope; + + Persistent *cb = cb_unwrap(data); + + if (result == NULL) { + ResolveError(cb); + cb_destroy(cb); + return; + } + + /* canonical name */ + Local cname = String::New(result->dnssrv_cname); + + /* Time-To-Live (TTL) value */ + Local ttl = Integer::New(result->dnssrv_ttl); + + Local records = Array::New(result->dnssrv_nrr); + for (int i = 0; i < result->dnssrv_nrr; i++) { + HandleScope loop_scope; + + Local record = Object::New(); + + struct dns_srv *srv = &(result->dnssrv_srv[i]); + record->Set(priority_symbol, Integer::New(srv->priority)); + record->Set(weight_symbol, Integer::New(srv->weight)); + record->Set(port_symbol, Integer::New(srv->port)); + record->Set(name_symbol, String::New(srv->name)); + + records->Set(Integer::New(i), record); + } + + Local argv[3] = { records, ttl, cname }; + + TryCatch try_catch; + + (*cb)->Call(Context::GetCurrent()->Global(), 3, argv); + + if (try_catch.HasCaught()) { + FatalException(try_catch); + } + + cb_destroy(cb); +} + static Handle ResolveA(int type, const Arguments& args) { HandleScope scope; @@ -194,6 +338,18 @@ static Handle ResolveA(int type, const Arguments& args) { query = dns_submit_a6(NULL, *name, 0, AfterResolveA6, cb_persist(args[1])); break; + case DNS_T_MX: + query = dns_submit_mx(NULL, *name, 0, AfterResolveMX, cb_persist(args[1])); + break; + + case DNS_T_TXT: + query = dns_submit_txt(NULL, *name, DNS_C_IN, 0, AfterResolveTXT, cb_persist(args[1])); + break; + + case DNS_T_SRV: + query = dns_submit_srv(NULL, *name, NULL, NULL, 0, AfterResolveSRV, cb_persist(args[1])); + break; + default: return ThrowException(Exception::Error(String::New("Unsupported type"))); } @@ -213,6 +369,18 @@ static Handle ResolveA6(const Arguments& args) { return ResolveA(DNS_T_AAAA, args); } +static Handle ResolveMX(const Arguments& args) { + return ResolveA(DNS_T_MX, args); +} + +static Handle ResolveTXT(const Arguments& args) { + return ResolveA(DNS_T_TXT, args); +} + +static Handle ResolveSRV(const Arguments& args) { + return ResolveA(DNS_T_SRV, args); +} + static void AfterReverse(struct dns_ctx *ctx, struct dns_rr_ptr *result, void *data) { @@ -312,6 +480,12 @@ void DNS::Initialize(Handle target) { errno_symbol = NODE_PSYMBOL("errno"); + exchange_symbol = NODE_PSYMBOL("exchange"); + priority_symbol = NODE_PSYMBOL("priority"); + weight_symbol = NODE_PSYMBOL("weight"); + port_symbol = NODE_PSYMBOL("port"); + name_symbol = NODE_PSYMBOL("name"); + target->Set(String::NewSymbol("TEMPFAIL"), Integer::New(DNS_E_TEMPFAIL)); target->Set(String::NewSymbol("PROTOCOL"), Integer::New(DNS_E_PROTOCOL)); target->Set(String::NewSymbol("NXDOMAIN"), Integer::New(DNS_E_NXDOMAIN)); @@ -325,6 +499,15 @@ void DNS::Initialize(Handle target) { Local resolve6 = FunctionTemplate::New(ResolveA6); target->Set(String::NewSymbol("resolve6"), resolve6->GetFunction()); + Local resolveMx = FunctionTemplate::New(ResolveMX); + target->Set(String::NewSymbol("resolveMx"), resolveMx->GetFunction()); + + Local resolveTxt = FunctionTemplate::New(ResolveTXT); + target->Set(String::NewSymbol("resolveTxt"), resolveTxt->GetFunction()); + + Local resolveSrv = FunctionTemplate::New(ResolveSRV); + target->Set(String::NewSymbol("resolveSrv"), resolveSrv->GetFunction()); + Local reverse = FunctionTemplate::New(Reverse); target->Set(String::NewSymbol("reverse"), reverse->GetFunction()); } diff --git a/test/mjsunit/disabled/test-dns.js b/test/mjsunit/disabled/test-dns.js deleted file mode 100644 index 6226ba94f5a..00000000000 --- a/test/mjsunit/disabled/test-dns.js +++ /dev/null @@ -1,29 +0,0 @@ -process.mixin(require("../common")); -var dns = require("dns"); - -for (var i = 2; i < process.ARGV.length; i++) { - var name = process.ARGV[i] - puts("looking up " + name); - var resolution = dns.resolve4(name); - - resolution.addCallback(function (addresses, ttl, cname) { - puts("addresses: " + JSON.stringify(addresses)); - puts("ttl: " + JSON.stringify(ttl)); - puts("cname: " + JSON.stringify(cname)); - - for (var i = 0; i < addresses.length; i++) { - var a = addresses[i]; - var reversing = dns.reverse(a); - reversing.addCallback( function (domains, ttl, cname) { - puts("reverse for " + a + ": " + JSON.stringify(domains)); - }); - reversing.addErrback( function (code, msg) { - puts("reverse for " + a + " failed: " + msg); - }); - } - }); - - resolution.addErrback(function (code, msg) { - puts("error: " + msg); - }); -} diff --git a/test/mjsunit/test-dns.js b/test/mjsunit/test-dns.js new file mode 100644 index 00000000000..47fa6e144c3 --- /dev/null +++ b/test/mjsunit/test-dns.js @@ -0,0 +1,119 @@ +process.mixin(require("./common")); + +var dns = require("dns"), + sys = require("sys"); + +var hosts = ['example.com', 'example.org', + 'ietf.org', // AAAA + 'google.com', // MX, multiple A records + '_xmpp-client._tcp.google.com', // SRV + 'oakalynhall.co.uk']; // Multiple PTR replies + +var records = ['A', 'AAAA', 'MX', 'TXT', 'SRV']; + +var i = hosts.length; +while (i--) { + + var j = records.length; + while (j--) { + var hostCmd = "dig -t " + records[j] + " " + hosts[i] + + "| grep '^" + hosts[i] + "\\.\\W.*IN.*" + records[j] + "'" + + "| sed -E 's/[[:space:]]+/ /g' | cut -d ' ' -f 5- " + + "| sed -e 's/\\.$//'"; + + sys.exec(hostCmd).addCallback(checkDnsRecord(hosts[i], records[j])); + } +} + +function checkDnsRecord(host, record) { + var myHost = host, + myRecord = record; + return function(stdout) { + var expected = stdout.substr(0, stdout.length - 1).split("\n"); + + var resolution = dns.resolve(myHost, myRecord); + + switch (myRecord) { + case "A": + case "AAAA": + resolution.addCallback(function (result, ttl, cname) { + cmpResults(expected, result, ttl, cname); + + // do reverse lookup check + var ll = result.length; + while (ll--) { + var ip = result[ll]; + + var reverseCmd = "host " + ip + + "| cut -d \" \" -f 5-" + + "| sed -e 's/\\.$//'"; + + sys.exec(reverseCmd).addCallback(checkReverse(ip)); + } + }); + break; + case "MX": + resolution.addCallback(function (result, ttl, cname) { + var strResult = []; + var ll = result.length; + while (ll--) { + strResult.push(result[ll].priority + " " + result[ll].exchange); + } + + cmpResults(expected, strResult, ttl, cname); + }); + break; + case "TXT": + resolution.addCallback(function (result, ttl, cname) { + var strResult = []; + var ll = result.length; + while (ll--) { + strResult.push('"' + result[ll] + '"'); + } + cmpResults(expected, strResult, ttl, cname); + }); + break; + case "SRV": + resolution.addCallback(function (result, ttl, cname) { + var strResult = []; + var ll = result.length; + while (ll--) { + strResult.push(result[ll].priority + " " + + result[ll].weight + " " + + result[ll].port + " " + + result[ll].name); + } + cmpResults(expected, strResult, ttl, cname); + }); + break; + } + } +} + +function checkReverse(ip) { + var myIp = ip; + + return function (stdout) { + var expected = stdout.substr(0, stdout.length - 1).split("\n"); + + var reversing = dns.reverse(myIp); + + reversing.addCallback( + function (domains, ttl, cname) { + cmpResults(expected, domains, ttl, cname); + }); + } +} + +function cmpResults(expected, result, ttl, cname) { + assert.equal(expected.length, result.length); + + expected.sort(); + result.sort(); + + ll = expected.length; + while (ll--) { + assert.equal(result[ll], expected[ll]); +// puts("Result " + result[ll] + " was equal to expected " + expected[ll]); + } +}