Bug 1373640 implement async dns resolve api for webextensions, r?kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 22 Feb 2018 16:39:28 -0600
changeset 758642 160b0fe08e3ce23cd57ab99e3055403314d18e1c
parent 758497 5e9bd04333f20e00911b8c1dfbf2b2e070c61e2d
push id100151
push usermixedpuppy@gmail.com
push dateThu, 22 Feb 2018 22:40:04 +0000
reviewerskmag
bugs1373640
milestone60.0a1
Bug 1373640 implement async dns resolve api for webextensions, r?kmag MozReview-Commit-ID: 9lBEfVMUb3P
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,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]