Bug 1373640 implement async dns resolve api for webextensions, r?kmag
MozReview-Commit-ID: 9lBEfVMUb3P
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -96,16 +96,17 @@ webextPerms.optionalPermsDeny.label=Deny
webextPerms.optionalPermsDeny.accessKey=D
webextPerms.description.bookmarks=Read and modify bookmarks
webextPerms.description.browserSettings=Read and modify browser settings
webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data
webextPerms.description.clipboardRead=Get data from the clipboard
webextPerms.description.clipboardWrite=Input data to the clipboard
webextPerms.description.devtools=Extend developer tools to access your data in open tabs
+webextPerms.description.dns=Access IP address and hostname information
webextPerms.description.downloads=Download files and read and modify the browser’s download history
webextPerms.description.downloads.open=Open files downloaded to your computer
webextPerms.description.find=Read the text of all open tabs
webextPerms.description.geolocation=Access your location
webextPerms.description.history=Access browsing history
webextPerms.description.management=Monitor extension usage and manage themes
# LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
# %S will be replaced with the name of the application
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-dns.js
@@ -0,0 +1,84 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const dnssFlags = {
+ "bypass_cache": Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
+ "canonical_name": Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ "priority_medium": Ci.nsIDNSService.RESOLVE_PRIORITY_MEDIUM,
+ "priority_low": Ci.nsIDNSService.RESOLVE_PRIORITY_LOW,
+ "speculate": Ci.nsIDNSService.RESOLVE_SPECULATE,
+ "disable_ipv6": Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ "disable_ipv4": Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ "offline": Ci.nsIDNSService.RESOLVE_OFFLINE,
+ "allow_name_collisions": Ci.nsIDNSService.RESOLVE_ALLOW_NAME_COLLISION,
+ "disable_trr": Ci.nsIDNSService.RESOLVE_DISABLE_TRR,
+};
+
+function getErrorString(nsresult) {
+ try {
+ throw Components.Exception("", nsresult);
+ } catch (e) {
+ let name = e.name;
+ let message = "DNS resolve failure";
+ let match = name.match(/NS_ERROR_(.*)/);
+ if (match && match.length > 1) {
+ name = match[1];
+ }
+ match = e.toString().match(/\"(.*?)\"/);
+ if (match && match.length > 1) {
+ message = match[1];
+ }
+ return `${name}: ${message}`;
+ }
+}
+
+this.dns = class extends ExtensionAPI {
+ getAPI(context) {
+ const dnss = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+ return {
+ dns: {
+ resolve: function(hostname, flags) {
+ let dnsFlags = 0;
+ flags = flags || [];
+ for (let flag of flags) {
+ dnsFlags |= dnssFlags[flag];
+ }
+
+ return new Promise((resolve, reject) => {
+ let request;
+ let response = {
+ addresses: [],
+ };
+ let listener = (inRequest, inRecord, inStatus) => {
+ if (inRequest === request) {
+ if (!Components.isSuccessCode(inStatus)) {
+ return reject({message: getErrorString(inStatus)});
+ }
+ if (flags.includes("canonical_name")) {
+ try {
+ response.canonicalName = inRecord.canonicalName;
+ } catch (e) {
+ // no canonicalName
+ }
+ }
+ response.isTRR = inRecord.IsTRR();
+ while (true) {
+ try {
+ response.addresses.push(inRecord.getNextAddrAsString());
+ } catch (e) {
+ return resolve(response);
+ }
+ }
+ }
+ };
+ request = dnss.asyncResolve(hostname, dnsFlags, listener, null, {} /* defaultOriginAttributes */);
+ });
+ },
+ },
+ };
+ }
+};
--- a/toolkit/components/extensions/ext-toolkit.json
+++ b/toolkit/components/extensions/ext-toolkit.json
@@ -53,16 +53,24 @@
"cookies": {
"url": "chrome://extensions/content/ext-cookies.js",
"schema": "chrome://extensions/content/schemas/cookies.json",
"scopes": ["addon_parent"],
"paths": [
["cookies"]
]
},
+ "dns": {
+ "url": "chrome://extensions/content/ext-dns.js",
+ "schema": "chrome://extensions/content/schemas/dns.json",
+ "scopes": ["addon_parent"],
+ "paths": [
+ ["dns"]
+ ]
+ },
"downloads": {
"url": "chrome://extensions/content/ext-downloads.js",
"schema": "chrome://extensions/content/schemas/downloads.json",
"scopes": ["addon_parent"],
"paths": [
["downloads"]
]
},
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -8,16 +8,17 @@ toolkit.jar:
content/extensions/ext-alarms.js
content/extensions/ext-backgroundPage.js
content/extensions/ext-browser-content.js
content/extensions/ext-browserSettings.js
content/extensions/ext-contentScripts.js
content/extensions/ext-contextualIdentities.js
content/extensions/ext-clipboard.js
content/extensions/ext-cookies.js
+ content/extensions/ext-dns.js
content/extensions/ext-downloads.js
content/extensions/ext-extension.js
content/extensions/ext-i18n.js
#ifndef ANDROID
content/extensions/ext-identity.js
#endif
content/extensions/ext-idle.js
content/extensions/ext-management.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/dns.json
@@ -0,0 +1,68 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "dns"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "dns",
+ "description": "Asynchronous DNS API",
+ "permissions": ["dns"],
+ "types": [
+ {
+ "id": "DNSRecord",
+ "type": "object",
+ "description": "An object encapsulating a DNS Record.",
+ "properties": {
+ "canonicalName": {
+ "type": "string",
+ "description": "The canonical hostname for this record. this value is empty if the record was not fetched with the 'canonical_name' flag."
+ },
+ "isTRR": {
+ "type": "string",
+ "description": "Record retreived with TRR."
+ },
+ "addresses": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "resolve",
+ "type": "function",
+ "description": "Resolves a hostname to a DNS entry.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "hostname",
+ "type": "string"
+ },
+ {
+ "name": "flags",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "bypass_cache", "canonical_name", "priority_medium", "priority_low", "speculate",
+ "disable_ipv6", "disable_ipv4", "offline", "allow_name_collisions", "disable_trr"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -5,16 +5,17 @@
toolkit.jar:
% content extensions %content/extensions/
content/extensions/schemas/alarms.json
content/extensions/schemas/browser_settings.json
content/extensions/schemas/clipboard.json
content/extensions/schemas/content_scripts.json
content/extensions/schemas/contextual_identities.json
content/extensions/schemas/cookies.json
+ content/extensions/schemas/dns.json
content/extensions/schemas/downloads.json
content/extensions/schemas/events.json
content/extensions/schemas/experiments.json
content/extensions/schemas/extension.json
content/extensions/schemas/extension_types.json
content/extensions/schemas/extension_protocol_handlers.json
content/extensions/schemas/i18n.json
#ifndef ANDROID
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dns.js
@@ -0,0 +1,100 @@
+"use strict";
+
+function getExtension(background = undefined) {
+ let manifest = {
+ "permissions": [
+ "dns",
+ ],
+ };
+ return ExtensionTestUtils.loadExtension({
+ manifest,
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ browser.dns.resolve(data.hostname, data.flags).then(result => {
+ browser.test.sendMessage("resolved", result);
+ }).catch(e => {
+ browser.test.sendMessage("resolved", {message: e.message});
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+}
+
+const tests = [
+ {
+ request: {
+ hostname: "localhost",
+ },
+ expect: {
+ addresses: ["127.0.0.1", "::1"],
+ },
+ },
+ {
+ request: {
+ hostname: "localhost",
+ flags: ["offline"],
+ },
+ expect: {
+ addresses: ["127.0.0.1", "::1"],
+ },
+ },
+ {
+ request: {
+ hostname: "test.example",
+ },
+ expect: {
+ error: /UNKNOWN_HOST/,
+ },
+ },
+ {
+ request: {
+ hostname: "127.0.0.1",
+ flags: ["canonical_name"],
+ },
+ expect: {
+ canonicalName: "127.0.0.1",
+ addresses: ["127.0.0.1"],
+ },
+ },
+ {
+ request: {
+ hostname: "localhost",
+ flags: ["disable_ipv4"],
+ },
+ expect: {
+ addresses: ["::1"],
+ },
+ },
+ {
+ request: {
+ hostname: "localhost",
+ flags: ["disable_ipv6"],
+ },
+ expect: {
+ addresses: ["127.0.0.1"],
+ },
+ },
+];
+
+add_task(async function test_dns_resolve() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let test of tests) {
+ extension.sendMessage("resolve", test.request);
+ let result = await extension.awaitMessage("resolved");
+ if (test.expect.error) {
+ ok(test.expect.error.test(result.message), `expected error ${result.message}`);
+ } else {
+ equal(result.canonicalName, test.expect.canonicalName, "canonicalName match");
+ equal(result.addresses.length, test.expect.addresses.length, "expected number of addresses returned");
+ for (let addr of test.expect.addresses) {
+ ok(result.addresses.includes(addr), `expected ip match`);
+ }
+ }
+ }
+
+ await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -15,16 +15,17 @@ skip-if = os == "android" # Android does
skip-if = os == "android"
[test_ext_browserSettings.js]
[test_ext_browserSettings_homepage.js]
skip-if = os == "android"
[test_ext_cookieBehaviors.js]
[test_ext_contextual_identities.js]
skip-if = os == "android" # Containers are not exposed to android.
[test_ext_debugging_utils.js]
+[test_ext_dns.js]
[test_ext_downloads.js]
[test_ext_downloads_download.js]
skip-if = os == "android"
[test_ext_downloads_misc.js]
skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870
[test_ext_downloads_private.js]
skip-if = os == "android"
[test_ext_downloads_search.js]