http3_common.js (19969B)
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 /* import-globals-from head_http3.js */ 11 12 const { HttpServer } = ChromeUtils.importESModule( 13 "resource://testing-common/httpd.sys.mjs" 14 ); 15 16 function makeChan(uri) { 17 let chan = NetUtil.newChannel({ 18 uri, 19 loadUsingSystemPrincipal: true, 20 }).QueryInterface(Ci.nsIHttpChannel); 21 chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; 22 return chan; 23 } 24 25 // Promise-backed Http3CheckListener 26 class Http3CheckListener { 27 constructor( 28 { expectedStatus = Cr.NS_OK, expectedRoute = "" } = {}, 29 resolve, 30 reject 31 ) { 32 this.onDataAvailableFired = false; 33 this.expectedStatus = expectedStatus; 34 this.expectedRoute = expectedRoute; 35 this._resolve = resolve; 36 this._reject = reject; 37 } 38 39 onStartRequest(request) { 40 Assert.ok(request instanceof Ci.nsIHttpChannel); 41 Assert.equal(request.status, this.expectedStatus); 42 if (Components.isSuccessCode(this.expectedStatus)) { 43 Assert.equal(request.responseStatus, 200); 44 } 45 } 46 47 onDataAvailable(request, stream, off, cnt) { 48 this.onDataAvailableFired = true; 49 read_stream(stream, cnt); 50 } 51 52 onStopRequest(request, status) { 53 Assert.equal(status, this.expectedStatus); 54 55 let routed = "NA"; 56 try { 57 routed = request.getRequestHeader("Alt-Used"); 58 } catch (e) {} 59 dump("routed is " + routed + "\n"); 60 61 Assert.equal(routed, this.expectedRoute); 62 63 if (Components.isSuccessCode(this.expectedStatus)) { 64 let httpVersion = ""; 65 try { 66 httpVersion = request.protocolVersion; 67 } catch (e) {} 68 Assert.equal(httpVersion, "h3"); 69 Assert.equal(this.onDataAvailableFired, true); 70 Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3"); 71 } 72 73 this._resolve?.(request); 74 } 75 } 76 77 class WaitForHttp3Listener extends Http3CheckListener { 78 constructor( 79 { 80 expectedStatus = Cr.NS_OK, 81 expectedRoute = "", 82 uri = "", 83 h3AltSvc = "", 84 retry, 85 delayMs = 500, 86 } = {}, 87 resolve, 88 reject 89 ) { 90 super({ expectedStatus, expectedRoute }, resolve, reject); 91 this.uri = uri; 92 this.h3AltSvc = h3AltSvc; 93 this._retry = retry; // function to re-open the request 94 this._delayMs = delayMs; // poll interval 95 } 96 97 onStopRequest(request, status) { 98 Assert.equal(status, this.expectedStatus); 99 100 let routed = "NA"; 101 try { 102 routed = request.getRequestHeader("Alt-Used"); 103 } catch (e) {} 104 dump(`routed is ${routed}\n`); 105 106 let httpVersion = ""; 107 try { 108 httpVersion = request.protocolVersion; 109 } catch (e) {} 110 111 if (routed === this.expectedRoute) { 112 // (This is where run_next_test() used to be.) 113 Assert.equal(routed, this.expectedRoute); // useful log/assert 114 Assert.equal(httpVersion, "h3"); 115 this._resolve?.(request); 116 return; 117 } 118 119 // Not routed yet: mirror old behavior (log + supportsHTTP3 check + retry) 120 dump("poll later for alt-svc mapping\n"); 121 if (httpVersion === "h2") { 122 request.QueryInterface(Ci.nsIHttpChannelInternal); 123 Assert.ok(request.supportsHTTP3); 124 } 125 126 if (typeof this._retry === "function") { 127 // schedule another attempt (replaces do_test_pending/do_timeout recursion) 128 do_timeout(this._delayMs, () => 129 this._retry(this.uri, this.expectedRoute, this.h3AltSvc) 130 ); 131 } 132 // Promise remains pending until a later attempt matches expectedRoute. 133 } 134 } 135 136 // Factory to create { listener, promise } 137 function createHttp3CheckListener(options = {}) { 138 let resolve, reject; 139 const promise = new Promise((res, rej) => { 140 resolve = res; 141 reject = rej; 142 }); 143 const listener = new Http3CheckListener(options, resolve, reject); 144 return { listener, promise }; 145 } 146 147 function createWaitForHttp3Listener(options = {}) { 148 let resolve, reject; 149 const promise = new Promise((res, rej) => { 150 resolve = res; 151 reject = rej; 152 }); 153 const listener = new WaitForHttp3Listener(options, resolve, reject); 154 return { listener, promise }; 155 } 156 157 // --- Async wrapper that does the polling by re-issuing the request --- 158 async function waitForHttp3Route( 159 uri, 160 expectedRoute, 161 altSvc, 162 { delayMs = 500 } = {} 163 ) { 164 let listenerRef; 165 166 // Function to (re)open the channel using the same listener instance. 167 const retry = () => { 168 const chan = makeChan(uri); 169 if (altSvc) { 170 chan.setRequestHeader("x-altsvc", altSvc, false); 171 } 172 chan.asyncOpen(listenerRef); 173 }; 174 175 const { listener, promise } = createWaitForHttp3Listener({ 176 expectedStatus: Cr.NS_OK, 177 expectedRoute, 178 uri, 179 h3AltSvc: altSvc, 180 retry, 181 delayMs, 182 }); 183 listenerRef = listener; 184 185 // Kick off first attempt; subsequent attempts are scheduled by the listener. 186 retry(); 187 188 // Resolves only when routed === expectedRoute 189 return promise; 190 } 191 192 // Promise-backed MultipleListener 193 class MultipleListener { 194 constructor( 195 { 196 number_of_parallel_requests = 0, 197 expectedRoute = "", 198 with_error = Cr.NS_OK, // NS_OK means we expect success for all 199 } = {}, 200 resolve, 201 reject 202 ) { 203 this.number_of_parallel_requests = number_of_parallel_requests; 204 this.expectedRoute = expectedRoute; 205 this.with_error = with_error; 206 207 this.count_of_done_requests = 0; 208 this.error_found_onstart = false; 209 this.error_found_onstop = false; 210 this.need_cancel_found = false; 211 212 this._resolve = resolve; 213 this._reject = reject; 214 } 215 216 onStartRequest(request) { 217 Assert.ok(request instanceof Ci.nsIHttpChannel); 218 219 // Optional cancel behavior via header "CancelMe" 220 let need_cancel = ""; 221 try { 222 need_cancel = request.getRequestHeader("CancelMe"); 223 } catch (_) {} 224 if (need_cancel !== "") { 225 this.need_cancel_found = true; 226 request.cancel(Cr.NS_ERROR_ABORT); 227 return; 228 } 229 230 // Original logic: either 200 OK for success, or exactly one failure 231 if (Components.isSuccessCode(request.status)) { 232 Assert.equal(request.responseStatus, 200); 233 } else if (this.error_found_onstart) { 234 // Fail fast: more than one failing request on start 235 this._reject?.( 236 new Error("We should have only one request failing (onStart).") 237 ); 238 } else { 239 Assert.equal(request.status, this.with_error); 240 this.error_found_onstart = true; 241 } 242 } 243 244 onDataAvailable(request, stream, off, cnt) { 245 read_stream(stream, cnt); 246 } 247 248 onStopRequest(request) { 249 // Check Alt-Used routing matches expectation 250 let routed = ""; 251 try { 252 routed = request.getRequestHeader("Alt-Used"); 253 } catch (_) {} 254 Assert.equal(routed, this.expectedRoute); 255 256 // If success, ensure HTTP/3 257 if (Components.isSuccessCode(request.status)) { 258 let httpVersion = ""; 259 try { 260 httpVersion = request.protocolVersion; 261 } catch (_) {} 262 Assert.equal(httpVersion, "h3"); 263 } 264 265 // Track/validate failures (at most one) 266 if (!Components.isSuccessCode(request.status)) { 267 if (this.error_found_onstop) { 268 this._reject?.( 269 new Error("We should have only one request failing (onStop).") 270 ); 271 return; 272 } 273 Assert.equal(request.status, this.with_error); 274 this.error_found_onstop = true; 275 } 276 277 // Count completion and maybe resolve 278 this.count_of_done_requests++; 279 if (this.count_of_done_requests === this.number_of_parallel_requests) { 280 if (Components.isSuccessCode(this.with_error)) { 281 // All were expected to succeed 282 Assert.equal(this.error_found_onstart, false); 283 Assert.equal(this.error_found_onstop, false); 284 } else { 285 // One failure was expected OR a cancel path was exercised 286 Assert.ok(this.error_found_onstart || this.need_cancel_found); 287 Assert.equal(this.error_found_onstop, true); 288 } 289 this._resolve?.(); 290 } 291 } 292 } 293 294 // Factory to create { listener, promise } 295 function createMultipleListener(options = {}) { 296 let resolve, reject; 297 const promise = new Promise((res, rej) => { 298 resolve = res; 299 reject = rej; 300 }); 301 const listener = new MultipleListener(options, resolve, reject); 302 return { listener, promise }; 303 } 304 305 async function do_test_multiple_requests( 306 number_of_parallel_requests, 307 h3Route, 308 httpsOrigin 309 ) { 310 dump("test_multiple_requests()\n"); 311 312 const { listener, promise } = createMultipleListener({ 313 number_of_parallel_requests, 314 expectedRoute: h3Route, 315 with_error: Cr.NS_OK, 316 }); 317 318 for (let i = 0; i < number_of_parallel_requests; i++) { 319 const chan = makeChan(httpsOrigin + "20000"); 320 chan.asyncOpen(listener); 321 } 322 323 await promise; 324 } 325 326 async function do_test_request_cancelled_by_server(h3Route, httpsOrigin) { 327 dump("do_test_request_cancelled_by_server()\n"); 328 329 const { listener, promise } = createHttp3CheckListener({ 330 expectedStatus: Cr.NS_ERROR_NET_INTERRUPT, 331 expectedRoute: h3Route, 332 }); 333 334 const chan = makeChan(httpsOrigin + "RequestCancelled"); 335 chan.asyncOpen(listener); 336 337 // Resolves at the point where run_next_test() used to be called 338 await promise; 339 } 340 341 // Promise-backed Http3CheckListener must already exist: 342 // createHttp3CheckListener({ expectedStatus, expectedRoute }) 343 344 class CancelRequestListener extends Http3CheckListener { 345 constructor({ expectedRoute = "" } = {}, resolve, reject) { 346 super( 347 { expectedStatus: Cr.NS_ERROR_ABORT, expectedRoute }, 348 resolve, 349 reject 350 ); 351 } 352 353 onStartRequest(request) { 354 Assert.ok(request instanceof Ci.nsIHttpChannel); 355 Assert.equal(Components.isSuccessCode(request.status), true); 356 // Cancel the request immediately (simulate Necko cancelling) 357 request.cancel(Cr.NS_ERROR_ABORT); 358 } 359 } 360 361 function createCancelRequestListener(options = {}) { 362 let resolve, reject; 363 const promise = new Promise((res, rej) => { 364 resolve = res; 365 reject = rej; 366 }); 367 const listener = new CancelRequestListener(options, resolve, reject); 368 return { listener, promise }; 369 } 370 371 // Cancel stream after OnStartRequest. 372 async function do_test_stream_cancelled_by_necko(h3Route, httpsOrigin) { 373 dump("do_test_stream_cancelled_by_necko()\n"); 374 375 const { listener, promise } = createCancelRequestListener({ 376 expectedRoute: h3Route, 377 }); 378 379 const chan = makeChan(httpsOrigin + "20000"); 380 chan.asyncOpen(listener); 381 382 // Resolves at the end of onStopRequest (where run_next_test() used to be) 383 await promise; 384 } 385 386 async function do_test_multiple_request_one_is_cancelled( 387 number_of_parallel_requests, 388 h3Route, 389 httpsOrigin 390 ) { 391 dump("do_test_multiple_request_one_is_cancelled()\n"); 392 393 const { listener, promise } = createMultipleListener({ 394 number_of_parallel_requests, 395 expectedRoute: h3Route, 396 with_error: Cr.NS_ERROR_NET_INTERRUPT, // one request is expected to fail (server-cancelled) 397 }); 398 399 for (let i = 0; i < number_of_parallel_requests; i++) { 400 let uri = httpsOrigin + "20000"; 401 if (i === 4) { 402 // Add a request that will be cancelled by the server. 403 uri = httpsOrigin + "RequestCancelled"; 404 } 405 const chan = makeChan(uri); 406 chan.asyncOpen(listener); 407 } 408 409 // Resolves when all parallel requests complete and invariants are checked 410 await promise; 411 } 412 413 async function do_test_multiple_request_one_is_cancelled_by_necko( 414 number_of_parallel_requests, 415 h3Route, 416 httpsOrigin 417 ) { 418 dump("do_test_multiple_request_one_is_cancelled_by_necko()\n"); 419 420 const { listener, promise } = createMultipleListener({ 421 number_of_parallel_requests, 422 expectedRoute: h3Route, 423 with_error: Cr.NS_ERROR_ABORT, 424 }); 425 426 for (let i = 0; i < number_of_parallel_requests; i++) { 427 let chan = makeChan(httpsOrigin + "20000"); 428 if (i === 4) { 429 // MultipleListener will cancel request with this header. 430 chan.setRequestHeader("CancelMe", "true", false); 431 } 432 chan.asyncOpen(listener); 433 } 434 435 // Resolves when all parallel requests complete and invariants are checked 436 await promise; 437 } 438 439 // Promise-backed Http3CheckListener assumed available: 440 // function createHttp3CheckListener({ expectedStatus, expectedRoute }) 441 442 class PostListener extends Http3CheckListener { 443 constructor(opts = {}, resolve, reject) { 444 super(opts, resolve, reject); 445 } 446 onDataAvailable(request, stream, off, cnt) { 447 this.onDataAvailableFired = true; 448 read_stream(stream, cnt); 449 } 450 } 451 452 // Factory for PostListener 453 function createPostListener(options = {}) { 454 let resolve, reject; 455 const promise = new Promise((res, rej) => { 456 resolve = res; 457 reject = rej; 458 }); 459 const listener = new PostListener(options, resolve, reject); 460 return { listener, promise }; 461 } 462 463 // Helper to perform a POST (or any method with a body) 464 function openWithBody( 465 content, 466 chan, 467 method = "POST", 468 contentType = "text/plain" 469 ) { 470 const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( 471 Ci.nsIStringInputStream 472 ); 473 stream.setByteStringData(content); 474 475 const uchan = chan.QueryInterface(Ci.nsIUploadChannel); 476 uchan.setUploadStream(stream, contentType, stream.available()); 477 chan.requestMethod = method; 478 return chan; 479 } 480 481 // Generate a post with known pre-calculated md5 sum. 482 function generateContent(size) { 483 let content = ""; 484 for (let i = 0; i < size; i++) { 485 content += "0"; 486 } 487 return content; 488 } 489 490 let post = generateContent(10); 491 492 // Test a simple POST (async) 493 async function do_test_post(httpsOrigin, h3Route) { 494 dump("do_test_post()\n"); 495 496 const chan = makeChan(httpsOrigin + "post"); 497 openWithBody(post, chan, "POST"); 498 499 const { listener, promise } = createPostListener({ 500 expectedStatus: Cr.NS_OK, 501 expectedRoute: h3Route, 502 }); 503 504 chan.asyncOpen(listener); 505 await promise; // resolves at end of onStopRequest in Http3CheckListener 506 } 507 508 // Test a simple PATCH 509 async function do_test_patch(httpsOrigin, h3Route) { 510 dump("do_test_post()\n"); 511 512 const chan = makeChan(httpsOrigin + "patch"); 513 openWithBody(post, chan, "PATCH"); 514 515 const { listener, promise } = createPostListener({ 516 expectedStatus: Cr.NS_OK, 517 expectedRoute: h3Route, 518 }); 519 520 chan.asyncOpen(listener); 521 await promise; 522 } 523 524 let h1Server = null; 525 let altsvcHost = ""; 526 let httpOrigin = ""; 527 528 function h1Response(metadata, response) { 529 response.setStatusLine(metadata.httpVersion, 200, "OK"); 530 response.setHeader("Content-Type", "text/plain", false); 531 response.setHeader("Connection", "close", false); 532 response.setHeader("Cache-Control", "no-cache", false); 533 response.setHeader("Access-Control-Allow-Origin", "*", false); 534 response.setHeader("Access-Control-Allow-Method", "GET", false); 535 response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); 536 537 try { 538 let hval = "h3=" + metadata.getHeader("x-altsvc"); 539 response.setHeader("Alt-Svc", hval, false); 540 } catch (e) {} 541 542 let body = "Q: What did 0 say to 8? A: Nice Belt!\n"; 543 response.bodyOutputStream.write(body, body.length); 544 } 545 546 function h1ServerWK(metadata, response) { 547 response.setStatusLine(metadata.httpVersion, 200, "OK"); 548 response.setHeader("Content-Type", "application/json", false); 549 response.setHeader("Connection", "close", false); 550 response.setHeader("Cache-Control", "no-cache", false); 551 response.setHeader("Access-Control-Allow-Origin", "*", false); 552 response.setHeader("Access-Control-Allow-Method", "GET", false); 553 response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); 554 555 let body = `["http://${altsvcHost}:${h1Server.identity.primaryPort}"]`; 556 response.bodyOutputStream.write(body, body.length); 557 } 558 559 function setup_h1_server(host) { 560 altsvcHost = host; 561 h1Server = new HttpServer(); 562 h1Server.registerPathHandler("/http3-test", h1Response); 563 h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); 564 h1Server.registerPathHandler("/VersionFallback", h1Response); 565 h1Server.start(-1); 566 h1Server.identity.setPrimary( 567 "http", 568 altsvcHost, 569 h1Server.identity.primaryPort 570 ); 571 httpOrigin = `http://${altsvcHost}:${h1Server.identity.primaryPort}/`; 572 registerCleanupFunction(() => { 573 h1Server.stop(); 574 }); 575 } 576 577 // Promise-backed base assumed available: 578 // class Http3CheckListener { ... } 579 // function createHttp3CheckListener(opts) { return { listener, promise }; } 580 581 class SlowReceiverListener extends Http3CheckListener { 582 constructor( 583 { 584 expectedStatus = Cr.NS_OK, 585 expectedRoute = "", 586 expectedBytes = 10_000_000, 587 } = {}, 588 resolve, 589 reject 590 ) { 591 super({ expectedStatus, expectedRoute }, resolve, reject); 592 this.count = 0; 593 this.expectedBytes = expectedBytes; 594 } 595 596 onDataAvailable(request, stream, off, cnt) { 597 this.onDataAvailableFired = true; 598 this.count += cnt; 599 read_stream(stream, cnt); 600 } 601 602 onStopRequest(request, status) { 603 Assert.equal(status, this.expectedStatus); 604 Assert.equal(this.count, this.expectedBytes); 605 606 let routed = "NA"; 607 try { 608 routed = request.getRequestHeader("Alt-Used"); 609 } catch (e) {} 610 dump(`routed is ${routed}\n`); 611 Assert.equal(routed, this.expectedRoute); 612 613 if (Components.isSuccessCode(this.expectedStatus)) { 614 let httpVersion = ""; 615 try { 616 httpVersion = request.protocolVersion; 617 } catch (e) {} 618 Assert.equal(httpVersion, "h3"); 619 Assert.equal(this.onDataAvailableFired, true); 620 } 621 622 // Resolve where run_next_test() used to be 623 this._resolve?.(request); 624 } 625 } 626 627 function createSlowReceiverListener(options = {}) { 628 let resolve, reject; 629 const promise = new Promise((res, rej) => { 630 resolve = res; 631 reject = rej; 632 }); 633 const listener = new SlowReceiverListener(options, resolve, reject); 634 return { listener, promise }; 635 } 636 637 // Test: slow receiver (suspend, then resume) 638 async function do_test_slow_receiver(httpsOrigin, h3Route) { 639 dump("do_test_slow_receiver()\n"); 640 641 const chan = makeChan(httpsOrigin + "10000000"); 642 643 const { listener, promise } = createSlowReceiverListener({ 644 expectedStatus: Cr.NS_OK, 645 expectedRoute: h3Route, 646 expectedBytes: 10_000_000, 647 }); 648 649 chan.asyncOpen(listener); 650 651 // Suspend immediately, then resume after 1s (replaces do_test_pending/do_timeout) 652 chan.suspend(); 653 await new Promise(r => do_timeout(1000, r)); 654 chan.resume(); 655 656 // Wait for completion (used to be run_next_test/do_test_finished) 657 await promise; 658 } 659 660 // Promise-backed listener for version fallback checks 661 class CheckFallbackListener { 662 constructor(resolve, reject) { 663 this._resolve = resolve; 664 this._reject = reject; 665 } 666 667 onStartRequest(request) { 668 Assert.ok(request instanceof Ci.nsIHttpChannel); 669 Assert.equal(request.status, Cr.NS_OK); 670 Assert.equal(request.responseStatus, 200); 671 } 672 673 onDataAvailable(request, stream, off, cnt) { 674 read_stream(stream, cnt); 675 } 676 677 onStopRequest(request, status) { 678 Assert.equal(status, Cr.NS_OK); 679 680 let routed = "NA"; 681 try { 682 routed = request.getRequestHeader("Alt-Used"); 683 } catch (e) {} 684 dump(`routed is ${routed}\n`); 685 Assert.equal(routed, "0"); 686 687 let httpVersion = ""; 688 try { 689 httpVersion = request.protocolVersion; 690 } catch (e) {} 691 Assert.equal(httpVersion, "http/1.1"); 692 693 // Resolve where run_next_test() used to be called 694 this._resolve?.(request); 695 } 696 } 697 698 // Factory to create { listener, promise } 699 function createCheckFallbackListener() { 700 let resolve, reject; 701 const promise = new Promise((res, rej) => { 702 resolve = res; 703 reject = rej; 704 }); 705 const listener = new CheckFallbackListener(resolve, reject); 706 return { listener, promise }; 707 } 708 709 // Server cancels request with VersionFallback. 710 async function do_test_version_fallback(httpsOrigin) { 711 dump("do_test_version_fallback()\n"); 712 713 const chan = makeChan(httpsOrigin + "VersionFallback"); 714 const { listener, promise } = createCheckFallbackListener(); 715 716 chan.asyncOpen(listener); 717 718 await promise; 719 }