diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js new file mode 100644 index 000000000000..f3c2155093e4 --- /dev/null +++ b/netwerk/test/unit/head_trr.js @@ -0,0 +1,343 @@ +/* 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/. */ + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ + +/* globals require, __dirname, global, Buffer */ + +const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/// Sets the TRR related prefs and adds the certificate we use for the HTTP2 +/// server. +function trr_test_setup() { + dump("start!\n"); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.spdy.enabled", true); + Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // use the h2 server as DOH provider + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - reserved, 2 - TRR first, 3 - TRR only, 4 - reserved + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + Services.prefs.setBoolPref("network.trr.wait-for-portal", false); + // By default wait for all responses before notifying the listeners. + Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true); + // don't confirm that TRR is working, just go! + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + // some tests rely on the cache not being cleared on pref change. + // we specifically test that this works + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +/// Clears the prefs that we're likely to set while testing TRR code +function trr_clear_prefs() { + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("network.trr.credentials"); + Services.prefs.clearUserPref("network.trr.wait-for-portal"); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.confirmationNS"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + Services.prefs.clearUserPref("network.trr.blacklist-duration"); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.clearUserPref("network.trr.early-AAAA"); + Services.prefs.clearUserPref("network.trr.skip-AAAA-when-not-supported"); + Services.prefs.clearUserPref("network.trr.wait-for-A-and-AAAA"); + Services.prefs.clearUserPref("network.trr.excluded-domains"); + Services.prefs.clearUserPref("network.trr.builtin-excluded-domains"); + Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + Services.prefs.clearUserPref("network.http.spdy.enabled"); + Services.prefs.clearUserPref("network.http.spdy.enabled.http2"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + Services.prefs.clearUserPref( + "network.trr.send_empty_accept-encoding_headers" + ); +} + +/// This class sends a DNS query and can be awaited as a promise to get the +/// response. +class TRRDNSListener { + constructor( + name, + expectedAnswer, + expectedSuccess = true, + delay, + trrServer = "", + expectEarlyFail = false + ) { + this.name = name; + this.expectedAnswer = expectedAnswer; + this.expectedSuccess = expectedSuccess; + this.delay = delay; + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + + const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService + ); + + if (trrServer == "") { + this.request = dns.asyncResolve( + name, + 0, + this, + Services.tm.currentThread, + {} // defaultOriginAttributes + ); + } else { + try { + this.request = dns.asyncResolveWithTrrServer( + name, + trrServer, + 0, + this, + Services.tm.currentThread, + {} // defaultOriginAttributes + ); + Assert.ok(!expectEarlyFail); + } catch (e) { + Assert.ok(expectEarlyFail); + this.resolve([e]); + } + } + } + + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok( + inRequest == this.request, + "Checking that this is the correct callback" + ); + + // If we don't expect success here, just resolve and the caller will + // decide what to do with the results. + if (!this.expectedSuccess) { + this.resolve([inRequest, inRecord, inStatus]); + return; + } + + Assert.equal(inStatus, Cr.NS_OK, "Checking status"); + let answer = inRecord.getNextAddrAsString(); + Assert.equal( + answer, + this.expectedAnswer, + `Checking result for ${this.name}` + ); + + if (this.delay !== undefined) { + Assert.greaterOrEqual( + inRecord.trrFetchDurationNetworkOnly, + this.delay, + `the response should take at least ${this.delay}` + ); + + Assert.greaterOrEqual( + inRecord.trrFetchDuration, + this.delay, + `the response should take at least ${this.delay}` + ); + + if (this.delay == 0) { + // The response timing should be really 0 + Assert.equal( + inRecord.trrFetchDurationNetworkOnly, + 0, + `the response time should be 0` + ); + + Assert.equal( + inRecord.trrFetchDuration, + this.delay, + `the response time should be 0` + ); + } + } + + this.resolve([inRequest, inRecord, inStatus]); + } + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + // Implement then so we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +/// Implements a basic HTTP2 server +class TRRServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + + const url = require("url"); + global.path_handlers = {}; + global.handler = (req, resp) => { + const path = req.headers[global.http2.constants.HTTP2_HEADER_PATH]; + let u = url.parse(req.url, true); + let handler = global.path_handlers[u.pathname]; + if (handler) { + return handler(req, resp, u); + } + + // Didn't find a handler for this path. + let response = `

404 Path not found: ${path}

