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/. */
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
16 function trr_test_setup() {
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
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");
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(
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.
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
99 class TRRDNSListener
{
100 constructor(...args
) {
101 if (args
.length
< 2) {
102 Assert
.ok(false, "TRRDNSListener requires at least two arguments");
105 if (typeof args
[1] == "object") {
106 this.options
= args
[1];
109 expectedAnswer
: args
[1],
110 expectedSuccess
: args
[2] ?? true,
112 trrServer
: args
[4] ?? "",
113 expectEarlyFail
: args
[5] ?? "",
115 type
: args
[7] ?? Ci
.nsIDNSService
.RESOLVE_TYPE_DEFAULT
,
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(
134 const currentThread
= threadManager
.currentThread
;
136 this.additionalInfo
=
137 trrServer
== "" && port
== -1
139 : Services
.dns
.newAdditionalInfo(trrServer
, port
);
141 this.request
= Services
.dns
.asyncResolve(
144 this.options
.flags
|| 0,
148 this.options
.originAttributes
|| {} // defaultOriginAttributes
150 Assert
.ok(!this.options
.expectEarlyFail
, "asyncResolve ok");
152 Assert
.ok(this.options
.expectEarlyFail
, "asyncResolve fail");
153 this.resolve({ error
: e
});
157 onLookupComplete(inRequest
, inRecord
, inStatus
) {
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
});
170 Assert
.equal(inStatus
, Cr
.NS_OK
, "Checking status");
172 if (this.type
!= Ci
.nsIDNSService
.RESOLVE_TYPE_DEFAULT
) {
173 this.resolve({ inRequest
, inRecord
, inStatus
});
177 inRecord
.QueryInterface(Ci
.nsIDNSAddrRecord
);
178 let answer
= inRecord
.getNextAddrAsString();
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
,
190 `the response should take at least ${this.delay}`
193 Assert
.greaterOrEqual(
194 inRecord
.trrFetchDuration
,
196 `the response should take at least ${this.delay}`
199 if (this.delay
== 0) {
200 // The response timing should be really 0
202 inRecord
.trrFetchDurationNetworkOnly
,
204 `the response time should be 0`
208 inRecord
.trrFetchDuration
,
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
)) {
222 throw Components
.Exception("", Cr
.NS_ERROR_NO_INTERFACE
);
225 // Implement then so we can await this as a promise.
227 return this.promise
.then
.apply(this.promise
, arguments
);
230 cancel(aStatus
= Cr
.NS_ERROR_ABORT
) {
231 Services
.dns
.cancelAsyncResolve(
234 this.options
.flags
|| 0,
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")) {
249 resp
.end("Missing search parameter");
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({
262 answers
: response
.answers
|| [],
263 additionals
: response
.additionals
|| [],
265 let writeResponse
= (resp2
, buf2
) => {
267 let data
= buf2
.toString("hex");
268 resp2
.setHeader("Content-Length", data
.length
);
269 resp2
.writeHead(200, { "Content-Type": "plain/text" });
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
) {
299 resp
.end("Missing dns parameter");
303 requestBody
= Buffer
.from(url
.query
.dns
, "base64");
304 processRequest(req
, resp
, requestBody
);
306 // unexpected method.
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({
332 questions
: dnsQuery
.questions
,
333 answers
: response
.answers
|| [],
334 additionals
: response
.additionals
|| [],
337 let writeResponse
= (resp2
, buf2
, context
) => {
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");
347 // Bigger error means force close the session
348 req1
.stream
.session
.close();
351 resp2
.setHeader("Content-Length", buf2
.length
);
352 resp2
.writeHead(200, { "Content-Type": "application/dns-message" });
358 if (response
.delay
) {
359 // This function is handled within the httpserver where setTimeout is
361 // eslint-disable-next-line no-undef
364 writeResponse(arg
[0], arg
[1], arg
[2]);
367 [resp1
, buf
, response
]
372 writeResponse(resp1
, buf
, response
);
376 function getRequestCount(domain
, type
) {
377 if (!global
.dns_query_counts
[domain
]) {
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 = {};
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");
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
413 /// name: "bar.example.com",
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(
427 return this.execute(text
);
430 async
requestCount(domain
, type
) {
431 return this.execute(`getRequestCount("${domain}", "${type}")`);
435 // Implements a basic HTTP2 proxy server
437 static async
startServer(endServerPort
) {
438 const fs
= require("fs");
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
);
447 global
.endServerPort
= endServerPort
;
449 await global
.proxy
.listen(0);
451 let serverPort
= global
.proxy
.address().port
;
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() {
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 });
497 global
.proxy_stream_count
++;
498 const net
= require("net");
499 const socket
= net
.connect(global
.endServerPort
, "127.0.0.1", () => {
501 stream
.respond({ ":status": 200 });
504 } catch (exception
) {
505 console
.log(exception
);
509 socket
.on("error", error
=> {
511 `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`
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
);
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(
545 `TRRProxyCode.proxyRequestCount()`
547 return parseInt(data
);