head_trr.js (36615B)
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/. */ 4 5 "use strict"; 6 7 /* import-globals-from head_cache.js */ 8 /* import-globals-from head_cookies.js */ 9 /* import-globals-from head_channels.js */ 10 /* globals require, __dirname, global, Buffer, process, setTimeout */ 11 12 var { NodeHTTP2Server: TRRNodeHttp2Server, NodeServer: TRRNodeServer } = 13 ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); 14 15 const { AppConstants: TRRAppConstants } = ChromeUtils.importESModule( 16 "resource://gre/modules/AppConstants.sys.mjs" 17 ); 18 19 /// Sets the TRR related prefs and adds the certificate we use for the HTTP2 20 /// server. 21 function trr_test_setup() { 22 dump("start!\n"); 23 24 let h2Port = Services.env.get("MOZHTTP2_PORT"); 25 Assert.notEqual(h2Port, null); 26 Assert.notEqual(h2Port, ""); 27 28 // Set to allow the cert presented by our H2 server 29 do_get_profile(); 30 31 Services.prefs.setBoolPref("network.http.http2.enabled", true); 32 // the TRR server is on 127.0.0.1 33 if (TRRAppConstants.platform == "android") { 34 Services.prefs.setCharPref("network.trr.bootstrapAddr", "10.0.2.2"); 35 } else { 36 Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1"); 37 } 38 39 // make all native resolve calls "secretly" resolve localhost instead 40 Services.prefs.setBoolPref("network.dns.native-is-localhost", true); 41 42 Services.prefs.setBoolPref("network.trr.wait-for-portal", false); 43 // don't confirm that TRR is working, just go! 44 Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); 45 // some tests rely on the cache not being cleared on pref change. 46 // we specifically test that this works 47 Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); 48 49 // Turn off strict fallback mode and TRR retry for most tests, 50 // it is tested specifically. 51 Services.prefs.setBoolPref("network.trr.strict_native_fallback", false); 52 Services.prefs.setBoolPref("network.trr.retry_on_recoverable_errors", false); 53 54 // Turn off temp blocklist feature in tests. When enabled we may issue a 55 // lookup to resolve a parent name when blocklisting, which may bleed into 56 // and interfere with subsequent tasks. 57 Services.prefs.setBoolPref("network.trr.temp_blocklist", false); 58 59 // We intentionally don't set the TRR mode. Each test should set it 60 // after setup in the first test. 61 62 return h2Port; 63 } 64 65 /// Clears the prefs that we're likely to set while testing TRR code 66 function trr_clear_prefs() { 67 Services.prefs.clearUserPref("network.trr.mode"); 68 Services.prefs.clearUserPref("network.trr.uri"); 69 Services.prefs.clearUserPref("network.trr.credentials"); 70 Services.prefs.clearUserPref("network.trr.wait-for-portal"); 71 Services.prefs.clearUserPref("network.trr.allow-rfc1918"); 72 Services.prefs.clearUserPref("network.trr.useGET"); 73 Services.prefs.clearUserPref("network.trr.confirmationNS"); 74 Services.prefs.clearUserPref("network.trr.bootstrapAddr"); 75 Services.prefs.clearUserPref("network.trr.temp_blocklist_duration_sec"); 76 Services.prefs.clearUserPref("network.trr.request_timeout_ms"); 77 Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); 78 Services.prefs.clearUserPref("network.trr.disable-ECS"); 79 Services.prefs.clearUserPref("network.trr.early-AAAA"); 80 Services.prefs.clearUserPref("network.trr.excluded-domains"); 81 Services.prefs.clearUserPref("network.trr.builtin-excluded-domains"); 82 Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); 83 Services.prefs.clearUserPref("captivedetect.canonicalURL"); 84 85 Services.prefs.clearUserPref("network.http.http2.enabled"); 86 Services.prefs.clearUserPref("network.dns.localDomains"); 87 Services.prefs.clearUserPref("network.dns.native-is-localhost"); 88 Services.prefs.clearUserPref( 89 "network.trr.send_empty_accept-encoding_headers" 90 ); 91 Services.prefs.clearUserPref("network.trr.strict_native_fallback"); 92 Services.prefs.clearUserPref("network.trr.temp_blocklist"); 93 } 94 95 /// This class sends a DNS query and can be awaited as a promise to get the 96 /// response. 97 class TRRDNSListener { 98 constructor(...args) { 99 if (args.length < 2) { 100 Assert.ok(false, "TRRDNSListener requires at least two arguments"); 101 } 102 this.name = args[0]; 103 if (typeof args[1] == "object") { 104 this.options = args[1]; 105 } else { 106 this.options = { 107 expectedAnswer: args[1], 108 expectedSuccess: args[2] ?? true, 109 delay: args[3], 110 trrServer: args[4] ?? "", 111 expectEarlyFail: args[5] ?? "", 112 flags: args[6] ?? 0, 113 type: args[7] ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, 114 port: args[8] ?? -1, 115 }; 116 } 117 this.expectedAnswer = this.options.expectedAnswer ?? undefined; 118 this.expectedSuccess = this.options.expectedSuccess ?? true; 119 this.delay = this.options.delay; 120 this.promise = new Promise(resolve => { 121 this.resolve = resolve; 122 }); 123 this.type = this.options.type ?? Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT; 124 let trrServer = this.options.trrServer || ""; 125 let port = this.options.port || -1; 126 127 // This may be called in a child process that doesn't have Services available. 128 // eslint-disable-next-line mozilla/use-services 129 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( 130 Ci.nsIThreadManager 131 ); 132 const currentThread = threadManager.currentThread; 133 134 this.additionalInfo = 135 trrServer == "" && port == -1 136 ? null 137 : Services.dns.newAdditionalInfo(trrServer, port); 138 try { 139 this.request = Services.dns.asyncResolve( 140 this.name, 141 this.type, 142 this.options.flags || 0, 143 this.additionalInfo, 144 this, 145 currentThread, 146 this.options.originAttributes || {} // defaultOriginAttributes 147 ); 148 Assert.ok(!this.options.expectEarlyFail, "asyncResolve ok"); 149 } catch (e) { 150 Assert.ok(this.options.expectEarlyFail, "asyncResolve fail"); 151 this.resolve({ error: e }); 152 } 153 } 154 155 onLookupComplete(inRequest, inRecord, inStatus) { 156 Assert.equal( 157 inRequest, 158 this.request, 159 "Checking that this is the correct callback" 160 ); 161 162 // If we don't expect success here, just resolve and the caller will 163 // decide what to do with the results. 164 if (!this.expectedSuccess) { 165 this.resolve({ inRequest, inRecord, inStatus }); 166 return; 167 } 168 169 Assert.equal(inStatus, Cr.NS_OK, "Checking status"); 170 171 if (this.type != Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT) { 172 this.resolve({ inRequest, inRecord, inStatus }); 173 return; 174 } 175 176 inRecord.QueryInterface(Ci.nsIDNSAddrRecord); 177 let answer = inRecord.getNextAddrAsString(); 178 Assert.equal( 179 answer, 180 this.expectedAnswer, 181 `Checking result for ${this.name}` 182 ); 183 inRecord.rewind(); // In case the caller also checks the addresses 184 185 if (this.delay !== undefined) { 186 Assert.greaterOrEqual( 187 inRecord.trrFetchDurationNetworkOnly, 188 this.delay, 189 `the response should take at least ${this.delay}` 190 ); 191 192 Assert.greaterOrEqual( 193 inRecord.trrFetchDuration, 194 this.delay, 195 `the response should take at least ${this.delay}` 196 ); 197 198 if (this.delay == 0) { 199 // The response timing should be really 0 200 Assert.equal( 201 inRecord.trrFetchDurationNetworkOnly, 202 0, 203 `the response time should be 0` 204 ); 205 206 Assert.equal( 207 inRecord.trrFetchDuration, 208 this.delay, 209 `the response time should be 0` 210 ); 211 } 212 } 213 214 this.resolve({ inRequest, inRecord, inStatus }); 215 } 216 217 QueryInterface(aIID) { 218 if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) { 219 return this; 220 } 221 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 222 } 223 224 // Implement then so we can await this as a promise. 225 then() { 226 return this.promise.then.apply(this.promise, arguments); 227 } 228 229 cancel(aStatus = Cr.NS_ERROR_ABORT) { 230 Services.dns.cancelAsyncResolve( 231 this.name, 232 this.type, 233 this.options.flags || 0, 234 this.resolverInfo, 235 this, 236 aStatus, 237 {} 238 ); 239 } 240 } 241 242 // This is for reteriiving the raw bytes from a DNS answer. 243 function answerHandler(req, resp) { 244 let searchParams = new URL(req.url, "http://example.com").searchParams; 245 if (!searchParams.get("host")) { 246 resp.writeHead(400); 247 resp.end("Missing search parameter"); 248 return; 249 } 250 251 function processRequest(req1, resp1) { 252 let domain = searchParams.get("host"); 253 let type = searchParams.get("type"); 254 let response = global.dns_query_answers[`${domain}/${type}`] || {}; 255 let buf = global.dnsPacket.encode({ 256 type: "response", 257 id: 0, 258 flags: 0, 259 questions: [], 260 answers: response.answers || [], 261 additionals: response.additionals || [], 262 }); 263 let writeResponse = (resp2, buf2) => { 264 try { 265 let data = buf2.toString("hex"); 266 resp2.setHeader("Content-Length", data.length); 267 resp2.writeHead(200, { "Content-Type": "plain/text" }); 268 resp2.write(data); 269 resp2.end(""); 270 } catch (e) {} 271 }; 272 273 writeResponse(resp1, buf, response); 274 } 275 276 processRequest(req, resp); 277 } 278 279 /// This is the default handler for /dns-query 280 /// It implements basic functionality for parsing the DoH packet, then 281 /// queries global.dns_query_answers for available answers for the DNS query. 282 function trrQueryHandler(req, resp) { 283 let requestBody = Buffer.from(""); 284 let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD]; 285 let contentLength = req.headers["content-length"]; 286 287 if (method == "POST") { 288 req.on("data", chunk => { 289 requestBody = Buffer.concat([requestBody, chunk]); 290 if (requestBody.length == contentLength) { 291 processRequest(req, resp, requestBody); 292 } 293 }); 294 } else if (method == "GET") { 295 let searchParams = new URL(req.url, "http://example.com").searchParams; 296 if (!searchParams.get("dns")) { 297 resp.writeHead(400); 298 resp.end("Missing dns parameter"); 299 return; 300 } 301 302 requestBody = Buffer.from(searchParams.get("dns"), "base64"); 303 processRequest(req, resp, requestBody); 304 } else { 305 // unexpected method. 306 resp.writeHead(405); 307 resp.end("Unexpected method"); 308 } 309 310 function processRequest(req1, resp1, payload) { 311 let dnsQuery = global.dnsPacket.decode(payload); 312 let domain = dnsQuery.questions[0].name; 313 let type = dnsQuery.questions[0].type; 314 let response = global.dns_query_answers[`${domain}/${type}`] || {}; 315 let delay = response.delay || 0; 316 let searchParams = new URL(req1.url, "http://example.com").searchParams; 317 if (searchParams.get("conncycle")) { 318 if (domain.startsWith("newconn")) { 319 // If we haven't seen a req for this newconn name before, 320 // or if we've seen one for the same name on the same port, 321 // synthesize a timeout. 322 if ( 323 !global.gDoHNewConnLog[domain] || 324 global.gDoHNewConnLog[domain] == req1.socket.remotePort 325 ) { 326 delay = 1000; 327 } 328 if (!global.gDoHNewConnLog[domain]) { 329 global.gDoHNewConnLog[domain] = req1.socket.remotePort; 330 } 331 } 332 global.gDoHPortsLog.push([domain, req1.socket.remotePort]); 333 } 334 335 if (!global.dns_query_counts[domain]) { 336 global.dns_query_counts[domain] = {}; 337 } 338 global.dns_query_counts[domain][type] = 339 global.dns_query_counts[domain][type] + 1 || 1; 340 341 let flags = global.dnsPacket.RECURSION_DESIRED; 342 if (!response.answers && !response.flags) { 343 flags |= 2; // SERVFAIL 344 } 345 flags |= response.flags || 0; 346 let buf = global.dnsPacket.encode({ 347 type: "response", 348 id: dnsQuery.id, 349 flags, 350 questions: dnsQuery.questions, 351 answers: response.answers || [], 352 additionals: response.additionals || [], 353 }); 354 355 let writeResponse = (resp2, buf2, context) => { 356 try { 357 if (context.error) { 358 // If the error is a valid HTTP response number just write it out. 359 if (context.error < 600) { 360 resp2.writeHead(context.error); 361 resp2.end("Intentional error"); 362 return; 363 } 364 365 // Bigger error means force close the session 366 req1.stream.session.close(); 367 return; 368 } 369 resp2.setHeader("Content-Length", buf2.length); 370 resp2.writeHead(200, { "Content-Type": "application/dns-message" }); 371 resp2.write(buf2); 372 resp2.end(""); 373 } catch (e) {} 374 }; 375 376 if (delay) { 377 // This function is handled within the httpserver where setTimeout is 378 // available. 379 // eslint-disable-next-line no-undef 380 setTimeout( 381 arg => { 382 writeResponse(arg[0], arg[1], arg[2]); 383 }, 384 delay, 385 [resp1, buf, response] 386 ); 387 return; 388 } 389 390 writeResponse(resp1, buf, response); 391 } 392 } 393 394 function dohHandler(req, res) { 395 let u = global.url.parse(req.url, true); 396 397 function handleAuth() { 398 // There's a Set-Cookie: header in the response for "/dns" , which this 399 // request subsequently would include if the http channel wasn't 400 // anonymous. Thus, if there's a cookie in this request, we know Firefox 401 // mishaved. If there's not, we're fine. 402 if (req.headers.cookie) { 403 res.writeHead(403); 404 res.end("cookie for me, not for you"); 405 return false; 406 } 407 if (req.headers.authorization != "user:password") { 408 res.writeHead(401); 409 res.end("bad boy!"); 410 return false; 411 } 412 413 return true; 414 } 415 416 function createDNSAnswer(response, packet, responseIP, requestPayload) { 417 // This shuts down the connection so we can test if the client reconnects 418 if (packet.questions.length && packet.questions[0].name == "closeme.com") { 419 // response.stream.connection.close("INTERNAL_ERROR", response.stream.id); 420 req.stream.session.close(); 421 return null; 422 } 423 424 let answers = []; 425 426 if (u.query.httpssvc) { 427 responseIP = "none"; 428 answers.push({ 429 name: packet.questions[0].name, 430 type: packet.questions[0].type, 431 ttl: 55, 432 class: "IN", 433 flush: false, 434 data: { 435 priority: 1, 436 name: "h3pool", 437 values: [ 438 { key: "alpn", value: ["h2", "h3"] }, 439 { key: "no-default-alpn" }, 440 { key: "port", value: 8888 }, 441 { key: "ipv4hint", value: "1.2.3.4" }, 442 { key: "echconfig", value: "123..." }, 443 { key: "ipv6hint", value: "::1" }, 444 { key: 30, value: "somelargestring" }, 445 { key: "odoh", value: "456..." }, 446 ], 447 }, 448 }); 449 answers.push({ 450 name: packet.questions[0].name, 451 type: packet.questions[0].type, 452 ttl: 55, 453 class: "IN", 454 flush: false, 455 data: { 456 priority: 2, 457 name: ".", 458 values: [ 459 { key: "alpn", value: "h2" }, 460 { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] }, 461 { key: "echconfig", value: "abc..." }, 462 { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] }, 463 { key: "odoh", value: "def..." }, 464 ], 465 }, 466 }); 467 answers.push({ 468 name: packet.questions[0].name, 469 type: packet.questions[0].type, 470 ttl: 55, 471 class: "IN", 472 flush: false, 473 data: { 474 priority: 3, 475 name: "hello", 476 values: [], 477 }, 478 }); 479 } else if (u.query.httpssvc_as_altsvc) { 480 responseIP = "none"; 481 if (packet.questions[0].type == "HTTPS") { 482 let priority = 1; 483 if (packet.questions[0].name === "foo.notexisted.com") { 484 priority = 0; 485 } 486 answers.push({ 487 name: packet.questions[0].name, 488 type: packet.questions[0].type, 489 ttl: 55, 490 class: "IN", 491 flush: false, 492 data: { 493 priority, 494 name: packet.questions[0].name, 495 values: [ 496 { key: "alpn", value: "h2" }, 497 { key: "port", value: global.serverPort }, 498 { key: 30, value: "somelargestring" }, 499 ], 500 }, 501 }); 502 } else { 503 answers.push({ 504 name: packet.questions[0].name, 505 type: "A", 506 ttl: 55, 507 flush: false, 508 data: "127.0.0.1", 509 }); 510 } 511 } else if (u.query.httpssvc_use_iphint) { 512 responseIP = "none"; 513 answers.push({ 514 name: packet.questions[0].name, 515 type: "HTTPS", 516 ttl: 55, 517 class: "IN", 518 flush: false, 519 data: { 520 priority: 1, 521 name: ".", 522 values: [ 523 { key: "alpn", value: "h2" }, 524 { key: "port", value: global.serverPort }, 525 { key: "ipv4hint", value: "127.0.0.1" }, 526 ], 527 }, 528 }); 529 } 530 531 if (packet.questions.length && packet.questions[0].name.endsWith(".pd")) { 532 // Bug 1543811: test edns padding extension. Return whether padding was 533 // included via the first half of the ip address (1.1 vs 2.2) and the 534 // size of the request in the second half of the ip address allowing to 535 // verify that the correct amount of padding was added. 536 if ( 537 !!packet.additionals.length && 538 packet.additionals[0].type == "OPT" && 539 packet.additionals[0].options.some(o => o.type === "PADDING") 540 ) { 541 // add padding to the response, because the client must be able ignore it 542 answers.push({ 543 name: ".", 544 type: "PADDING", 545 data: Buffer.from( 546 // PADDING_PADDING_PADDING 547 "50414444494e475f50414444494e475f50414444494e47", 548 "hex" 549 ), 550 }); 551 responseIP = 552 "1.1." + 553 ((requestPayload.length >> 8) & 0xff) + 554 "." + 555 (requestPayload.length & 0xff); 556 } else { 557 responseIP = 558 "2.2." + 559 ((requestPayload.length >> 8) & 0xff) + 560 "." + 561 (requestPayload.length & 0xff); 562 } 563 } 564 565 if (u.query.corruptedAnswer) { 566 // DNS response header is 12 bytes, we check for this minimum length 567 // at the start of decoding so this is the simplest way to force 568 // a decode error. 569 return "\xFF\xFF\xFF\xFF"; 570 } 571 572 // Because we send two TRR requests (A and AAAA), skip the first two 573 // requests when testing retry. 574 if (u.query.retryOnDecodeFailure && global.gDoHRequestCount < 2) { 575 global.gDoHRequestCount++; 576 return "\xFF\xFF\xFF\xFF"; 577 } 578 579 function responseData() { 580 if ( 581 !!packet.questions.length && 582 packet.questions[0].name == "confirm.example.com" && 583 packet.questions[0].type == "NS" 584 ) { 585 return "ns.example.com"; 586 } 587 588 return responseIP; 589 } 590 591 if ( 592 responseIP != "none" && 593 responseType(packet, responseIP) == packet.questions[0].type 594 ) { 595 answers.push({ 596 name: u.query.hostname ? u.query.hostname : packet.questions[0].name, 597 ttl: 55, 598 type: responseType(packet, responseIP), 599 flush: false, 600 data: responseData(), 601 }); 602 } 603 604 // for use with test_dns_by_type_resolve.js 605 if (packet.questions[0].type == "TXT") { 606 answers.push({ 607 name: packet.questions[0].name, 608 type: packet.questions[0].type, 609 ttl: 55, 610 class: "IN", 611 flush: false, 612 data: Buffer.from( 613 "62586B67646D39705932556761584D6762586B676347467A63336476636D513D", 614 "hex" 615 ), 616 }); 617 } 618 619 if (u.query.cnameloop) { 620 answers.push({ 621 name: "cname.example.com", 622 type: "CNAME", 623 ttl: 55, 624 class: "IN", 625 flush: false, 626 data: "pointing-elsewhere.example.com", 627 }); 628 } 629 630 if (req.headers["accept-language"] || req.headers["user-agent"]) { 631 // If we get this header, don't send back any response. This should 632 // cause the tests to fail. This is easier then actually sending back 633 // the header value into test_trr.js 634 answers = []; 635 } 636 637 let buf = global.dnsPacket.encode({ 638 type: "response", 639 id: packet.id, 640 flags: global.dnsPacket.RECURSION_DESIRED, 641 questions: packet.questions, 642 answers, 643 }); 644 645 return buf; 646 } 647 648 function responseType(packet, responseIP) { 649 if ( 650 !!packet.questions.length && 651 packet.questions[0].name == "confirm.example.com" && 652 packet.questions[0].type == "NS" 653 ) { 654 return "NS"; 655 } 656 657 return global.ip.isV4Format(responseIP) ? "A" : "AAAA"; 658 } 659 660 function getDelayFromPacket(packet, type) { 661 let delay = 0; 662 if (packet.questions[0].type == "A") { 663 delay = parseInt(u.query.delayIPv4); 664 } else if (packet.questions[0].type == "AAAA") { 665 delay = parseInt(u.query.delayIPv6); 666 } 667 668 if (u.query.slowConfirm && type == "NS") { 669 delay += 1000; 670 } 671 672 return delay; 673 } 674 675 function writeDNSResponse(response, buf, delay, contentType) { 676 function writeResponse(resp, buffer) { 677 resp.setHeader("Set-Cookie", "trackyou=yes; path=/; max-age=100000;"); 678 resp.setHeader("Content-Type", contentType); 679 if (req.headers["accept-encoding"].includes("gzip")) { 680 global.zlib.gzip(buffer, function (err, result) { 681 resp.setHeader("Content-Encoding", "gzip"); 682 resp.setHeader("Content-Length", result.length); 683 try { 684 resp.writeHead(200); 685 resp.end(result); 686 } catch (e) { 687 // connection was closed by the time we started writing. 688 } 689 }); 690 } else { 691 const output = Buffer.from(buffer, "utf-8"); 692 resp.setHeader("Content-Length", output.length); 693 try { 694 resp.writeHead(200); 695 resp.write(output); 696 resp.end(""); 697 } catch (e) { 698 // connection was closed by the time we started writing. 699 } 700 } 701 } 702 703 if (delay) { 704 setTimeout( 705 arg => { 706 writeResponse(arg[0], arg[1]); 707 }, 708 delay + 1, 709 [response, buf] 710 ); 711 return; 712 } 713 714 writeResponse(response, buf); 715 } 716 717 let responseIP = u.query.responseIP; 718 if (!responseIP) { 719 responseIP = "5.5.5.5"; 720 } 721 722 let redirect = u.query.redirect; 723 if (redirect) { 724 responseIP = redirect; 725 if (u.query.dns) { 726 res.setHeader( 727 "Location", 728 "https://localhost:" + 729 global.serverPort + 730 "/doh?responseIP=" + 731 responseIP + 732 "&dns=" + 733 u.query.dns 734 ); 735 } else { 736 res.setHeader( 737 "Location", 738 "https://localhost:" + 739 global.serverPort + 740 "/doh?responseIP=" + 741 responseIP 742 ); 743 } 744 res.writeHead(307); 745 res.end(""); 746 return; 747 } 748 749 if (u.query.auth) { 750 if (!handleAuth()) { 751 return; 752 } 753 } 754 755 if (u.query.noResponse) { 756 return; 757 } 758 759 if (u.query.push) { 760 // push.example.org has AAAA entry 2018::2018 761 let pcontent = global.dnsPacket.encode({ 762 id: 0, 763 type: "response", 764 flags: global.dnsPacket.RECURSION_DESIRED, 765 questions: [{ name: "push.example.org", type: "AAAA", class: "IN" }], 766 answers: [ 767 { 768 name: "push.example.org", 769 type: "AAAA", 770 ttl: 55, 771 class: "IN", 772 flush: false, 773 data: "2018::2018", 774 }, 775 ], 776 }); 777 let push = res.push({ 778 hostname: "foo.example.com:" + global.serverPort, 779 port: global.serverPort, 780 path: "/dns-pushed-response?dns=AAAAAAABAAAAAAAABHB1c2gHZXhhbXBsZQNvcmcAABwAAQ", 781 method: "GET", 782 headers: { 783 accept: "application/dns-message", 784 }, 785 }); 786 push.writeHead(200, { 787 "content-type": "application/dns-message", 788 pushed: "yes", 789 "content-length": pcontent.length, 790 "X-Connection-Http2": "yes", 791 }); 792 push.end(pcontent); 793 } 794 795 let payload = Buffer.from(""); 796 797 function emitResponse(response, requestPayload, decodedPacket, delay) { 798 let packet = decodedPacket || global.dnsPacket.decode(requestPayload); 799 let answer = createDNSAnswer(response, packet, responseIP, requestPayload); 800 if (!answer) { 801 return; 802 } 803 writeDNSResponse( 804 response, 805 answer, 806 delay || getDelayFromPacket(packet, responseType(packet, responseIP)), 807 "application/dns-message" 808 ); 809 } 810 811 if (u.query.dns) { 812 payload = Buffer.from(u.query.dns, "base64"); 813 emitResponse(res, payload); 814 return; 815 } 816 817 req.on("data", function receiveData(chunk) { 818 payload = Buffer.concat([payload, chunk]); 819 }); 820 req.on("end", function finishedData() { 821 // parload is empty when we send redirect response. 822 if (payload.length) { 823 let packet = global.dnsPacket.decode(payload); 824 emitResponse(res, payload, packet); 825 } 826 }); 827 } 828 829 function cnameHandler(req, res) { 830 // asking for cname.example.com 831 832 function createCNameContent(payload) { 833 let packet = global.dnsPacket.decode(payload); 834 if ( 835 packet.questions[0].name == "cname.example.com" && 836 packet.questions[0].type == "A" 837 ) { 838 return global.dnsPacket.encode({ 839 id: 0, 840 type: "response", 841 flags: global.dnsPacket.RECURSION_DESIRED, 842 questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }], 843 answers: [ 844 { 845 name: packet.questions[0].name, 846 ttl: 55, 847 type: "CNAME", 848 flush: false, 849 data: "pointing-elsewhere.example.com", 850 }, 851 ], 852 }); 853 } 854 if ( 855 packet.questions[0].name == "pointing-elsewhere.example.com" && 856 packet.questions[0].type == "A" 857 ) { 858 return global.dnsPacket.encode({ 859 id: 0, 860 type: "response", 861 flags: global.dnsPacket.RECURSION_DESIRED, 862 questions: [{ name: packet.questions[0].name, type: "A", class: "IN" }], 863 answers: [ 864 { 865 name: packet.questions[0].name, 866 ttl: 55, 867 type: "A", 868 flush: false, 869 data: "99.88.77.66", 870 }, 871 ], 872 }); 873 } 874 875 return global.dnsPacket.encode({ 876 id: 0, 877 type: "response", 878 flags: 879 global.dnsPacket.RECURSION_DESIRED | 880 global.dnsPacket.rcodes.toRcode("NXDOMAIN"), 881 questions: [ 882 { 883 name: packet.questions[0].name, 884 type: packet.questions[0].type, 885 class: "IN", 886 }, 887 ], 888 answers: [], 889 }); 890 } 891 892 function emitResponse(response, payload) { 893 let pcontent = createCNameContent(payload); 894 response.setHeader("Content-Type", "application/dns-message"); 895 response.setHeader("Content-Length", pcontent.length); 896 response.writeHead(200); 897 response.write(pcontent); 898 response.end(""); 899 } 900 901 let payload = Buffer.from(""); 902 req.on("data", function receiveData(chunk) { 903 payload = Buffer.concat([payload, chunk]); 904 }); 905 req.on("end", function finishedData() { 906 emitResponse(res, payload); 907 }); 908 } 909 910 function cnameAHandler(req, res) { 911 function createCNameARecord() { 912 // test23 asks for cname-a.example.com 913 // this responds with a CNAME to here.example.com *and* an A record 914 // for here.example.com 915 let rContent; 916 917 rContent = Buffer.from( 918 "0000" + 919 "0100" + 920 "0001" + // QDCOUNT 921 "0002" + // ANCOUNT 922 "00000000" + // NSCOUNT + ARCOUNT 923 "07636E616D652d61" + // cname-a 924 "076578616D706C6503636F6D00" + // .example.com 925 "00010001" + // question type (A) + question class (IN) 926 // answer record 1 927 "C00C" + // name pointer to cname-a.example.com 928 "0005" + // type (CNAME) 929 "0001" + // class 930 "00000037" + // TTL 931 "0012" + // RDLENGTH 932 "0468657265" + // here 933 "076578616D706C6503636F6D00" + // .example.com 934 // answer record 2, the A entry for the CNAME above 935 "0468657265" + // here 936 "076578616D706C6503636F6D00" + // .example.com 937 "0001" + // type (A) 938 "0001" + // class 939 "00000037" + // TTL 940 "0004" + // RDLENGTH 941 "09080706", // IPv4 address 942 "hex" 943 ); 944 945 return rContent; 946 } 947 948 let rContent = createCNameARecord(); 949 res.setHeader("Content-Type", "application/dns-message"); 950 res.setHeader("Content-Length", rContent.length); 951 res.writeHead(200); 952 res.write(rContent); 953 res.end(""); 954 } 955 956 function getRequestCount(domain, type) { 957 if (!global.dns_query_counts[domain]) { 958 return 0; 959 } 960 return global.dns_query_counts[domain][type] || 0; 961 } 962 963 // A convenient wrapper around NodeServer 964 class TRRServer extends TRRNodeHttp2Server { 965 /// Starts the server 966 /// @port - default 0 967 /// when provided, will attempt to listen on that port. 968 async start(port = 0) { 969 await super.start(port); 970 await this.execute(`( () => { 971 // key: string "name/type" 972 // value: array [answer1, answer2] 973 global.dns_query_answers = {}; 974 975 // key: domain 976 // value: a map containing {key: type, value: number of requests} 977 global.dns_query_counts = {}; 978 979 global.gDoHPortsLog = []; 980 global.gDoHNewConnLog = {}; 981 global.gDoHRequestCount = 0; 982 983 global.dnsPacket = require(\`\${__dirname}/../dns-packet\`); 984 global.ip = require(\`\${__dirname}/../node_ip\`); 985 global.http2 = require("http2"); 986 global.url = require("url"); 987 global.zlib = require("zlib"); 988 })()`); 989 await this.registerPathHandler("/dns-query", trrQueryHandler); 990 await this.registerPathHandler("/dnsAnswer", answerHandler); 991 await this.registerPathHandler("/doh", dohHandler); 992 await this.registerPathHandler("/reset-doh-request-count", (req, res) => { 993 global.gDoHRequestCount = 0; 994 res.setHeader("Content-Type", "text/plain"); 995 res.setHeader("Content-Length", "ok".length); 996 res.writeHead(200); 997 res.write("ok"); 998 res.end(""); 999 }); 1000 await this.registerPathHandler("/", (req, res) => { 1001 if (req.httpVersionMajor === 2) { 1002 res.setHeader("X-Connection-Http2", "yes"); 1003 res.setHeader("X-Http2-StreamId", "" + req.stream.id); 1004 } else { 1005 res.setHeader("X-Connection-Http2", "no"); 1006 } 1007 res.setHeader("Content-Type", "text/plain"); 1008 res.writeHead(404); 1009 res.end(""); 1010 }); 1011 await this.registerPathHandler("/dns-cname", cnameHandler); 1012 await this.registerPathHandler("/dns-cname-a", cnameAHandler); 1013 await this.registerPathHandler("/server-timing", (req, res) => { 1014 if (req.httpVersionMajor === 2) { 1015 res.setHeader("X-Connection-Http2", "yes"); 1016 res.setHeader("X-Http2-StreamId", "" + req.stream.id); 1017 } else { 1018 res.setHeader("X-Connection-Http2", "no"); 1019 } 1020 1021 res.setHeader("Content-Type", "text/plain"); 1022 res.setHeader("Content-Length", "12"); 1023 res.setHeader("Trailer", "Server-Timing"); 1024 res.setHeader( 1025 "Server-Timing", 1026 "metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1" 1027 ); 1028 res.write("data reached"); 1029 res.addTrailers({ 1030 "Server-Timing": 1031 "metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3", 1032 }); 1033 res.end(); 1034 }); 1035 await this.registerPathHandler("/redirect_to_http", (req, res) => { 1036 let u = global.url.parse(req.url, true); 1037 res.setHeader( 1038 "Location", 1039 `http://test.httpsrr.redirect.com:${u.query.port}/redirect_to_http?port=${u.query.port}` 1040 ); 1041 res.writeHead(307); 1042 res.end(""); 1043 }); 1044 await this.registerPathHandler("/origin_header", (req, res) => { 1045 if (req.httpVersionMajor === 2) { 1046 res.setHeader("X-Connection-Http2", "yes"); 1047 res.setHeader("X-Http2-StreamId", "" + req.stream.id); 1048 } else { 1049 res.setHeader("X-Connection-Http2", "no"); 1050 } 1051 1052 let originHeader = req.headers.origin; 1053 res.setHeader("Content-Length", originHeader.length); 1054 res.setHeader("Content-Type", "text/plain"); 1055 res.writeHead(200); 1056 res.write(originHeader); 1057 res.end(); 1058 }); 1059 1060 await this.execute(getRequestCount); 1061 await this.execute(`global.serverPort = ${this.port()}`); 1062 } 1063 1064 /// @name : string - name we're providing answers for. eg: foo.example.com 1065 /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc 1066 /// @response : a map containing the response 1067 /// answers: array of answers (hashmap) that dnsPacket can parse 1068 /// eg: [{ 1069 /// name: "bar.example.com", 1070 /// ttl: 55, 1071 /// type: "A", 1072 /// flush: false, 1073 /// data: "1.2.3.4", 1074 /// }] 1075 /// additionals - array of answers (hashmap) to be added to the additional section 1076 /// delay: int - if not 0 the response will be sent with after `delay` ms. 1077 /// flags: int - flags to be set on the answer 1078 /// error: int - HTTP status. If truthy then the response will send this status 1079 async registerDoHAnswers(name, type, response = {}) { 1080 let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify( 1081 response 1082 )}`; 1083 return this.execute(text); 1084 } 1085 1086 async requestCount(domain, type) { 1087 return this.execute(`getRequestCount("${domain}", "${type}")`); 1088 } 1089 } 1090 1091 // Implements a basic HTTP2 proxy server 1092 class TRRProxyCode { 1093 static async startServer(endServerPort) { 1094 const fs = require("fs"); 1095 const options = { 1096 key: fs.readFileSync(__dirname + "/http2-cert.key"), 1097 cert: fs.readFileSync(__dirname + "/http2-cert.pem"), 1098 }; 1099 1100 const http2 = require("http2"); 1101 global.proxy = http2.createSecureServer(options); 1102 this.setupProxy(); 1103 global.endServerPort = endServerPort; 1104 1105 await global.proxy.listen(0); 1106 1107 let serverPort = global.proxy.address().port; 1108 return serverPort; 1109 } 1110 1111 static closeProxy() { 1112 global.proxy.closeSockets(); 1113 return new Promise(resolve => { 1114 global.proxy.close(resolve); 1115 }); 1116 } 1117 1118 static proxyRequestCount() { 1119 return global.proxy_stream_count; 1120 } 1121 1122 static setupProxy() { 1123 if (!global.proxy) { 1124 throw new Error("proxy is null"); 1125 } 1126 1127 global.proxy_stream_count = 0; 1128 1129 // We need to track active connections so we can forcefully close keep-alive 1130 // connections when shutting down the proxy. 1131 global.proxy.socketIndex = 0; 1132 global.proxy.socketMap = {}; 1133 global.proxy.on("connection", function (socket) { 1134 let index = global.proxy.socketIndex++; 1135 global.proxy.socketMap[index] = socket; 1136 socket.on("close", function () { 1137 delete global.proxy.socketMap[index]; 1138 }); 1139 }); 1140 global.proxy.closeSockets = function () { 1141 for (let i in global.proxy.socketMap) { 1142 global.proxy.socketMap[i].destroy(); 1143 } 1144 }; 1145 1146 global.proxy.on("stream", (stream, headers) => { 1147 if (headers[":method"] !== "CONNECT") { 1148 // Only accept CONNECT requests 1149 stream.respond({ ":status": 405 }); 1150 stream.end(); 1151 return; 1152 } 1153 global.proxy_stream_count++; 1154 const net = require("net"); 1155 const socket = net.connect(global.endServerPort, "127.0.0.1", () => { 1156 try { 1157 stream.respond({ ":status": 200 }); 1158 socket.pipe(stream); 1159 stream.pipe(socket); 1160 } catch (exception) { 1161 console.log(exception); 1162 stream.close(); 1163 } 1164 }); 1165 socket.on("error", error => { 1166 throw new Error( 1167 `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'` 1168 ); 1169 }); 1170 }); 1171 } 1172 } 1173 1174 class TRRProxy { 1175 // Starts the proxy 1176 async start(port) { 1177 info("TRRProxy start!"); 1178 this.processId = await TRRNodeServer.fork(); 1179 info("processid=" + this.processId); 1180 await this.execute(TRRProxyCode); 1181 this.port = await this.execute(`TRRProxyCode.startServer(${port})`); 1182 Assert.notEqual(this.port, null); 1183 } 1184 1185 // Executes a command in the context of the node server 1186 async execute(command) { 1187 return TRRNodeServer.execute(this.processId, command); 1188 } 1189 1190 // Stops the server 1191 async stop() { 1192 if (this.processId) { 1193 await TRRNodeServer.execute(this.processId, `TRRProxyCode.closeProxy()`); 1194 await TRRNodeServer.kill(this.processId); 1195 } 1196 } 1197 1198 async request_count() { 1199 let data = await TRRNodeServer.execute( 1200 this.processId, 1201 `TRRProxyCode.proxyRequestCount()` 1202 ); 1203 return parseInt(data); 1204 } 1205 }