releases-comm-central/mail/modules/DNS.jsm

485 строки
13 KiB
JavaScript

/* 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/. */
/**
* This module is responsible for performing DNS queries using ctypes for
* loading system DNS libraries on Linux, Mac and Windows.
*/
const EXPORTED_SYMBOLS = ["DNS"];
var DNS = null;
if (typeof Components !== "undefined") {
var { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { BasePromiseWorker } = ChromeUtils.import(
"resource://gre/modules/PromiseWorker.jsm"
);
}
var LOCATION = "resource:///modules/DNS.jsm";
// These constants are luckily shared, but with different names
var NS_T_TXT = 16; // DNS_TYPE_TXT
var NS_T_SRV = 33; // DNS_TYPE_SRV
var NS_T_MX = 15; // DNS_TYPE_MX
// For Linux and Mac.
function load_libresolv() {
this._open();
}
load_libresolv.prototype = {
library: null,
// Tries to find and load library.
_open() {
function findLibrary() {
let lastException = null;
let candidates = [
{ name: "resolv.9", suffix: "" },
{ name: "resolv", suffix: ".2" },
{ name: "resolv", suffix: "" },
];
let tried = [];
for (let candidate of candidates) {
try {
let name = ctypes.libraryName(candidate.name) + candidate.suffix;
tried.push(name);
return ctypes.open(name);
} catch (ex) {
lastException = ex;
}
}
throw new Error(
"Could not find libresolv in any of " +
tried +
" Exception: " +
lastException +
"\n"
);
}
// Declaring functions to be able to call them.
function declare(aSymbolNames, ...aArgs) {
let lastException = null;
if (!Array.isArray(aSymbolNames)) {
aSymbolNames = [aSymbolNames];
}
for (let name of aSymbolNames) {
try {
return library.declare(name, ...aArgs);
} catch (ex) {
lastException = ex;
}
}
library.close();
throw new Error(
"Failed to declare " +
aSymbolNames +
" Exception: " +
lastException +
"\n"
);
}
let 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"],
ctypes.default_abi,
ctypes.unsigned_int,
ctypes.unsigned_char.ptr
);
this.ns_get32 = declare(
["res_9_ns_get32", "ns_get32"],
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;
},
close() {
this.library.close();
this.library = null;
},
// Maps record to SRVRecord, TXTRecord, or MXRecord according to aTypeID and
// returns it.
_mapAnswer(aTypeID, aAnswer, aIdx, aLength) {
if (aTypeID == NS_T_SRV) {
let prio = this.ns_get16(aAnswer.addressOfElement(aIdx));
let weight = this.ns_get16(aAnswer.addressOfElement(aIdx + 2));
let port = this.ns_get16(aAnswer.addressOfElement(aIdx + 4));
let hostbuf = ctypes.char.array(this.NS_MAXCDNAME)();
let hostlen = this.dn_expand(
aAnswer.addressOfElement(0),
aAnswer.addressOfElement(aLength),
aAnswer.addressOfElement(aIdx + 6),
hostbuf,
this.NS_MAXCDNAME
);
let host = hostlen > -1 ? hostbuf.readString() : null;
return new SRVRecord(prio, weight, host, port);
} else if (aTypeID == NS_T_TXT) {
// TODO should only read dataLength characters.
let data = ctypes.unsigned_char.ptr(aAnswer.addressOfElement(aIdx + 1));
return new TXTRecord(data.readString());
} else if (aTypeID == NS_T_MX) {
let prio = this.ns_get16(aAnswer.addressOfElement(aIdx));
let hostbuf = ctypes.char.array(this.NS_MAXCDNAME)();
let hostlen = this.dn_expand(
aAnswer.addressOfElement(0),
aAnswer.addressOfElement(aLength),
aAnswer.addressOfElement(aIdx + 2),
hostbuf,
this.NS_MAXCDNAME
);
let host = hostlen > -1 ? hostbuf.readString() : null;
return new MXRecord(prio, host);
}
return {};
},
// Performs a DNS query for aTypeID on a certain address (aName) and returns
// array of records of aTypeID.
lookup(aName, aTypeID) {
let qname = ctypes.char.array()(aName);
let answer = ctypes.unsigned_char.array(this.QUERYBUF_SIZE)();
let length = this.res_search(
qname,
this.NS_C_IN,
aTypeID,
answer,
this.QUERYBUF_SIZE
);
// There is an error.
if (length < 0) {
return [];
}
let results = [];
let idx = this.NS_HFIXEDSZ;
let qdcount = this.ns_get16(answer.addressOfElement(4));
let ancount = this.ns_get16(answer.addressOfElement(6));
for (let qdidx = 0; qdidx < qdcount && idx < length; qdidx++) {
idx +=
this.NS_QFIXEDSZ +
this.dn_skipname(
answer.addressOfElement(idx),
answer.addressOfElement(length)
);
}
for (let anidx = 0; anidx < ancount && idx < length; anidx++) {
idx += this.dn_skipname(
answer.addressOfElement(idx),
answer.addressOfElement(length)
);
let rridx = idx;
let type = this.ns_get16(answer.addressOfElement(rridx));
let dataLength = this.ns_get16(answer.addressOfElement(rridx + 8));
idx += this.NS_RRFIXEDSZ;
if (type === aTypeID) {
let resource = this._mapAnswer(aTypeID, answer, idx, length);
resource.type = type;
resource.nsclass = this.ns_get16(answer.addressOfElement(rridx + 2));
resource.ttl = this.ns_get32(answer.addressOfElement(rridx + 4)) | 0;
results.push(resource);
}
idx += dataLength;
}
return results;
},
};
// For Windows.
function load_dnsapi() {
this._open();
}
load_dnsapi.prototype = {
library: null,
// 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 + " Exception: " + ex + "\n"
);
}
}
let 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.
_mapAnswer(aTypeID, aData) {
if (aTypeID == NS_T_SRV) {
let srvdata = ctypes.cast(aData, this.DNS_SRV_DATA);
return new SRVRecord(
srvdata.wPriority,
srvdata.wWeight,
srvdata.pNameTarget.readString(),
srvdata.wPort
);
} else if (aTypeID == NS_T_TXT) {
let txtdata = ctypes.cast(aData, this.DNS_TXT_DATA);
if (txtdata.dwStringCount > 0) {
return new TXTRecord(txtdata.pStringArray[0].readString());
}
} else if (aTypeID == NS_T_MX) {
let mxdata = ctypes.cast(aData, this.DNS_MX_DATA);
return new MXRecord(mxdata.wPriority, mxdata.pNameTarget.readString());
}
return {};
},
// Performs a DNS query for aTypeID on a certain address (aName) and returns
// array of records of aTypeID (e.g. SRVRecord, TXTRecord, or MXRecord).
lookup(aName, aTypeID) {
let queryResultsSet = this.PDNS_RECORD();
let qname = ctypes.jschar.array()(aName);
let 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 [];
}
let results = [];
for (
let presult = queryResultsSet;
presult && !presult.isNull();
presult = presult.contents.pNext
) {
let result = presult.contents;
if (result.wType == aTypeID) {
let 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;
},
};
// Used to make results of different libraries consistent for SRV queries.
function SRVRecord(aPrio, aWeight, aHost, aPort) {
this.prio = aPrio;
this.weight = aWeight;
this.host = aHost;
this.port = aPort;
}
// Used to make results of different libraries consistent for TXT queries.
function TXTRecord(aData) {
this.data = aData;
}
// Used to make results of different libraries consistent for MX queries.
function MXRecord(aPrio, aHost) {
this.prio = aPrio;
this.host = aHost;
}
if (typeof Components === "undefined") {
/* eslint-env worker */
// We are in a worker, wait for our message then execute the wanted method.
importScripts("resource://gre/modules/workers/require.js");
let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
let worker = new PromiseWorker.AbstractWorker();
worker.dispatch = function(aMethod, aArgs = []) {
return self[aMethod](...aArgs);
};
worker.postMessage = function(...aArgs) {
self.postMessage(...aArgs);
};
worker.close = function() {
self.close();
};
self.addEventListener("message", msg => worker.handleMessage(msg));
// eslint-disable-next-line no-unused-vars
function execute(aOS, aMethod, aArgs) {
let DNS = aOS == "WINNT" ? new load_dnsapi() : new load_libresolv();
return DNS[aMethod].apply(DNS, aArgs);
}
} else {
// We are loaded as a JSM, provide the async front that will start the
// worker.
var dns_async_front = {
/**
* Constants for use with the lookup function.
*/
TXT: NS_T_TXT,
SRV: NS_T_SRV,
MX: NS_T_MX,
/**
* Do an asynchronous DNS lookup. The returned promise resolves with
* one of the Answer objects as defined above, or rejects with the
* error from the worker.
*
* Example: DNS.lookup("_caldavs._tcp.example.com", DNS.SRV)
*
* @param aName The aName to look up.
* @param aTypeID The RR type to look up as a constant.
* @return A promise resolved when completed.
*/
lookup(aName, aTypeID) {
let worker = new BasePromiseWorker(LOCATION);
return worker.post("execute", [
Services.appinfo.OS,
"lookup",
[...arguments],
]);
},
/** Convenience functions */
srv(aName) {
return this.lookup(aName, NS_T_SRV);
},
txt(aName) {
return this.lookup(aName, NS_T_TXT);
},
mx(aName) {
return this.lookup(aName, NS_T_MX);
},
};
DNS = dns_async_front;
}