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