1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/**
6 * This module is responsible for performing DNS queries using ctypes for
7 * loading system DNS libraries on Linux, Mac and Windows.
8 */
9
10const EXPORTED_SYMBOLS = ["DNS"];
11
12var DNS = null;
13
14if (typeof Components !== "undefined") {
15  var { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
16  var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17  var { BasePromiseWorker } = ChromeUtils.import(
18    "resource://gre/modules/PromiseWorker.jsm"
19  );
20}
21
22var LOCATION = "resource:///modules/DNS.jsm";
23
24// These constants are luckily shared, but with different names
25var NS_T_TXT = 16; // DNS_TYPE_TXT
26var NS_T_SRV = 33; // DNS_TYPE_SRV
27var NS_T_MX = 15; // DNS_TYPE_MX
28
29// For Linux and Mac.
30function load_libresolv() {
31  this._open();
32}
33
34load_libresolv.prototype = {
35  library: null,
36
37  // Tries to find and load library.
38  _open() {
39    function findLibrary() {
40      let lastException = null;
41      if(Services.appinfo.OS.toLowerCase() == "freebsd") {
42        let candidates = [
43          { name: "c", suffix: ".7" },
44        ]
45      } else {
46        let candidates = [
47          { name: "resolv.9", suffix: "" },
48          { name: "resolv", suffix: ".2" },
49          { name: "resolv", suffix: "" },
50        ];
51      }
52      let tried = [];
53      for (let candidate of candidates) {
54        try {
55          let name = ctypes.libraryName(candidate.name) + candidate.suffix;
56          tried.push(name);
57          return ctypes.open(name);
58        } catch (ex) {
59          lastException = ex;
60        }
61      }
62      throw new Error(
63        "Could not find libresolv in any of " +
64          tried +
65          " Exception: " +
66          lastException +
67          "\n"
68      );
69    }
70
71    // Declaring functions to be able to call them.
72    function declare(aSymbolNames, ...aArgs) {
73      let lastException = null;
74      if (!Array.isArray(aSymbolNames)) {
75        aSymbolNames = [aSymbolNames];
76      }
77
78      for (let name of aSymbolNames) {
79        try {
80          return library.declare(name, ...aArgs);
81        } catch (ex) {
82          lastException = ex;
83        }
84      }
85      library.close();
86      throw new Error(
87        "Failed to declare " +
88          aSymbolNames +
89          " Exception: " +
90          lastException +
91          "\n"
92      );
93    }
94
95    let library = (this.library = findLibrary());
96    this.res_search = declare(
97      ["res_9_search", "res_search", "__res_search"],
98      ctypes.default_abi,
99      ctypes.int,
100      ctypes.char.ptr,
101      ctypes.int,
102      ctypes.int,
103      ctypes.unsigned_char.ptr,
104      ctypes.int
105    );
106    this.res_query = declare(
107      ["res_9_query", "res_query", "__res_query"],
108      ctypes.default_abi,
109      ctypes.int,
110      ctypes.char.ptr,
111      ctypes.int,
112      ctypes.int,
113      ctypes.unsigned_char.ptr,
114      ctypes.int
115    );
116    this.dn_expand = declare(
117      ["res_9_dn_expand", "dn_expand", "__dn_expand"],
118      ctypes.default_abi,
119      ctypes.int,
120      ctypes.unsigned_char.ptr,
121      ctypes.unsigned_char.ptr,
122      ctypes.unsigned_char.ptr,
123      ctypes.char.ptr,
124      ctypes.int
125    );
126    this.dn_skipname = declare(
127      ["res_9_dn_skipname", "dn_skipname", "__dn_skipname"],
128      ctypes.default_abi,
129      ctypes.int,
130      ctypes.unsigned_char.ptr,
131      ctypes.unsigned_char.ptr
132    );
133    this.ns_get16 = declare(
134      ["res_9_ns_get16", "ns_get16"],
135      ctypes.default_abi,
136      ctypes.unsigned_int,
137      ctypes.unsigned_char.ptr
138    );
139    this.ns_get32 = declare(
140      ["res_9_ns_get32", "ns_get32"],
141      ctypes.default_abi,
142      ctypes.unsigned_long,
143      ctypes.unsigned_char.ptr
144    );
145
146    this.QUERYBUF_SIZE = 1024;
147    this.NS_MAXCDNAME = 255;
148    this.NS_HFIXEDSZ = 12;
149    this.NS_QFIXEDSZ = 4;
150    this.NS_RRFIXEDSZ = 10;
151    this.NS_C_IN = 1;
152  },
153
154  close() {
155    this.library.close();
156    this.library = null;
157  },
158
159  // Maps record to SRVRecord, TXTRecord, or MXRecord according to aTypeID and
160  // returns it.
161  _mapAnswer(aTypeID, aAnswer, aIdx, aLength) {
162    if (aTypeID == NS_T_SRV) {
163      let prio = this.ns_get16(aAnswer.addressOfElement(aIdx));
164      let weight = this.ns_get16(aAnswer.addressOfElement(aIdx + 2));
165      let port = this.ns_get16(aAnswer.addressOfElement(aIdx + 4));
166
167      let hostbuf = ctypes.char.array(this.NS_MAXCDNAME)();
168      let hostlen = this.dn_expand(
169        aAnswer.addressOfElement(0),
170        aAnswer.addressOfElement(aLength),
171        aAnswer.addressOfElement(aIdx + 6),
172        hostbuf,
173        this.NS_MAXCDNAME
174      );
175      let host = hostlen > -1 ? hostbuf.readString() : null;
176      return new SRVRecord(prio, weight, host, port);
177    } else if (aTypeID == NS_T_TXT) {
178      // TODO should only read dataLength characters.
179      let data = ctypes.unsigned_char.ptr(aAnswer.addressOfElement(aIdx + 1));
180
181      return new TXTRecord(data.readString());
182    } else if (aTypeID == NS_T_MX) {
183      let prio = this.ns_get16(aAnswer.addressOfElement(aIdx));
184
185      let hostbuf = ctypes.char.array(this.NS_MAXCDNAME)();
186      let hostlen = this.dn_expand(
187        aAnswer.addressOfElement(0),
188        aAnswer.addressOfElement(aLength),
189        aAnswer.addressOfElement(aIdx + 2),
190        hostbuf,
191        this.NS_MAXCDNAME
192      );
193      let host = hostlen > -1 ? hostbuf.readString() : null;
194      return new MXRecord(prio, host);
195    }
196    return {};
197  },
198
199  // Performs a DNS query for aTypeID on a certain address (aName) and returns
200  // array of records of aTypeID.
201  lookup(aName, aTypeID) {
202    let qname = ctypes.char.array()(aName);
203    let answer = ctypes.unsigned_char.array(this.QUERYBUF_SIZE)();
204    let length = this.res_search(
205      qname,
206      this.NS_C_IN,
207      aTypeID,
208      answer,
209      this.QUERYBUF_SIZE
210    );
211
212    // There is an error.
213    if (length < 0) {
214      return [];
215    }
216
217    let results = [];
218    let idx = this.NS_HFIXEDSZ;
219
220    let qdcount = this.ns_get16(answer.addressOfElement(4));
221    let ancount = this.ns_get16(answer.addressOfElement(6));
222
223    for (let qdidx = 0; qdidx < qdcount && idx < length; qdidx++) {
224      idx +=
225        this.NS_QFIXEDSZ +
226        this.dn_skipname(
227          answer.addressOfElement(idx),
228          answer.addressOfElement(length)
229        );
230    }
231
232    for (let anidx = 0; anidx < ancount && idx < length; anidx++) {
233      idx += this.dn_skipname(
234        answer.addressOfElement(idx),
235        answer.addressOfElement(length)
236      );
237      let rridx = idx;
238      let type = this.ns_get16(answer.addressOfElement(rridx));
239      let dataLength = this.ns_get16(answer.addressOfElement(rridx + 8));
240
241      idx += this.NS_RRFIXEDSZ;
242
243      if (type === aTypeID) {
244        let resource = this._mapAnswer(aTypeID, answer, idx, length);
245        resource.type = type;
246        resource.nsclass = this.ns_get16(answer.addressOfElement(rridx + 2));
247        resource.ttl = this.ns_get32(answer.addressOfElement(rridx + 4)) | 0;
248        results.push(resource);
249      }
250      idx += dataLength;
251    }
252    return results;
253  },
254};
255
256// For Windows.
257function load_dnsapi() {
258  this._open();
259}
260
261load_dnsapi.prototype = {
262  library: null,
263
264  // Tries to find and load library.
265  _open() {
266    function declare(aSymbolName, ...aArgs) {
267      try {
268        return library.declare(aSymbolName, ...aArgs);
269      } catch (ex) {
270        throw new Error(
271          "Failed to declare " + aSymbolName + " Exception: " + ex + "\n"
272        );
273      }
274    }
275
276    let library = (this.library = ctypes.open(ctypes.libraryName("DnsAPI")));
277
278    this.DNS_SRV_DATA = ctypes.StructType("DNS_SRV_DATA", [
279      { pNameTarget: ctypes.jschar.ptr },
280      { wPriority: ctypes.unsigned_short },
281      { wWeight: ctypes.unsigned_short },
282      { wPort: ctypes.unsigned_short },
283      { Pad: ctypes.unsigned_short },
284    ]);
285
286    this.DNS_TXT_DATA = ctypes.StructType("DNS_TXT_DATA", [
287      { dwStringCount: ctypes.unsigned_long },
288      { pStringArray: ctypes.jschar.ptr.array(1) },
289    ]);
290
291    this.DNS_MX_DATA = ctypes.StructType("DNS_MX_DATA", [
292      { pNameTarget: ctypes.jschar.ptr },
293      { wPriority: ctypes.unsigned_short },
294      { Pad: ctypes.unsigned_short },
295    ]);
296
297    this.DNS_RECORD = ctypes.StructType("_DnsRecord");
298    this.DNS_RECORD.define([
299      { pNext: this.DNS_RECORD.ptr },
300      { pName: ctypes.jschar.ptr },
301      { wType: ctypes.unsigned_short },
302      { wDataLength: ctypes.unsigned_short },
303      { Flags: ctypes.unsigned_long },
304      { dwTtl: ctypes.unsigned_long },
305      { dwReserved: ctypes.unsigned_long },
306      { Data: this.DNS_SRV_DATA }, // it's a union, can be cast to many things
307    ]);
308
309    this.PDNS_RECORD = ctypes.PointerType(this.DNS_RECORD);
310    this.DnsQuery_W = declare(
311      "DnsQuery_W",
312      ctypes.winapi_abi,
313      ctypes.long,
314      ctypes.jschar.ptr,
315      ctypes.unsigned_short,
316      ctypes.unsigned_long,
317      ctypes.voidptr_t,
318      this.PDNS_RECORD.ptr,
319      ctypes.voidptr_t.ptr
320    );
321    this.DnsRecordListFree = declare(
322      "DnsRecordListFree",
323      ctypes.winapi_abi,
324      ctypes.void_t,
325      this.PDNS_RECORD,
326      ctypes.int
327    );
328
329    this.ERROR_SUCCESS = ctypes.Int64(0);
330    this.DNS_QUERY_STANDARD = 0;
331    this.DnsFreeRecordList = 1;
332  },
333
334  close() {
335    this.library.close();
336    this.library = null;
337  },
338
339  // Maps record to SRVRecord, TXTRecord, or MXRecord according to aTypeID and
340  // returns it.
341  _mapAnswer(aTypeID, aData) {
342    if (aTypeID == NS_T_SRV) {
343      let srvdata = ctypes.cast(aData, this.DNS_SRV_DATA);
344
345      return new SRVRecord(
346        srvdata.wPriority,
347        srvdata.wWeight,
348        srvdata.pNameTarget.readString(),
349        srvdata.wPort
350      );
351    } else if (aTypeID == NS_T_TXT) {
352      let txtdata = ctypes.cast(aData, this.DNS_TXT_DATA);
353      if (txtdata.dwStringCount > 0) {
354        return new TXTRecord(txtdata.pStringArray[0].readString());
355      }
356    } else if (aTypeID == NS_T_MX) {
357      let mxdata = ctypes.cast(aData, this.DNS_MX_DATA);
358
359      return new MXRecord(mxdata.wPriority, mxdata.pNameTarget.readString());
360    }
361    return {};
362  },
363
364  // Performs a DNS query for aTypeID on a certain address (aName) and returns
365  // array of records of aTypeID (e.g. SRVRecord, TXTRecord, or MXRecord).
366  lookup(aName, aTypeID) {
367    let queryResultsSet = this.PDNS_RECORD();
368    let qname = ctypes.jschar.array()(aName);
369    let dnsStatus = this.DnsQuery_W(
370      qname,
371      aTypeID,
372      this.DNS_QUERY_STANDARD,
373      null,
374      queryResultsSet.address(),
375      null
376    );
377
378    // There is an error.
379    if (ctypes.Int64.compare(dnsStatus, this.ERROR_SUCCESS) != 0) {
380      return [];
381    }
382
383    let results = [];
384    for (
385      let presult = queryResultsSet;
386      presult && !presult.isNull();
387      presult = presult.contents.pNext
388    ) {
389      let result = presult.contents;
390      if (result.wType == aTypeID) {
391        let resource = this._mapAnswer(aTypeID, result.Data);
392        resource.type = result.wType;
393        resource.nsclass = 0;
394        resource.ttl = result.dwTtl | 0;
395        results.push(resource);
396      }
397    }
398
399    this.DnsRecordListFree(queryResultsSet, this.DnsFreeRecordList);
400    return results;
401  },
402};
403
404// Used to make results of different libraries consistent for SRV queries.
405function SRVRecord(aPrio, aWeight, aHost, aPort) {
406  this.prio = aPrio;
407  this.weight = aWeight;
408  this.host = aHost;
409  this.port = aPort;
410}
411
412// Used to make results of different libraries consistent for TXT queries.
413function TXTRecord(aData) {
414  this.data = aData;
415}
416
417// Used to make results of different libraries consistent for MX queries.
418function MXRecord(aPrio, aHost) {
419  this.prio = aPrio;
420  this.host = aHost;
421}
422
423if (typeof Components === "undefined") {
424  /* eslint-env worker */
425
426  // We are in a worker, wait for our message then execute the wanted method.
427  importScripts("resource://gre/modules/workers/require.js");
428  let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
429
430  let worker = new PromiseWorker.AbstractWorker();
431  worker.dispatch = function(aMethod, aArgs = []) {
432    return self[aMethod](...aArgs);
433  };
434  worker.postMessage = function(...aArgs) {
435    self.postMessage(...aArgs);
436  };
437  worker.close = function() {
438    self.close();
439  };
440  self.addEventListener("message", msg => worker.handleMessage(msg));
441
442  // eslint-disable-next-line no-unused-vars
443  function execute(aOS, aMethod, aArgs) {
444    let DNS = aOS == "WINNT" ? new load_dnsapi() : new load_libresolv();
445    return DNS[aMethod].apply(DNS, aArgs);
446  }
447} else {
448  // We are loaded as a JSM, provide the async front that will start the
449  // worker.
450  var dns_async_front = {
451    /**
452     * Constants for use with the lookup function.
453     */
454    TXT: NS_T_TXT,
455    SRV: NS_T_SRV,
456    MX: NS_T_MX,
457
458    /**
459     * Do an asynchronous DNS lookup. The returned promise resolves with
460     * one of the Answer objects as defined above, or rejects with the
461     * error from the worker.
462     *
463     * Example: DNS.lookup("_caldavs._tcp.example.com", DNS.SRV)
464     *
465     * @param aName           The aName to look up.
466     * @param aTypeID         The RR type to look up as a constant.
467     * @return                A promise resolved when completed.
468     */
469    lookup(aName, aTypeID) {
470      let worker = new BasePromiseWorker(LOCATION);
471      return worker.post("execute", [
472        Services.appinfo.OS,
473        "lookup",
474        [...arguments],
475      ]);
476    },
477
478    /** Convenience functions */
479    srv(aName) {
480      return this.lookup(aName, NS_T_SRV);
481    },
482    txt(aName) {
483      return this.lookup(aName, NS_T_TXT);
484    },
485    mx(aName) {
486      return this.lookup(aName, NS_T_MX);
487    },
488  };
489  DNS = dns_async_front;
490}
491