dns: validate arguments in resolver
[platform/upstream/nodejs.git] / lib / dns.js
1 // Copyright Joyent, Inc. and other Node contributors.
2 //
3 // Permission is hereby granted, free of charge, to any person obtaining a
4 // copy of this software and associated documentation files (the
5 // 'Software'), to deal in the Software without restriction, including
6 // without limitation the rights to use, copy, modify, merge, publish,
7 // distribute, sublicense, and/or sell copies of the Software, and to permit
8 // persons to whom the Software is furnished to do so, subject to the
9 // following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included
12 // in all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17 // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20 // USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 var net = require('net');
23 var util = require('util');
24
25 var cares = process.binding('cares_wrap');
26 var uv = process.binding('uv');
27
28 var isIp = net.isIP;
29
30
31 function errnoException(err, syscall, hostname) {
32   // FIXME(bnoordhuis) Remove this backwards compatibility shite and pass
33   // the true error to the user. ENOTFOUND is not even a proper POSIX error!
34   if (err === uv.UV_EAI_MEMORY ||
35       err === uv.UV_EAI_NODATA ||
36       err === uv.UV_EAI_NONAME) {
37     err = 'ENOTFOUND';
38   }
39   var ex = null;
40   if (typeof err === 'string') {  // c-ares error code.
41     ex = new Error(syscall + ' ' + err);
42     ex.code = err;
43     ex.errno = err;
44     ex.syscall = syscall;
45   } else {
46     ex = util._errnoException(err, syscall);
47   }
48   if (hostname) {
49     ex.hostname = hostname;
50   }
51   return ex;
52 }
53
54
55 // c-ares invokes a callback either synchronously or asynchronously,
56 // but the dns API should always invoke a callback asynchronously.
57 //
58 // This function makes sure that the callback is invoked asynchronously.
59 // It returns a function that invokes the callback within nextTick().
60 //
61 // To avoid invoking unnecessary nextTick(), `immediately` property of
62 // returned function should be set to true after c-ares returned.
63 //
64 // Usage:
65 //
66 // function someAPI(callback) {
67 //   callback = makeAsync(callback);
68 //   channel.someAPI(..., callback);
69 //   callback.immediately = true;
70 // }
71 function makeAsync(callback) {
72   if (!util.isFunction(callback)) {
73     return callback;
74   }
75   return function asyncCallback() {
76     if (asyncCallback.immediately) {
77       // The API already returned, we can invoke the callback immediately.
78       callback.apply(null, arguments);
79     } else {
80       var args = arguments;
81       process.nextTick(function() {
82         callback.apply(null, args);
83       });
84     }
85   };
86 }
87
88
89 function onlookup(err, addresses) {
90   if (err) {
91     return this.callback(errnoException(err, 'getaddrinfo', this.hostname));
92   }
93   if (this.family) {
94     this.callback(null, addresses[0], this.family);
95   } else {
96     this.callback(null, addresses[0], addresses[0].indexOf(':') >= 0 ? 6 : 4);
97   }
98 }
99
100
101 // Easy DNS A/AAAA look up
102 // lookup(hostname, [family,] callback)
103 exports.lookup = function(hostname, family, callback) {
104   // parse arguments
105   if (arguments.length === 2) {
106     callback = family;
107     family = 0;
108   } else if (!family) {
109     family = 0;
110   } else {
111     family = +family;
112     if (family !== 4 && family !== 6) {
113       throw new Error('invalid argument: `family` must be 4 or 6');
114     }
115   }
116   callback = makeAsync(callback);
117
118   if (!hostname) {
119     callback(null, null, family === 6 ? 6 : 4);
120     return {};
121   }
122
123   var matchedFamily = net.isIP(hostname);
124   if (matchedFamily) {
125     callback(null, hostname, matchedFamily);
126     return {};
127   }
128
129   var req = {
130     callback: callback,
131     family: family,
132     hostname: hostname,
133     oncomplete: onlookup
134   };
135   var err = cares.getaddrinfo(req, hostname, family);
136   if (err) throw errnoException(err, 'getaddrinfo', hostname);
137
138   callback.immediately = true;
139   return req;
140 };
141
142
143 function onresolve(err, result) {
144   if (err)
145     this.callback(errnoException(err, this.bindingName, this.hostname));
146   else
147     this.callback(null, result);
148 }
149
150
151 function resolver(bindingName) {
152   var binding = cares[bindingName];
153
154   return function query(name, callback) {
155     if (!util.isString(name)) {
156       throw new Error('Name must be a string');
157     } else if (!util.isFunction(callback)) {
158       throw new Error('Callback must be a function');
159     }
160
161     callback = makeAsync(callback);
162     var req = {
163       bindingName: bindingName,
164       callback: callback,
165       hostname: name,
166       oncomplete: onresolve
167     };
168     var err = binding(req, name);
169     if (err) throw errnoException(err, bindingName);
170     callback.immediately = true;
171     return req;
172   }
173 }
174
175
176 var resolveMap = {};
177 exports.resolve4 = resolveMap.A = resolver('queryA');
178 exports.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
179 exports.resolveCname = resolveMap.CNAME = resolver('queryCname');
180 exports.resolveMx = resolveMap.MX = resolver('queryMx');
181 exports.resolveNs = resolveMap.NS = resolver('queryNs');
182 exports.resolveTxt = resolveMap.TXT = resolver('queryTxt');
183 exports.resolveSrv = resolveMap.SRV = resolver('querySrv');
184 exports.resolveNaptr = resolveMap.NAPTR = resolver('queryNaptr');
185 exports.resolveSoa = resolveMap.SOA = resolver('querySoa');
186 exports.reverse = resolveMap.PTR = resolver('getHostByAddr');
187
188
189 exports.resolve = function(hostname, type_, callback_) {
190   var resolver, callback;
191   if (util.isString(type_)) {
192     resolver = resolveMap[type_];
193     callback = callback_;
194   } else if (util.isFunction(type_)) {
195     resolver = exports.resolve4;
196     callback = type_;
197   } else {
198     throw new Error('Type must be a string');
199   }
200
201   if (util.isFunction(resolver)) {
202     return resolver(hostname, callback);
203   } else {
204     throw new Error('Unknown type "' + type_ + '"');
205   }
206 };
207
208
209 exports.getServers = function() {
210   return cares.getServers();
211 };
212
213
214 exports.setServers = function(servers) {
215   // cache the original servers because in the event of an error setting the
216   // servers cares won't have any servers available for resolution
217   var orig = cares.getServers();
218
219   var newSet = [];
220
221   servers.forEach(function(serv) {
222     var ver = isIp(serv);
223
224     if (ver)
225       return newSet.push([ver, serv]);
226
227     var match = serv.match(/\[(.*)\](:\d+)?/);
228
229     // we have an IPv6 in brackets
230     if (match) {
231       ver = isIp(match[1]);
232       if (ver)
233         return newSet.push([ver, match[1]]);
234     }
235
236     var s = serv.split(/:\d+$/)[0];
237     ver = isIp(s);
238
239     if (ver)
240       return newSet.push([ver, s]);
241
242     throw new Error('IP address is not properly formatted: ' + serv);
243   });
244
245   var r = cares.setServers(newSet);
246
247   if (r) {
248     // reset the servers to the old servers, because ares probably unset them
249     cares.setServers(orig.join(','));
250
251     var err = cares.strerror(r);
252     throw new Error('c-ares failed to set servers: "' + err +
253                     '" [' + servers + ']');
254   }
255 };
256
257
258 // ERROR CODES
259 exports.NODATA = 'ENODATA';
260 exports.FORMERR = 'EFORMERR';
261 exports.SERVFAIL = 'ESERVFAIL';
262 exports.NOTFOUND = 'ENOTFOUND';
263 exports.NOTIMP = 'ENOTIMP';
264 exports.REFUSED = 'EREFUSED';
265 exports.BADQUERY = 'EBADQUERY';
266 exports.ADNAME = 'EADNAME';
267 exports.BADFAMILY = 'EBADFAMILY';
268 exports.BADRESP = 'EBADRESP';
269 exports.CONNREFUSED = 'ECONNREFUSED';
270 exports.TIMEOUT = 'ETIMEOUT';
271 exports.EOF = 'EOF';
272 exports.FILE = 'EFILE';
273 exports.NOMEM = 'ENOMEM';
274 exports.DESTRUCTION = 'EDESTRUCTION';
275 exports.BADSTR = 'EBADSTR';
276 exports.BADFLAGS = 'EBADFLAGS';
277 exports.NONAME = 'ENONAME';
278 exports.BADHINTS = 'EBADHINTS';
279 exports.NOTINITIALIZED = 'ENOTINITIALIZED';
280 exports.LOADIPHLPAPI = 'ELOADIPHLPAPI';
281 exports.ADDRGETNETWORKPARAMS = 'EADDRGETNETWORKPARAMS';
282 exports.CANCELLED = 'ECANCELLED';