Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / netwerk / test / unit / head_trr.js
bloba66cdbc9a015047ac194f3542f8d6711b2ff6e17
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 /* import-globals-from head_cache.js */
8 /* import-globals-from head_cookies.js */
9 /* import-globals-from head_channels.js */
10 /* import-globals-from head_servers.js */
12 /* globals require, __dirname, global, Buffer, process */
14 /// Sets the TRR related prefs and adds the certificate we use for the HTTP2
15 /// server.
16 function trr_test_setup() {
17 dump("start!\n");
19 let h2Port = Services.env.get("MOZHTTP2_PORT");
20 Assert.notEqual(h2Port, null);
21 Assert.notEqual(h2Port, "");
23 // Set to allow the cert presented by our H2 server
24 do_get_profile();
26 Services.prefs.setBoolPref("network.http.http2.enabled", true);
27 // the TRR server is on 127.0.0.1
28 if (AppConstants.platform == "android") {
29 Services.prefs.setCharPref("network.trr.bootstrapAddr", "10.0.2.2");
30 } else {
31 Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
34 // make all native resolve calls "secretly" resolve localhost instead
35 Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
37 Services.prefs.setBoolPref("network.trr.wait-for-portal", false);
38 // don't confirm that TRR is working, just go!
39 Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
40 // some tests rely on the cache not being cleared on pref change.
41 // we specifically test that this works
42 Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
44 // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
45 // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
46 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
47 Ci.nsIX509CertDB
49 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
51 // Turn off strict fallback mode and TRR retry for most tests,
52 // it is tested specifically.
53 Services.prefs.setBoolPref("network.trr.strict_native_fallback", false);
54 Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false);
56 // Turn off temp blocklist feature in tests. When enabled we may issue a
57 // lookup to resolve a parent name when blocklisting, which may bleed into
58 // and interfere with subsequent tasks.
59 Services.prefs.setBoolPref("network.trr.temp_blocklist", false);
61 // We intentionally don't set the TRR mode. Each test should set it
62 // after setup in the first test.
64 return h2Port;
67 /// Clears the prefs that we're likely to set while testing TRR code
68 function trr_clear_prefs() {
69 Services.prefs.clearUserPref("network.trr.mode");
70 Services.prefs.clearUserPref("network.trr.uri");
71 Services.prefs.clearUserPref("network.trr.credentials");
72 Services.prefs.clearUserPref("network.trr.wait-for-portal");
73 Services.prefs.clearUserPref("network.trr.allow-rfc1918");
74 Services.prefs.clearUserPref("network.trr.useGET");
75 Services.prefs.clearUserPref("network.trr.confirmationNS");
76 Services.prefs.clearUserPref("network.trr.bootstrapAddr");
77 Services.prefs.clearUserPref("network.trr.temp_blocklist_duration_sec");
78 Services.prefs.clearUserPref("network.trr.request_timeout_ms");
79 Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
80 Services.prefs.clearUserPref("network.trr.disable-ECS");
81 Services.prefs.clearUserPref("network.trr.early-AAAA");
82 Services.prefs.clearUserPref("network.trr.excluded-domains");
83 Services.prefs.clearUserPref("network.trr.builtin-excluded-domains");
84 Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change");
85 Services.prefs.clearUserPref("captivedetect.canonicalURL");
87 Services.prefs.clearUserPref("network.http.http2.enabled");
88 Services.prefs.clearUserPref("network.dns.localDomains");
89 Services.prefs.clearUserPref("network.dns.native-is-localhost");
90 Services.prefs.clearUserPref(
91 "network.trr.send_empty_accept-encoding_headers"
93 Services.prefs.clearUserPref("network.trr.strict_native_fallback");
94 Services.prefs.clearUserPref("network.trr.temp_blocklist");
97 /// This class sends a DNS query and can be awaited as a promise to get the
98 /// response.
99 class TRRDNSListener {
100 constructor(...args) {
101 if (args.length < 2) {
102 Assert.ok(false, "TRRDNSListener requires at least two arguments");
104 this.name = args[0];
105 if (typeof args[1] == "object") {
106 this.options = args[1];
107 } else {
108 this.options = {
109 expectedAnswer: args[1],
110 expectedSuccess: args[2] ?? true,
111 delay: args[3],
112 trrServer: args[4] ?? "",
113 expectEarlyFail: args[5] ?? "",
114 flags: args[6] ?? 0,
115 type: args[7] ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
116 port: args[8] ?? -1,
119 this.expectedAnswer = this.options.expectedAnswer ?? undefined;
120 this.expectedSuccess = this.options.expectedSuccess ?? true;
121 this.delay = this.options.delay;
122 this.promise = new Promise(resolve => {
123 this.resolve = resolve;
125 this.type = this.options.type ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT;
126 let trrServer = this.options.trrServer || "";
127 let port = this.options.port || -1;
129 // This may be called in a child process that doesn't have Services available.
130 // eslint-disable-next-line mozilla/use-services
131 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(
132 Ci.nsIThreadManager
134 const currentThread = threadManager.currentThread;
136 this.additionalInfo =
137 trrServer == "" && port == -1
138 ? null
139 : Services.dns.newAdditionalInfo(trrServer, port);
140 try {
141 this.request = Services.dns.asyncResolve(
142 this.name,
143 this.type,
144 this.options.flags || 0,
145 this.additionalInfo,
146 this,
147 currentThread,
148 this.options.originAttributes || {} // defaultOriginAttributes
150 Assert.ok(!this.options.expectEarlyFail, "asyncResolve ok");
151 } catch (e) {
152 Assert.ok(this.options.expectEarlyFail, "asyncResolve fail");
153 this.resolve({ error: e });
157 onLookupComplete(inRequest, inRecord, inStatus) {
158 Assert.ok(
159 inRequest == this.request,
160 "Checking that this is the correct callback"
163 // If we don't expect success here, just resolve and the caller will
164 // decide what to do with the results.
165 if (!this.expectedSuccess) {
166 this.resolve({ inRequest, inRecord, inStatus });
167 return;
170 Assert.equal(inStatus, Cr.NS_OK, "Checking status");
172 if (this.type != Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT) {
173 this.resolve({ inRequest, inRecord, inStatus });
174 return;
177 inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
178 let answer = inRecord.getNextAddrAsString();
179 Assert.equal(
180 answer,
181 this.expectedAnswer,
182 `Checking result for ${this.name}`
184 inRecord.rewind(); // In case the caller also checks the addresses
186 if (this.delay !== undefined) {
187 Assert.greaterOrEqual(
188 inRecord.trrFetchDurationNetworkOnly,
189 this.delay,
190 `the response should take at least ${this.delay}`
193 Assert.greaterOrEqual(
194 inRecord.trrFetchDuration,
195 this.delay,
196 `the response should take at least ${this.delay}`
199 if (this.delay == 0) {
200 // The response timing should be really 0
201 Assert.equal(
202 inRecord.trrFetchDurationNetworkOnly,
204 `the response time should be 0`
207 Assert.equal(
208 inRecord.trrFetchDuration,
209 this.delay,
210 `the response time should be 0`
215 this.resolve({ inRequest, inRecord, inStatus });
218 QueryInterface(aIID) {
219 if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) {
220 return this;
222 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
225 // Implement then so we can await this as a promise.
226 then() {
227 return this.promise.then.apply(this.promise, arguments);
230 cancel(aStatus = Cr.NS_ERROR_ABORT) {
231 Services.dns.cancelAsyncResolve(
232 this.name,
233 this.type,
234 this.options.flags || 0,
235 this.resolverInfo,
236 this,
237 aStatus,
243 // This is for reteriiving the raw bytes from a DNS answer.
244 function answerHandler(req, resp) {
245 let searchParams = new URL(req.url, "http://example.com").searchParams;
246 console.log("req.searchParams:" + searchParams);
247 if (!searchParams.get("host")) {
248 resp.writeHead(400);
249 resp.end("Missing search parameter");
250 return;
253 function processRequest(req1, resp1) {
254 let domain = searchParams.get("host");
255 let type = searchParams.get("type");
256 let response = global.dns_query_answers[`${domain}/${type}`] || {};
257 let buf = global.dnsPacket.encode({
258 type: "response",
259 id: 0,
260 flags: 0,
261 questions: [],
262 answers: response.answers || [],
263 additionals: response.additionals || [],
265 let writeResponse = (resp2, buf2) => {
266 try {
267 let data = buf2.toString("hex");
268 resp2.setHeader("Content-Length", data.length);
269 resp2.writeHead(200, { "Content-Type": "plain/text" });
270 resp2.write(data);
271 resp2.end("");
272 } catch (e) {}
275 writeResponse(resp1, buf, response);
278 processRequest(req, resp);
281 /// This is the default handler for /dns-query
282 /// It implements basic functionality for parsing the DoH packet, then
283 /// queries global.dns_query_answers for available answers for the DNS query.
284 function trrQueryHandler(req, resp, url) {
285 let requestBody = Buffer.from("");
286 let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD];
287 let contentLength = req.headers["content-length"];
289 if (method == "POST") {
290 req.on("data", chunk => {
291 requestBody = Buffer.concat([requestBody, chunk]);
292 if (requestBody.length == contentLength) {
293 processRequest(req, resp, requestBody);
296 } else if (method == "GET") {
297 if (!url.query.dns) {
298 resp.writeHead(400);
299 resp.end("Missing dns parameter");
300 return;
303 requestBody = Buffer.from(url.query.dns, "base64");
304 processRequest(req, resp, requestBody);
305 } else {
306 // unexpected method.
307 resp.writeHead(405);
308 resp.end("Unexpected method");
311 function processRequest(req1, resp1, payload) {
312 let dnsQuery = global.dnsPacket.decode(payload);
313 let domain = dnsQuery.questions[0].name;
314 let type = dnsQuery.questions[0].type;
315 let response = global.dns_query_answers[`${domain}/${type}`] || {};
317 if (!global.dns_query_counts[domain]) {
318 global.dns_query_counts[domain] = {};
320 global.dns_query_counts[domain][type] =
321 global.dns_query_counts[domain][type] + 1 || 1;
323 let flags = global.dnsPacket.RECURSION_DESIRED;
324 if (!response.answers && !response.flags) {
325 flags |= 2; // SERVFAIL
327 flags |= response.flags || 0;
328 let buf = global.dnsPacket.encode({
329 type: "response",
330 id: dnsQuery.id,
331 flags,
332 questions: dnsQuery.questions,
333 answers: response.answers || [],
334 additionals: response.additionals || [],
337 let writeResponse = (resp2, buf2, context) => {
338 try {
339 if (context.error) {
340 // If the error is a valid HTTP response number just write it out.
341 if (context.error < 600) {
342 resp2.writeHead(context.error);
343 resp2.end("Intentional error");
344 return;
347 // Bigger error means force close the session
348 req1.stream.session.close();
349 return;
351 resp2.setHeader("Content-Length", buf2.length);
352 resp2.writeHead(200, { "Content-Type": "application/dns-message" });
353 resp2.write(buf2);
354 resp2.end("");
355 } catch (e) {}
358 if (response.delay) {
359 // This function is handled within the httpserver where setTimeout is
360 // available.
361 // eslint-disable-next-line no-undef
362 setTimeout(
363 arg => {
364 writeResponse(arg[0], arg[1], arg[2]);
366 response.delay,
367 [resp1, buf, response]
369 return;
372 writeResponse(resp1, buf, response);
376 function getRequestCount(domain, type) {
377 if (!global.dns_query_counts[domain]) {
378 return 0;
380 return global.dns_query_counts[domain][type] || 0;
383 // A convenient wrapper around NodeServer
384 class TRRServer extends NodeHTTP2Server {
385 /// Starts the server
386 /// @port - default 0
387 /// when provided, will attempt to listen on that port.
388 async start(port = 0) {
389 await super.start(port);
390 await this.execute(`( () => {
391 // key: string "name/type"
392 // value: array [answer1, answer2]
393 global.dns_query_answers = {};
395 // key: domain
396 // value: a map containing {key: type, value: number of requests}
397 global.dns_query_counts = {};
399 global.dnsPacket = require(\`\${__dirname}/../dns-packet\`);
400 global.ip = require(\`\${__dirname}/../node_ip\`);
401 global.http2 = require("http2");
402 })()`);
403 await this.registerPathHandler("/dns-query", trrQueryHandler);
404 await this.registerPathHandler("/dnsAnswer", answerHandler);
405 await this.execute(getRequestCount);
408 /// @name : string - name we're providing answers for. eg: foo.example.com
409 /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc
410 /// @response : a map containing the response
411 /// answers: array of answers (hashmap) that dnsPacket can parse
412 /// eg: [{
413 /// name: "bar.example.com",
414 /// ttl: 55,
415 /// type: "A",
416 /// flush: false,
417 /// data: "1.2.3.4",
418 /// }]
419 /// additionals - array of answers (hashmap) to be added to the additional section
420 /// delay: int - if not 0 the response will be sent with after `delay` ms.
421 /// flags: int - flags to be set on the answer
422 /// error: int - HTTP status. If truthy then the response will send this status
423 async registerDoHAnswers(name, type, response = {}) {
424 let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify(
425 response
426 )}`;
427 return this.execute(text);
430 async requestCount(domain, type) {
431 return this.execute(`getRequestCount("${domain}", "${type}")`);
435 // Implements a basic HTTP2 proxy server
436 class TRRProxyCode {
437 static async startServer(endServerPort) {
438 const fs = require("fs");
439 const options = {
440 key: fs.readFileSync(__dirname + "/http2-cert.key"),
441 cert: fs.readFileSync(__dirname + "/http2-cert.pem"),
444 const http2 = require("http2");
445 global.proxy = http2.createSecureServer(options);
446 this.setupProxy();
447 global.endServerPort = endServerPort;
449 await global.proxy.listen(0);
451 let serverPort = global.proxy.address().port;
452 return serverPort;
455 static closeProxy() {
456 global.proxy.closeSockets();
457 return new Promise(resolve => {
458 global.proxy.close(resolve);
462 static proxyRequestCount() {
463 return global.proxy_stream_count;
466 static setupProxy() {
467 if (!global.proxy) {
468 throw new Error("proxy is null");
471 global.proxy_stream_count = 0;
473 // We need to track active connections so we can forcefully close keep-alive
474 // connections when shutting down the proxy.
475 global.proxy.socketIndex = 0;
476 global.proxy.socketMap = {};
477 global.proxy.on("connection", function (socket) {
478 let index = global.proxy.socketIndex++;
479 global.proxy.socketMap[index] = socket;
480 socket.on("close", function () {
481 delete global.proxy.socketMap[index];
484 global.proxy.closeSockets = function () {
485 for (let i in global.proxy.socketMap) {
486 global.proxy.socketMap[i].destroy();
490 global.proxy.on("stream", (stream, headers) => {
491 if (headers[":method"] !== "CONNECT") {
492 // Only accept CONNECT requests
493 stream.respond({ ":status": 405 });
494 stream.end();
495 return;
497 global.proxy_stream_count++;
498 const net = require("net");
499 const socket = net.connect(global.endServerPort, "127.0.0.1", () => {
500 try {
501 stream.respond({ ":status": 200 });
502 socket.pipe(stream);
503 stream.pipe(socket);
504 } catch (exception) {
505 console.log(exception);
506 stream.close();
509 socket.on("error", error => {
510 throw new Error(
511 `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`
518 class TRRProxy {
519 // Starts the proxy
520 async start(port) {
521 info("TRRProxy start!");
522 this.processId = await NodeServer.fork();
523 info("processid=" + this.processId);
524 await this.execute(TRRProxyCode);
525 this.port = await this.execute(`TRRProxyCode.startServer(${port})`);
526 Assert.notEqual(this.port, null);
529 // Executes a command in the context of the node server
530 async execute(command) {
531 return NodeServer.execute(this.processId, command);
534 // Stops the server
535 async stop() {
536 if (this.processId) {
537 await NodeServer.execute(this.processId, `TRRProxyCode.closeProxy()`);
538 await NodeServer.kill(this.processId);
542 async request_count() {
543 let data = await NodeServer.execute(
544 this.processId,
545 `TRRProxyCode.proxyRequestCount()`
547 return parseInt(data);