test_altsvc_http3.js (15373B)
1 "use strict"; 2 3 const { HttpServer } = ChromeUtils.importESModule( 4 "resource://testing-common/httpd.sys.mjs" 5 ); 6 7 var h2Port; 8 var h3Port; 9 10 // https://foo.example.com:(h3Port) 11 // https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo 12 var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort) 13 var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort) 14 15 var otherServer; // server socket listening for other connection. 16 17 var h2FooRoute; // foo.example.com:H2PORT 18 19 var h3FooRoute; // foo.example.com:H3PORT 20 var h3BarRoute; // bar.example.com:H3PORT 21 var h3Route; // :H3PORT 22 var httpFooOrigin; // http://foo.exmaple.com:PORT/ 23 var httpsFooOrigin; // https://foo.exmaple.com:PORT/ 24 var httpBarOrigin; // http://bar.example.com:PORT/ 25 var httpsBarOrigin; // https://bar.example.com:PORT/ 26 27 function run_test() { 28 h2Port = Services.env.get("MOZHTTP2_PORT"); 29 Assert.notEqual(h2Port, null); 30 Assert.notEqual(h2Port, ""); 31 32 h3Port = Services.env.get("MOZHTTP3_PORT"); 33 Assert.notEqual(h3Port, null); 34 Assert.notEqual(h3Port, ""); 35 36 // Set to allow the cert presented by our H3 server 37 do_get_profile(); 38 39 Services.prefs.setBoolPref("network.http.http2.enabled", true); 40 Services.prefs.setBoolPref("network.http.http3.enable", true); 41 Services.prefs.setBoolPref("network.http.altsvc.enabled", true); 42 Services.prefs.setBoolPref("network.http.altsvc.oe", true); 43 Services.prefs.setCharPref( 44 "network.dns.localDomains", 45 "foo.example.com, bar.example.com" 46 ); 47 48 // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem 49 // so add that cert to the trust list as a signing cert. The same cert is used 50 // for both h3FooRoute and h3BarRoute though it is only valid for 51 // the foo.example.com domain name. 52 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 53 Ci.nsIX509CertDB 54 ); 55 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 56 57 h1Foo = new HttpServer(); 58 h1Foo.registerPathHandler("/altsvc-test", h1Server); 59 h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); 60 h1Foo.start(-1); 61 h1Foo.identity.setPrimary( 62 "http", 63 "foo.example.com", 64 h1Foo.identity.primaryPort 65 ); 66 67 h1Bar = new HttpServer(); 68 h1Bar.registerPathHandler("/altsvc-test", h1Server); 69 h1Bar.start(-1); 70 h1Bar.identity.setPrimary( 71 "http", 72 "bar.example.com", 73 h1Bar.identity.primaryPort 74 ); 75 76 h2FooRoute = "foo.example.com:" + h2Port; 77 78 h3FooRoute = "foo.example.com:" + h3Port; 79 h3BarRoute = "bar.example.com:" + h3Port; 80 h3Route = ":" + h3Port; 81 82 httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/"; 83 httpsFooOrigin = "https://" + h3FooRoute + "/"; 84 httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/"; 85 httpsBarOrigin = "https://" + h3BarRoute + "/"; 86 dump( 87 "http foo - " + 88 httpFooOrigin + 89 "\n" + 90 "https foo - " + 91 httpsFooOrigin + 92 "\n" + 93 "http bar - " + 94 httpBarOrigin + 95 "\n" + 96 "https bar - " + 97 httpsBarOrigin + 98 "\n" 99 ); 100 101 doTest1(); 102 } 103 104 function h1Server(metadata, response) { 105 response.setStatusLine(metadata.httpVersion, 200, "OK"); 106 response.setHeader("Content-Type", "text/plain", false); 107 response.setHeader("Connection", "close", false); 108 response.setHeader("Cache-Control", "no-cache", false); 109 response.setHeader("Access-Control-Allow-Origin", "*", false); 110 response.setHeader("Access-Control-Allow-Method", "GET", false); 111 response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); 112 113 try { 114 // If needed, prefix Alt-Svc with "h3=". 115 if (metadata.getHeader("x-altsvc").includes("=")) { 116 response.setHeader("Alt-Svc", metadata.getHeader("x-altsvc"), false); 117 } else { 118 response.setHeader( 119 "Alt-Svc", 120 "h3=" + metadata.getHeader("x-altsvc"), 121 false 122 ); 123 } 124 } catch (e) {} 125 126 var body = "Q: What did 0 say to 8? A: Nice Belt!\n"; 127 response.bodyOutputStream.write(body, body.length); 128 } 129 130 function h1ServerWK(metadata, response) { 131 response.setStatusLine(metadata.httpVersion, 200, "OK"); 132 response.setHeader("Content-Type", "application/json", false); 133 response.setHeader("Connection", "close", false); 134 response.setHeader("Cache-Control", "no-cache", false); 135 response.setHeader("Access-Control-Allow-Origin", "*", false); 136 response.setHeader("Access-Control-Allow-Method", "GET", false); 137 response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); 138 139 var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]'; 140 response.bodyOutputStream.write(body, body.length); 141 } 142 143 function resetPrefs() { 144 Services.prefs.clearUserPref("network.http.http2.enabled"); 145 Services.prefs.clearUserPref("network.http.http3.enable"); 146 Services.prefs.clearUserPref("network.dns.localDomains"); 147 Services.prefs.clearUserPref("network.http.altsvc.enabled"); 148 Services.prefs.clearUserPref("network.http.altsvc.oe"); 149 Services.prefs.clearUserPref("network.dns.localDomains"); 150 Services.prefs.clearUserPref("network.security.ports.banned"); 151 } 152 153 function makeChan(origin) { 154 return NetUtil.newChannel({ 155 uri: origin + "altsvc-test", 156 loadUsingSystemPrincipal: true, 157 }).QueryInterface(Ci.nsIHttpChannel); 158 } 159 160 var origin; 161 var xaltsvc; 162 var loadWithoutClearingMappings = false; 163 var disallowH3 = false; 164 var disallowH2 = false; 165 var testKeepAliveNotSet = false; 166 var nextTest; 167 var expectPass = true; 168 var waitFor = 0; 169 var originAttributes = {}; 170 171 var Listener = function (expectedHttpVersion, expectedRoute) { 172 this._expectedRoute = expectedRoute; 173 this._expectedHttpVersion = expectedHttpVersion; 174 }; 175 176 Listener.prototype = { 177 onStartRequest: function testOnStartRequest(request) { 178 Assert.ok(request instanceof Ci.nsIHttpChannel); 179 180 if (expectPass) { 181 if (!Components.isSuccessCode(request.status)) { 182 do_throw( 183 "Channel should have a success code! (" + request.status + ")" 184 ); 185 } 186 Assert.equal(request.responseStatus, 200); 187 } else { 188 Assert.equal(Components.isSuccessCode(request.status), false); 189 } 190 }, 191 192 onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { 193 read_stream(stream, cnt); 194 }, 195 196 onStopRequest: function testOnStopRequest(request, status) { 197 var routed = ""; 198 try { 199 routed = request.getRequestHeader("Alt-Used"); 200 } catch (e) {} 201 dump("routed is " + routed + "\n"); 202 Assert.equal(Components.isSuccessCode(status), expectPass); 203 204 function assertHttpVersion(request, expectedHttpVersion) { 205 if (expectedHttpVersion) { 206 const httpVersion = request?.protocolVersion || ""; 207 Assert.equal(httpVersion, httpVersion); 208 } 209 } 210 211 if (waitFor != 0) { 212 Assert.equal(routed, ""); 213 do_test_pending(); 214 loadWithoutClearingMappings = true; 215 do_timeout(waitFor, () => { 216 doTest(this._expectedHttpVersion, this._expectedRoute); 217 }); 218 waitFor = 0; 219 xaltsvc = "NA"; 220 } else if (xaltsvc == "NA") { 221 Assert.equal(routed, ""); 222 nextTest(); 223 } else if (this._expectedRoute && this._expectedRoute == routed) { 224 assertHttpVersion(request, this._expectedHttpVersion); 225 nextTest(); 226 } else if (routed == xaltsvc) { 227 Assert.equal(routed, xaltsvc); // always true, but a useful log 228 assertHttpVersion(request, this._expectedHttpVersion); 229 nextTest(); 230 } else { 231 dump("poll later for alt svc mapping\n"); 232 do_test_pending(); 233 loadWithoutClearingMappings = true; 234 do_timeout(500, () => { 235 doTest(this._expectedHttpVersion, this._expectedRoute); 236 }); 237 } 238 239 do_test_finished(); 240 }, 241 }; 242 243 function testsDone() { 244 dump("testDone\n"); 245 resetPrefs(); 246 do_test_pending(); 247 otherServer.close(); 248 do_test_pending(); 249 h1Foo.stop(do_test_finished); 250 do_test_pending(); 251 h1Bar.stop(do_test_finished); 252 } 253 254 function doTest(expectedHttpVersion, expectedRoute) { 255 dump("execute doTest " + origin + "\n"); 256 var chan = makeChan(origin); 257 var listener = new Listener(expectedHttpVersion, expectedRoute); 258 if (xaltsvc != "NA") { 259 chan.setRequestHeader("x-altsvc", xaltsvc, false); 260 } 261 if (testKeepAliveNotSet) { 262 chan.setRequestHeader("Connection", "close", false); 263 testKeepAliveNotSet = false; 264 } 265 if (loadWithoutClearingMappings) { 266 chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; 267 } else { 268 chan.loadFlags = 269 Ci.nsIRequest.LOAD_FRESH_CONNECTION | 270 Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; 271 } 272 if (disallowH3) { 273 let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); 274 internalChannel.allowHttp3 = false; 275 disallowH3 = false; 276 } 277 if (disallowH2) { 278 let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); 279 internalChannel.allowSpdy = false; 280 disallowH2 = false; 281 } 282 loadWithoutClearingMappings = false; 283 chan.loadInfo.originAttributes = originAttributes; 284 chan.asyncOpen(listener); 285 } 286 287 // xaltsvc is overloaded to do two things.. 288 // 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header 289 // 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route) 290 // 291 // When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used, 292 // which is always explicit, so it needs to be changed after the channel is created but before the 293 // listener is invoked 294 295 // http://foo served from h3=:port 296 function doTest1() { 297 dump("doTest1()\n"); 298 origin = httpFooOrigin; 299 xaltsvc = h3Route; 300 nextTest = doTest2; 301 do_test_pending(); 302 doTest("h3"); 303 xaltsvc = h3FooRoute; 304 } 305 306 // http://foo served from h3=foo:port 307 function doTest2() { 308 dump("doTest2()\n"); 309 origin = httpFooOrigin; 310 xaltsvc = h3FooRoute; 311 nextTest = doTest3; 312 do_test_pending(); 313 doTest("h3"); 314 } 315 316 // http://foo served from h3=bar:port 317 // requires cert for foo 318 function doTest3() { 319 dump("doTest3()\n"); 320 origin = httpFooOrigin; 321 xaltsvc = h3BarRoute; 322 nextTest = doTest4; 323 do_test_pending(); 324 doTest("h3"); 325 } 326 327 // https://bar should fail because host bar has cert for foo 328 function doTest4() { 329 dump("doTest4()\n"); 330 origin = httpsBarOrigin; 331 xaltsvc = ""; 332 expectPass = false; 333 nextTest = doTest5; 334 do_test_pending(); 335 doTest(); 336 } 337 338 // http://bar via h3 on bar 339 // should not use TLS/h3 because h3BarRoute is not auth'd for bar 340 // however the test ought to PASS (i.e. get a 200) because fallback 341 // to plaintext happens.. thus the timeout 342 function doTest5() { 343 dump("doTest5()\n"); 344 origin = httpBarOrigin; 345 xaltsvc = h3BarRoute; 346 expectPass = true; 347 waitFor = 500; 348 nextTest = doTest6; 349 do_test_pending(); 350 doTest("h3"); 351 } 352 353 // http://bar served from h3=:port, which is like the bar route in 8 354 function doTest6() { 355 dump("doTest6()\n"); 356 origin = httpBarOrigin; 357 xaltsvc = h3Route; 358 expectPass = true; 359 waitFor = 500; 360 nextTest = doTest7; 361 do_test_pending(); 362 doTest(); 363 xaltsvc = h3BarRoute; 364 } 365 366 // check again https://bar should fail because host bar has cert for foo 367 function doTest7() { 368 dump("doTest7()\n"); 369 origin = httpsBarOrigin; 370 xaltsvc = ""; 371 expectPass = false; 372 nextTest = doTest8; 373 do_test_pending(); 374 doTest(); 375 } 376 377 // http://bar served from h3=foo, should fail because host foo only has 378 // cert for foo. Fail in this case means alt-svc is not used, but content 379 // is served 380 function doTest8() { 381 dump("doTest8()\n"); 382 origin = httpBarOrigin; 383 xaltsvc = h3FooRoute; 384 expectPass = true; 385 waitFor = 500; 386 nextTest = doTest9; 387 do_test_pending(); 388 doTest("h3"); 389 } 390 391 // Test 9-12: 392 // Insert a cache of http://foo served from h3=:port with origin attributes. 393 function doTest9() { 394 dump("doTest9()\n"); 395 origin = httpFooOrigin; 396 xaltsvc = h3Route; 397 originAttributes = { 398 userContextId: 1, 399 firstPartyDomain: "a.com", 400 }; 401 nextTest = doTest10; 402 do_test_pending(); 403 doTest("h3"); 404 xaltsvc = h3FooRoute; 405 } 406 407 // Make sure we get a cache miss with a different userContextId. 408 function doTest10() { 409 dump("doTest10()\n"); 410 origin = httpFooOrigin; 411 xaltsvc = "NA"; 412 originAttributes = { 413 userContextId: 2, 414 firstPartyDomain: "a.com", 415 }; 416 loadWithoutClearingMappings = true; 417 nextTest = doTest11; 418 do_test_pending(); 419 doTest("h3"); 420 } 421 422 // Make sure we get a cache miss with a different firstPartyDomain. 423 function doTest11() { 424 dump("doTest11()\n"); 425 origin = httpFooOrigin; 426 xaltsvc = "NA"; 427 originAttributes = { 428 userContextId: 1, 429 firstPartyDomain: "b.com", 430 }; 431 loadWithoutClearingMappings = true; 432 nextTest = doTest12; 433 do_test_pending(); 434 doTest("h3"); 435 } 436 // 437 // Make sure we get a cache hit with the same origin attributes. 438 function doTest12() { 439 dump("doTest12()\n"); 440 origin = httpFooOrigin; 441 xaltsvc = "NA"; 442 originAttributes = { 443 userContextId: 1, 444 firstPartyDomain: "a.com", 445 }; 446 loadWithoutClearingMappings = true; 447 nextTest = doTest13; 448 do_test_pending(); 449 doTest("h3"); 450 // This ensures a cache hit. 451 xaltsvc = h3FooRoute; 452 } 453 454 // Make sure we do not use H3 if it is disabled on a channel. 455 function doTest13() { 456 dump("doTest13()\n"); 457 origin = httpFooOrigin; 458 xaltsvc = "NA"; 459 disallowH3 = true; 460 originAttributes = { 461 userContextId: 1, 462 firstPartyDomain: "a.com", 463 }; 464 loadWithoutClearingMappings = true; 465 nextTest = doTest14; 466 do_test_pending(); 467 doTest("h3"); 468 } 469 470 // Make sure we use H3 if only Http2 is disabled on a channel. 471 function doTest14() { 472 dump("doTest14()\n"); 473 origin = httpFooOrigin; 474 xaltsvc = "NA"; 475 disallowH2 = true; 476 originAttributes = { 477 userContextId: 1, 478 firstPartyDomain: "a.com", 479 }; 480 loadWithoutClearingMappings = true; 481 nextTest = doTest15; 482 do_test_pending(); 483 doTest("h3"); 484 // This should ensures a cache hit. 485 xaltsvc = h3FooRoute; 486 } 487 488 // Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set. 489 function doTest15() { 490 dump("doTest15()\n"); 491 origin = httpFooOrigin; 492 xaltsvc = "NA"; 493 testKeepAliveNotSet = true; 494 originAttributes = { 495 userContextId: 1, 496 firstPartyDomain: "a.com", 497 }; 498 loadWithoutClearingMappings = true; 499 nextTest = doTest16; 500 do_test_pending(); 501 doTest("h3"); 502 } 503 504 // Check we don't connect to blocked ports 505 function doTest16() { 506 dump("doTest16()\n"); 507 origin = httpFooOrigin; 508 otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( 509 Ci.nsIServerSocket 510 ); 511 otherServer.init(-1, true, -1); 512 xaltsvc = "localhost:" + otherServer.port; 513 Services.prefs.setCharPref( 514 "network.security.ports.banned", 515 "" + otherServer.port 516 ); 517 dump("Blocked port: " + otherServer.port); 518 waitFor = 500; 519 otherServer.asyncListen({ 520 onSocketAccepted() { 521 Assert.ok(false, "Got connection to socket when we didn't expect it!"); 522 }, 523 onStopListening() { 524 // We get closed when the entire file is done, which guarantees we get the socket accept 525 // if we do connect to the alt-svc header 526 do_test_finished(); 527 }, 528 }); 529 nextTest = doTest17; 530 do_test_pending(); 531 doTest("h3"); 532 } 533 534 // Make sure we do not use a draft QUIC version. 535 function doTest17() { 536 dump("doTest17()\n"); 537 origin = httpFooOrigin; 538 nextTest = testsDone; 539 xaltsvc = "h3-29=" + h3FooRoute + ", h2=" + h2FooRoute; 540 disallowH2 = false; 541 originAttributes = { 542 userContextId: 1, 543 firstPartyDomain: "a.com", 544 }; 545 loadWithoutClearingMappings = true; 546 do_test_pending(); 547 doTest("h2", h2FooRoute); 548 }