Bug 1373640 implement async dns resolve api for webextensions, r=kmag
authorShane Caraveo <scaraveo@mozilla.com>
Tue, 27 Feb 2018 19:35:01 -0600
changeset 405760 998ff573d5cd7597451cbc864c6e34ccc414bc9c
parent 405759 76625cc640b4d3f65d95ec84836f508b12c627f1
child 405761 8b1656b26cb22818f2bf715c6a0f57f93be836ab
push id60349
push usermixedpuppy@gmail.com
push dateWed, 28 Feb 2018 20:14:23 +0000
treeherderautoland@998ff573d5cd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1373640
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1373640 implement async dns resolve api for webextensions, r=kmag MozReview-Commit-ID: Bzfr2x6Vmx2
browser/locales/en-US/chrome/browser/browser.properties
toolkit/components/extensions/ext-dns.js
toolkit/components/extensions/ext-toolkit.json
toolkit/components/extensions/jar.mn
toolkit/components/extensions/schemas/dns.json
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/test/xpcshell/test_ext_dns.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- 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]