`; + resp.setHeader("Content-Type", "text/html"); + resp.setHeader("Content-Length", response.length); + resp.writeHead(404); + resp.end(response); + }; + + // key: string "name/type" + // value: array [answer1, answer2] + global.dns_query_answers = {}; + + global.http2 = require("http2"); + global.server = global.http2.createSecureServer(options, global.handler); + + await global.server.listen(port); + + global.dnsPacket = require(`${__dirname}/../dns-packet`); + global.ip = require(`${__dirname}/../node-ip`); + + return global.server.address().port; + } +} + +/// This is the default handler for /dns-query +/// It implements basic functionality for parsing the DoH packet, then +/// queries global.dns_query_answers for available answers for the DNS query. +function trrQueryHandler(req, resp, url) { + let requestBody = Buffer.from(""); + let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD]; + + if (method == "POST") { + req + .on("data", chunk => { + requestBody = Buffer.concat([requestBody, chunk]); + }) + .on("end", () => { + return processRequest(req, resp, requestBody); + }); + } else if (method == "GET") { + if (!url.query.dns) { + resp.writeHead(400); + resp.end("Missing dns parameter"); + return; + } + + requestBody = Buffer.from(url.query.dns, "base64"); + return processRequest(req, resp, requestBody); + } else { + // unexpected method. + resp.writeHead(405); + resp.end("Unexpected method"); + } + + function processRequest(req, resp, payload) { + let dnsQuery = global.dnsPacket.decode(payload); + let answers = + global.dns_query_answers[ + `${dnsQuery.questions[0].name}/${dnsQuery.questions[0].type}` + ] || []; + + let buf = global.dnsPacket.encode({ + type: "response", + id: dnsQuery.id, + flags: global.dnsPacket.RECURSION_DESIRED, + questions: dnsQuery.questions, + answers, + }); + + resp.setHeader("Content-Length", buf.length); + resp.writeHead(200, { "Content-Type": "application/dns-message" }); + resp.write(buf); + resp.end(""); + } +} + +// A convenient wrapper around NodeServer +class TRRServer { + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(TRRServerCode); + this.port = await this.execute(`TRRServerCode.startServer(${port})`); + await this.registerPathHandler("/dns-query", trrQueryHandler); + } + + /// Executes a command in the context of the node server + async execute(command) { + return NodeServer.execute(this.processId, command); + } + + /// Stops the server + async stop() { + if (this.processId) { + await NodeServer.kill(this.processId); + this.processId = undefined; + } + } + + /// @path : string - the path on the server that we're handling. ex: /path + /// @handler : function(req, resp, url) - function that processes request and + /// emits a response. + async registerPathHandler(path, handler) { + return this.execute( + `global.path_handlers["${path}"] = ${handler.toString()}` + ); + } + + /// @name : string - name we're providing answers for. eg: foo.example.com + /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc + /// @answers : array - array of answers (hashmap) that dnsPacket can parse + /// eg: [{ + /// name: "bar.example.com", + /// ttl: 55, + /// type: "A", + /// flush: false, + /// data: "1.2.3.4", + /// }] + async registerDoHAnswers(name, type, answers) { + let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify( + answers + )}`; + return this.execute(text); + } +} diff --git a/netwerk/test/unit/test_http2-proxy.js b/netwerk/test/unit/test_http2-proxy.js index 60a19129a3ed..f6fafe8f49fb 100644 --- a/netwerk/test/unit/test_http2-proxy.js +++ b/netwerk/test/unit/test_http2-proxy.js @@ -27,7 +27,6 @@ "use strict"; const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); -const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js"); let proxy_port; let filter; diff --git a/netwerk/test/unit/test_node_execute.js b/netwerk/test/unit/test_node_execute.js index 4b51f9e43750..3640514a8e7e 100644 --- a/netwerk/test/unit/test_node_execute.js +++ b/netwerk/test/unit/test_node_execute.js @@ -5,8 +5,6 @@ "use strict"; -const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js"); - add_task(async function test_execute() { function f() { return "bla"; diff --git a/netwerk/test/unit/test_trr.js b/netwerk/test/unit/test_trr.js index 93e4aaa73861..c9e42e201269 100644 --- a/netwerk/test/unit/test_trr.js +++ b/netwerk/test/unit/test_trr.js @@ -1,8 +1,6 @@ "use strict"; -const { NodeServer, HttpServer } = ChromeUtils.import( - "resource://testing-common/httpd.js" -); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); const dns = Cc["@mozilla.org/network/dns-service;1"].getService( Ci.nsIDNSService ); diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 7be65c342ad5..382a58707ba3 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -1,5 +1,5 @@ [DEFAULT] -head = head_channels.js head_cache.js head_cache2.js head_cookies.js +head = head_channels.js head_cache.js head_cache2.js head_cookies.js head_trr.js support-files = http2-ca.pem client_cert_chooser.js