Bug 1373640 implement async dns resolve api for webextensions, r=kmag
MozReview-Commit-ID: Bzfr2x6Vmx2
--- 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,70 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const dnssFlags = {
+ "allow_name_collisions": Ci.nsIDNSService.RESOLVE_ALLOW_NAME_COLLISION,
+ "bypass_cache": Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
+ "canonical_name": Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ "disable_ipv4": Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ "disable_ipv6": Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ "disable_trr": Ci.nsIDNSService.RESOLVE_DISABLE_TRR,
+ "offline": Ci.nsIDNSService.RESOLVE_OFFLINE,
+ "priority_low": Ci.nsIDNSService.RESOLVE_PRIORITY_LOW,
+ "priority_medium": Ci.nsIDNSService.RESOLVE_PRIORITY_MEDIUM,
+ "speculate": Ci.nsIDNSService.RESOLVE_SPECULATE,
+};
+
+function getErrorString(nsresult) {
+ let e = new Components.Exception("", nsresult);
+ return e.name;
+}
+
+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 = flags.reduce((mask, flag) => mask | dnssFlags[flag], 0);
+
+ 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 (dnsFlags & Ci.nsIDNSService.RESOLVE_CANONICAL_NAME) {
+ try {
+ response.canonicalName = inRecord.canonicalName;
+ } catch (e) {
+ // no canonicalName
+ }
+ }
+ response.isTRR = inRecord.IsTRR();
+ while (inRecord.hasMore()) {
+ let addr = inRecord.getNextAddrAsString();
+ // Sometimes there are duplicate records with the same ip.
+ if (!response.addresses.includes(addr)) {
+ response.addresses.push(addr);
+ }
+ }
+ return resolve(response);
+ }
+ };
+ try {
+ request = dnss.asyncResolve(hostname, dnsFlags, listener, null, {} /* defaultOriginAttributes */);
+ } catch (e) {
+ // handle exceptions such as offline mode.
+ return reject({message: e.name});
+ }
+ });
+ },
+ },
+ };
+ }
+};
--- 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,82 @@
+[
+ {
+ "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",
+ "optional": true,
+ "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" }
+ }
+ }
+ },
+ {
+ "id": "ResolveFlags",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "allow_name_collisions",
+ "bypass_cache",
+ "canonical_name",
+ "disable_ipv4",
+ "disable_ipv6",
+ "disable_trr",
+ "offline",
+ "priority_low",
+ "priority_medium",
+ "speculate"
+ ]
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "resolve",
+ "type": "function",
+ "description": "Resolves a hostname to a DNS record.",
+ "async": true,
+ "parameters": [
+ {
+ "name": "hostname",
+ "type": "string"
+ },
+ {
+ "name": "flags",
+ "optional": true,
+ "default": [],
+ "$ref": "ResolveFlags"
+ }
+ ]
+ }
+ ]
+ }
+]
--- 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,103 @@
+"use strict";
+
+// Some test machines and android are not returing ipv6, turn it
+// off to get consistent test results.
+Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+
+function getExtension(background = undefined) {
+ let manifest = {
+ "permissions": [
+ "dns",
+ ],
+ };
+ return ExtensionTestUtils.loadExtension({
+ manifest,
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ browser.test.log(`=== dns resolve test ${JSON.stringify(data)}`);
+ browser.dns.resolve(data.hostname, data.flags).then(result => {
+ browser.test.log(`=== dns resolve result ${JSON.stringify(result)}`);
+ browser.test.sendMessage("resolved", result);
+ }).catch(e => {
+ browser.test.log(`=== dns resolve error ${e.message}`);
+ browser.test.sendMessage("resolved", {message: e.message});
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+}
+
+const tests = [
+ {
+ request: {
+ hostname: "localhost",
+ },
+ expect: {
+ addresses: ["127.0.0.1"], // ipv6 disabled , "::1"
+ },
+ },
+ {
+ request: {
+ hostname: "localhost",
+ flags: ["offline"],
+ },
+ expect: {
+ addresses: ["127.0.0.1"], // ipv6 disabled , "::1"
+ },
+ },
+ {
+ request: {
+ hostname: "test.example",
+ },
+ expect: {
+ // android will error with offline
+ error: /NS_ERROR_UNKNOWN_HOST|NS_ERROR_OFFLINE/,
+ },
+ },
+ {
+ 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_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");
+ // It seems there are platform differences happening that make this
+ // testing difficult. We're going to rely on other existing dns tests to validate
+ // the dns service itself works and only validate that we're getting generally
+ // expected results in the webext api.
+ ok(result.addresses.length >= test.expect.addresses.length, "expected number of addresses returned");
+ if (test.expect.addresses.length > 0 && result.addresses.length > 0) {
+ ok(result.addresses.includes(test.expect.addresses[0]), "got expected ip address");
+ }
+ }
+ }
+
+ 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]