/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ /* globals ctypes */ // We are in a worker, wait for our message then execute the wanted method. import { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs"; // These constants are luckily shared, but with different names const NS_T_TXT = 16; // DNS_TYPE_TXT const NS_T_SRV = 33; // DNS_TYPE_SRV const NS_T_MX = 15; // DNS_TYPE_MX /** * DNS API for *nix. */ class load_libresolv { library = null; /** * Constructor. * * @param {string} os - The OS - from AppConstants.unixstyle. */ constructor(os) { this._open(os); } /** * Tries to find and load library. * * @param {string} os - Operating System - from AppConstants.unixstyle. */ _open(os) { function findLibrary() { let lastException = null; let candidates = []; if (os == "freebsd") { candidates = [{ name: "c", suffix: ".7" }]; } else if (os == "openbsd") { candidates = [{ name: "c", suffix: "" }]; } else { candidates = [ { name: "resolv.9", suffix: "" }, { name: "resolv", suffix: ".2" }, { name: "resolv", suffix: "" }, ]; } const tried = []; for (const candidate of candidates) { try { const libName = ctypes.libraryName(candidate.name) + candidate.suffix; tried.push(libName); return ctypes.open(libName); } catch (ex) { lastException = ex; } } throw new Error(`Couldn't find libresolv for ${os}; tried: ${tried}`, { cause: lastException, }); } /** * Declaring functions to be able to call them. * * @param {string[]} aSymbolNames - OS function names. * @param {*} aArgs - Arguments to the call. */ function declare(aSymbolNames, ...aArgs) { let lastException = null; if (!Array.isArray(aSymbolNames)) { aSymbolNames = [aSymbolNames]; } for (const symbolName of aSymbolNames) { try { return library.declare(symbolName, ...aArgs); } catch (ex) { lastException = ex; } } library.close(); throw new Error(`Failed to declare: ${aSymbolNames}`, { cause: lastException, }); } const library = (this.library = findLibrary()); this.res_search = declare( ["res_9_search", "res_search", "__res_search"], ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int, ctypes.int, ctypes.unsigned_char.ptr, ctypes.int ); this.res_query = declare( ["res_9_query", "res_query", "__res_query"], ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int, ctypes.int, ctypes.unsigned_char.ptr, ctypes.int ); this.dn_expand = declare( ["res_9_dn_expand", "dn_expand", "__dn_expand"], ctypes.default_abi, ctypes.int, ctypes.unsigned_char.ptr, ctypes.unsigned_char.ptr, ctypes.unsigned_char.ptr, ctypes.char.ptr, ctypes.int ); this.dn_skipname = declare( ["res_9_dn_skipname", "dn_skipname", "__dn_skipname"], ctypes.default_abi, ctypes.int, ctypes.unsigned_char.ptr, ctypes.unsigned_char.ptr ); this.ns_get16 = declare( ["res_9_ns_get16", "ns_get16", "_getshort"], ctypes.default_abi, ctypes.unsigned_int, ctypes.unsigned_char.ptr ); this.ns_get32 = declare( ["res_9_ns_get32", "ns_get32", "_getlong"], ctypes.default_abi, ctypes.unsigned_long, ctypes.unsigned_char.ptr ); this.QUERYBUF_SIZE = 1024; this.NS_MAXCDNAME = 255; this.NS_HFIXEDSZ = 12; this.NS_QFIXEDSZ = 4; this.NS_RRFIXEDSZ = 10; this.NS_C_IN = 1; } /** * A wrapper around ns_get16 that performs bounds checks. * * This function will throw an exception if obtaining the value will result in * a buffer overrun. * * @param {index} idx The index to obtain the 16-bit value at. * @param {ctypes.unsigned_char.array} answer The DNS query answer buffer. * @param {integer} length The length of the DNS query answer buffer. * @returns {integer} The value at the index. */ _safe_get16(idx, answer, length) { if (idx + 2 > length) { throw new Error( `ns_get16: Illegal index ${idx} into array with length ${length}` ); } return this.ns_get16(answer.addressOfElement(idx)); } /** * A wrapper around ns_get32 that performs bounds checks. * * This function will throw an exception if obtaining the value will result in * a buffer overrun. * * @param {index} idx The index to obtain the 32-bit value at. * @param {ctypes.unsigned_char.array} answer The DNS query answer buffer. * @param {integer} length The length of the DNS query answer buffer. * @returns {integer} The value at the index. */ _safe_get32(idx, answer, length) { if (idx + 4 > length) { throw new Error( `ns_get32: Illegal index ${idx} into array with length ${length}` ); } return this.ns_get32(answer.addressOfElement(idx)); } close() { this.library.close(); this.library = null; } /** * Maps record to SRVRecord, TXTRecord, or MXRecord according to aTypeID and * returns it. * * @param {integer} aTypeID - Type, like NS_T_MX/NS_T_SRV/NS_T_MX. * @param {ctypes.unsigned_char.array} aAnswer - Message. * @param {index} aIdx - Offset into message, e.g. NS_HFIXEDSZ. * @param {integer} aLength - Message length (not the length of the current resource data). * @param {integer} aDataLength - The data length. * @returns {SRVRecord|TXTRecord|MXRecord} */ _mapAnswer(aTypeID, aAnswer, aIdx, aLength, aDataLength) { if (aTypeID == NS_T_SRV) { const prio = this._safe_get16(aIdx, aAnswer, aLength); const weight = this._safe_get16(aIdx + 2, aAnswer, aLength); const port = this._safe_get16(aIdx + 4, aAnswer, aLength); const hostbuf = ctypes.char.array(this.NS_MAXCDNAME)(); const hostlen = this.dn_expand( aAnswer.addressOfElement(0), aAnswer.addressOfElement(aLength), aAnswer.addressOfElement(aIdx + 6), hostbuf, this.NS_MAXCDNAME ); const host = hostlen > -1 ? hostbuf.readString() : null; return new SRVRecord(prio, weight, host, port); } if (aTypeID == NS_T_TXT) { // TXT records are 1 or more strings of 1 byte length followed by the data. const strings = []; let offset = 0; while (offset < aDataLength) { const txtlen = ctypes.cast( aAnswer.addressOfElement(aIdx + offset), ctypes.uint8_t.ptr ).contents; // Copy the data to a new array since readString() does not accept a length // property (and may overrun the string if there are additional answers). const txtstr = ctypes.unsigned_char.array(txtlen)(); for (let i = 0; i < txtlen; ++i) { txtstr.addressOfElement(i).contents = aAnswer.addressOfElement( aIdx + offset + 1 + i ).contents; } strings.push(txtstr.readString()); // Move past the current string. offset += txtlen + 1; } return new TXTRecord(strings); } if (aTypeID == NS_T_MX) { const prio = this._safe_get16(aIdx, aAnswer, aLength); const hostbuf = ctypes.char.array(this.NS_MAXCDNAME)(); const hostlen = this.dn_expand( aAnswer.addressOfElement(0), aAnswer.addressOfElement(aLength), aAnswer.addressOfElement(aIdx + 2), hostbuf, this.NS_MAXCDNAME ); const host = hostlen > -1 ? hostbuf.readString() : null; return new MXRecord(prio, host); } return null; } /** * Performs a DNS query for aTypeID on a certain address (aName) and returns * array of records of aTypeID. * * @param {string} aName - Address. * @param {integer} aTypeID - Type, like NS_T_MX/NS_T_SRV/NS_T_MX. * @returns {SRVRecord[]|TXTRecord[]|MXRecord[]} */ lookup(aName, aTypeID) { const qname = ctypes.char.array()(aName); const answer = ctypes.unsigned_char.array(this.QUERYBUF_SIZE)(); const length = this.res_search( qname, this.NS_C_IN, aTypeID, answer, this.QUERYBUF_SIZE ); // There is an error. if (length < 0) { return []; } const results = []; let idx = this.NS_HFIXEDSZ; const qdcount = this._safe_get16(4, answer, length); const ancount = this._safe_get16(6, answer, length); for (let qdidx = 0; qdidx < qdcount && idx < length; qdidx++) { const nextNameOffset = this.dn_skipname( answer.addressOfElement(idx), answer.addressOfElement(length) ); if (nextNameOffset < 0 || idx + nextNameOffset >= length) { return []; } idx += this.NS_QFIXEDSZ + nextNameOffset; } for (let anidx = 0; anidx < ancount && idx < length; anidx++) { const nextNameOffset = this.dn_skipname( answer.addressOfElement(idx), answer.addressOfElement(length) ); if (nextNameOffset < 0) { return []; } idx += nextNameOffset; const rridx = idx; const type = this._safe_get16(rridx, answer, length); const nsclass = this._safe_get16(rridx + 2, answer, length); const ttl = this._safe_get32(rridx + 4, answer, length) | 0; const dataLength = this._safe_get16(rridx + 8, answer, length); idx += this.NS_RRFIXEDSZ; // Make sure the data payload is within the answer buffer length bounds. if (idx + dataLength > length) { throw new Error( `Data overrun at index ${idx} with data length ${dataLength} for array of size ${length}.` ); } if (type === aTypeID) { const resource = this._mapAnswer( aTypeID, answer, idx, length, dataLength ); resource.type = type; resource.nsclass = nsclass; resource.ttl = ttl; results.push(resource); } idx += dataLength; } return results; } } /** * DNS API for Windows. */ class load_dnsapi { library = null; constructor() { this._open(); } /** * Tries to find and load library. */ _open() { function declare(aSymbolName, ...aArgs) { try { return library.declare(aSymbolName, ...aArgs); } catch (ex) { throw new Error(`Failed to declare: ${aSymbolName}`, { cause: ex }); } } const library = (this.library = ctypes.open(ctypes.libraryName("DnsAPI"))); this.DNS_SRV_DATA = ctypes.StructType("DNS_SRV_DATA", [ { pNameTarget: ctypes.jschar.ptr }, { wPriority: ctypes.unsigned_short }, { wWeight: ctypes.unsigned_short }, { wPort: ctypes.unsigned_short }, { Pad: ctypes.unsigned_short }, ]); this.DNS_TXT_DATA = ctypes.StructType("DNS_TXT_DATA", [ { dwStringCount: ctypes.unsigned_long }, { pStringArray: ctypes.jschar.ptr.array(1) }, ]); this.DNS_MX_DATA = ctypes.StructType("DNS_MX_DATA", [ { pNameTarget: ctypes.jschar.ptr }, { wPriority: ctypes.unsigned_short }, { Pad: ctypes.unsigned_short }, ]); this.DNS_RECORD = ctypes.StructType("_DnsRecord"); this.DNS_RECORD.define([ { pNext: this.DNS_RECORD.ptr }, { pName: ctypes.jschar.ptr }, { wType: ctypes.unsigned_short }, { wDataLength: ctypes.unsigned_short }, { Flags: ctypes.unsigned_long }, { dwTtl: ctypes.unsigned_long }, { dwReserved: ctypes.unsigned_long }, { Data: this.DNS_SRV_DATA }, // it's a union, can be cast to many things ]); this.PDNS_RECORD = ctypes.PointerType(this.DNS_RECORD); this.DnsQuery_W = declare( "DnsQuery_W", ctypes.winapi_abi, ctypes.long, ctypes.jschar.ptr, ctypes.unsigned_short, ctypes.unsigned_long, ctypes.voidptr_t, this.PDNS_RECORD.ptr, ctypes.voidptr_t.ptr ); this.DnsRecordListFree = declare( "DnsRecordListFree", ctypes.winapi_abi, ctypes.void_t, this.PDNS_RECORD, ctypes.int ); this.ERROR_SUCCESS = ctypes.Int64(0); this.DNS_QUERY_STANDARD = 0; this.DnsFreeRecordList = 1; } close() { this.library.close(); this.library = null; } /** * Maps record to SRVRecord, TXTRecord, or MXRecord according to aTypeID and * returns it. * * @param {integer} aTypeID - Type, like NS_T_MX/NS_T_SRV/NS_T_MX. * @param {object} aData - Raw data to map to a specific type. * @returns {SRVRecord|TXTRecord|MXRecord} */ _mapAnswer(aTypeID, aData) { if (aTypeID == NS_T_SRV) { const srvdata = ctypes.cast(aData, this.DNS_SRV_DATA); return new SRVRecord( srvdata.wPriority, srvdata.wWeight, srvdata.pNameTarget.readString(), srvdata.wPort ); } if (aTypeID == NS_T_TXT) { const txtdata = ctypes.cast(aData, this.DNS_TXT_DATA); const pStringArray = ctypes.cast( txtdata.pStringArray.addressOfElement(0), ctypes.char16_t.ptr.array(txtdata.dwStringCount).ptr ); return new TXTRecord( Array.from(pStringArray.contents, str => str.readString()) ); } if (aTypeID == NS_T_MX) { const mxdata = ctypes.cast(aData, this.DNS_MX_DATA); return new MXRecord(mxdata.wPriority, mxdata.pNameTarget.readString()); } return null; } /** * Performs a DNS query for aTypeID on a certain address (aName) and returns * array of records of aTypeID (e.g. SRVRecord, TXTRecord, or MXRecord). * * @param {string} aName - Address. * @param {integer} aTypeID - Type, like NS_T_MX/NS_T_SRV/NS_T_MX. * @returns {SRVRecord[]|TXTRecord[]|MXRecord[]} */ lookup(aName, aTypeID) { const queryResultsSet = this.PDNS_RECORD(); const qname = ctypes.jschar.array()(aName); const dnsStatus = this.DnsQuery_W( qname, aTypeID, this.DNS_QUERY_STANDARD, null, queryResultsSet.address(), null ); // There is an error. if (ctypes.Int64.compare(dnsStatus, this.ERROR_SUCCESS) != 0) { return []; } const results = []; for ( let presult = queryResultsSet; presult && !presult.isNull(); presult = presult.contents.pNext ) { const result = presult.contents; if (result.wType == aTypeID) { const resource = this._mapAnswer(aTypeID, result.Data); resource.type = result.wType; resource.nsclass = 0; resource.ttl = result.dwTtl | 0; results.push(resource); } } this.DnsRecordListFree(queryResultsSet, this.DnsFreeRecordList); return results; } } /** * Represents and SRV record. * Used to make results of different libraries consistent for SRV queries. * * @param {integer} prio * @param {integer} weight * @param {string} host * @param {?integer} port */ function SRVRecord(prio, weight, host, port) { this.prio = prio; this.weight = weight; this.host = host.toLowerCase(); this.port = port; } /** * Represents a TXT record. * Used to make results of different libraries consistent for TXT queries. * * @param {string} strings */ function TXTRecord(strings) { this.strings = strings; } /** * Represents an MX record. * Used to make results of different libraries consistent for MX queries. * * @param {integer} prio * @param {string} host */ function MXRecord(prio, host) { this.prio = prio; this.host = host.toLowerCase(); } const worker = new PromiseWorker.AbstractWorker(); worker.dispatch = (method, args = []) => { return worker[method](...args); // Call worker.execute() }; worker.execute = (platform, unixstyle, method, args) => { const DNS = platform == "win" ? new load_dnsapi() : new load_libresolv(unixstyle); return DNS[method].apply(DNS, args); }; worker.postMessage = function (...args) { self.postMessage(...args); }; worker.close = function () { self.close(); }; self.addEventListener("message", msg => worker.handleMessage(msg));