diff options
author | cjihrig <cjihrig@gmail.com> | 2018-06-11 21:56:33 +0300 |
---|---|---|
committer | cjihrig <cjihrig@gmail.com> | 2018-06-20 20:35:27 +0300 |
commit | 7486c4d71060e2ae3e754cf01e8fb02696eacd13 (patch) | |
tree | 8b48c47271d474800f1222f729e93f477f3fd395 /lib/internal/dns | |
parent | fea3595c2f92beb0d31feb93da1c972cc30e6fec (diff) |
dns: add promisified dns module
PR-URL: https://github.com/nodejs/node/pull/21264
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Diffstat (limited to 'lib/internal/dns')
-rw-r--r-- | lib/internal/dns/promises.js | 249 | ||||
-rw-r--r-- | lib/internal/dns/utils.js | 141 |
2 files changed, 390 insertions, 0 deletions
diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js new file mode 100644 index 00000000000..45e6c5ec64d --- /dev/null +++ b/lib/internal/dns/promises.js @@ -0,0 +1,249 @@ +'use strict'; +const { + bindDefaultResolver, + Resolver: CallbackResolver, + validateHints +} = require('internal/dns/utils'); +const { codes, dnsException } = require('internal/errors'); +const { isIP, isIPv4, isLegalPort } = require('internal/net'); +const { + getaddrinfo, + getnameinfo, + ChannelWrap, + GetAddrInfoReqWrap, + GetNameInfoReqWrap, + QueryReqWrap +} = process.binding('cares_wrap'); +const { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_OPT_VALUE, + ERR_MISSING_ARGS, + ERR_SOCKET_BAD_PORT +} = codes; + + +function onlookup(err, addresses) { + if (err) { + this.reject(dnsException(err, 'getaddrinfo', this.hostname)); + return; + } + + const family = this.family ? this.family : isIPv4(addresses[0]) ? 4 : 6; + this.resolve({ address: addresses[0], family }); +} + +function onlookupall(err, addresses) { + if (err) { + this.reject(dnsException(err, 'getaddrinfo', this.hostname)); + return; + } + + const family = this.family; + + for (var i = 0; i < addresses.length; i++) { + const address = addresses[i]; + + addresses[i] = { + address, + family: family ? family : isIPv4(addresses[i]) ? 4 : 6 + }; + } + + this.resolve(addresses); +} + +function createLookupPromise(family, hostname, all, hints, verbatim) { + return new Promise((resolve, reject) => { + if (!hostname) { + if (all) + resolve([]); + else + resolve({ address: null, family: family === 6 ? 6 : 4 }); + + return; + } + + const matchedFamily = isIP(hostname); + + if (matchedFamily !== 0) { + const result = { address: hostname, family: matchedFamily }; + if (all) + resolve([result]); + else + resolve(result); + + return; + } + + const req = new GetAddrInfoReqWrap(); + + req.family = family; + req.hostname = hostname; + req.oncomplete = all ? onlookupall : onlookup; + req.resolve = resolve; + req.reject = reject; + + const err = getaddrinfo(req, hostname, family, hints, verbatim); + + if (err) { + reject(dnsException(err, 'getaddrinfo', hostname)); + } + }); +} + +function lookup(hostname, options) { + var hints = 0; + var family = -1; + var all = false; + var verbatim = false; + + // Parse arguments + if (hostname && typeof hostname !== 'string') { + throw new ERR_INVALID_ARG_TYPE('hostname', ['string', 'falsy'], hostname); + } else if (options !== null && typeof options === 'object') { + hints = options.hints >>> 0; + family = options.family >>> 0; + all = options.all === true; + verbatim = options.verbatim === true; + + validateHints(hints); + } else { + family = options >>> 0; + } + + if (family !== 0 && family !== 4 && family !== 6) + throw new ERR_INVALID_OPT_VALUE('family', family); + + return createLookupPromise(family, hostname, all, hints, verbatim); +} + + +function onlookupservice(err, hostname, service) { + if (err) { + this.reject(dnsException(err, 'getnameinfo', this.host)); + return; + } + + this.resolve({ hostname, service }); +} + +function createLookupServicePromise(host, port) { + return new Promise((resolve, reject) => { + const req = new GetNameInfoReqWrap(); + + req.host = host; + req.port = port; + req.oncomplete = onlookupservice; + req.resolve = resolve; + req.reject = reject; + + const err = getnameinfo(req, host, port); + + if (err) + reject(dnsException(err, 'getnameinfo', host)); + }); +} + +function lookupService(host, port) { + if (arguments.length !== 2) + throw new ERR_MISSING_ARGS('host', 'port'); + + if (isIP(host) === 0) + throw new ERR_INVALID_OPT_VALUE('host', host); + + if (!isLegalPort(port)) + throw new ERR_SOCKET_BAD_PORT(port); + + return createLookupServicePromise(host, +port); +} + + +function onresolve(err, result, ttls) { + if (err) { + this.reject(dnsException(err, this.bindingName, this.hostname)); + return; + } + + if (ttls && this.ttl) + result = result.map((address, index) => ({ address, ttl: ttls[index] })); + + this.resolve(result); +} + +function createResolverPromise(resolver, bindingName, hostname, ttl) { + return new Promise((resolve, reject) => { + const req = new QueryReqWrap(); + + req.bindingName = bindingName; + req.hostname = hostname; + req.oncomplete = onresolve; + req.resolve = resolve; + req.reject = reject; + req.ttl = ttl; + + const err = resolver._handle[bindingName](req, hostname); + + if (err) + reject(dnsException(err, bindingName, hostname)); + }); +} + +function resolver(bindingName) { + function query(name, options) { + if (typeof name !== 'string') { + throw new ERR_INVALID_ARG_TYPE('name', 'string', name); + } + + const ttl = !!(options && options.ttl); + return createResolverPromise(this, bindingName, name, ttl); + } + + Object.defineProperty(query, 'name', { value: bindingName }); + return query; +} + + +const resolveMap = Object.create(null); + +// Resolver instances correspond 1:1 to c-ares channels. +class Resolver { + constructor() { + this._handle = new ChannelWrap(); + } +} + +Resolver.prototype.cancel = CallbackResolver.prototype.cancel; +Resolver.prototype.getServers = CallbackResolver.prototype.getServers; +Resolver.prototype.setServers = CallbackResolver.prototype.setServers; +Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny'); +Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA'); +Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa'); +Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname'); +Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx'); +Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs'); +Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt'); +Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv'); +Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr'); +Resolver.prototype.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr'); +Resolver.prototype.resolveSoa = resolveMap.SOA = resolver('querySoa'); +Resolver.prototype.reverse = resolver('getHostByAddr'); +Resolver.prototype.resolve = function resolve(hostname, rrtype) { + var resolver; + + if (typeof rrtype === 'string') { + resolver = resolveMap[rrtype]; + + if (typeof resolver !== 'function') + throw new ERR_INVALID_OPT_VALUE('rrtype', rrtype); + } else if (rrtype === undefined) { + resolver = resolveMap.A; + } else { + throw new ERR_INVALID_ARG_TYPE('rrtype', 'string', rrtype); + } + + return resolver.call(this, hostname); +}; + + +module.exports = { lookup, lookupService, Resolver }; +bindDefaultResolver(module.exports, Resolver.prototype); diff --git a/lib/internal/dns/utils.js b/lib/internal/dns/utils.js new file mode 100644 index 00000000000..43b65418848 --- /dev/null +++ b/lib/internal/dns/utils.js @@ -0,0 +1,141 @@ +'use strict'; +const errors = require('internal/errors'); +const { isIP } = require('internal/net'); +const { + ChannelWrap, + strerror, + AI_ADDRCONFIG, + AI_V4MAPPED +} = process.binding('cares_wrap'); +const IANA_DNS_PORT = 53; +const IPv6RE = /^\[([^[\]]*)\]/; +const addrSplitRE = /(^.+?)(?::(\d+))?$/; +const { + ERR_DNS_SET_SERVERS_FAILED, + ERR_INVALID_IP_ADDRESS, + ERR_INVALID_OPT_VALUE +} = errors.codes; + +// Resolver instances correspond 1:1 to c-ares channels. +class Resolver { + constructor() { + this._handle = new ChannelWrap(); + } + + cancel() { + this._handle.cancel(); + } + + getServers() { + return this._handle.getServers().map((val) => { + if (!val[1] || val[1] === IANA_DNS_PORT) + return val[0]; + + const host = isIP(val[0]) === 6 ? `[${val[0]}]` : val[0]; + return `${host}:${val[1]}`; + }); + } + + setServers(servers) { + // Cache the original servers because in the event of an error while + // setting the servers, c-ares won't have any servers available for + // resolution. + const orig = this._handle.getServers(); + const newSet = []; + + servers.forEach((serv) => { + var ipVersion = isIP(serv); + + if (ipVersion !== 0) + return newSet.push([ipVersion, serv, IANA_DNS_PORT]); + + const match = serv.match(IPv6RE); + + // Check for an IPv6 in brackets. + if (match) { + ipVersion = isIP(match[1]); + + if (ipVersion !== 0) { + const port = + parseInt(serv.replace(addrSplitRE, '$2')) || + IANA_DNS_PORT; + return newSet.push([ipVersion, match[1], port]); + } + } + + // addr::port + const addrSplitMatch = serv.match(addrSplitRE); + + if (addrSplitMatch) { + const hostIP = addrSplitMatch[1]; + const port = addrSplitMatch[2] || IANA_DNS_PORT; + + ipVersion = isIP(hostIP); + + if (ipVersion !== 0) { + return newSet.push([ipVersion, hostIP, parseInt(port)]); + } + } + + throw new ERR_INVALID_IP_ADDRESS(serv); + }); + + const errorNumber = this._handle.setServers(newSet); + + if (errorNumber !== 0) { + // Reset the servers to the old servers, because ares probably unset them. + this._handle.setServers(orig.join(',')); + const err = strerror(errorNumber); + throw new ERR_DNS_SET_SERVERS_FAILED(err, servers); + } + } +} + +let defaultResolver = new Resolver(); +const resolverKeys = [ + 'getServers', + 'resolve', + 'resolveAny', + 'resolve4', + 'resolve6', + 'resolveCname', + 'resolveMx', + 'resolveNs', + 'resolveTxt', + 'resolveSrv', + 'resolvePtr', + 'resolveNaptr', + 'resolveSoa', + 'reverse' +]; + +function getDefaultResolver() { + return defaultResolver; +} + +function setDefaultResolver(resolver) { + defaultResolver = resolver; +} + +function bindDefaultResolver(target, source) { + resolverKeys.forEach((key) => { + target[key] = source[key].bind(defaultResolver); + }); +} + +function validateHints(hints) { + if (hints !== 0 && + hints !== AI_ADDRCONFIG && + hints !== AI_V4MAPPED && + hints !== (AI_ADDRCONFIG | AI_V4MAPPED)) { + throw new ERR_INVALID_OPT_VALUE('hints', hints); + } +} + +module.exports = { + bindDefaultResolver, + getDefaultResolver, + setDefaultResolver, + validateHints, + Resolver +}; |