http3_proxy_common.js (16840B)
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 { 13 Http3ProxyFilter, 14 with_node_servers, 15 NodeHTTPServer, 16 NodeHTTPSServer, 17 NodeHTTP2Server, 18 NodeHTTP2ProxyServer, 19 NodeWebSocketHttp2Server, 20 WebSocketConnection, 21 } = ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); 22 23 function makeChan(uri) { 24 let chan = NetUtil.newChannel({ 25 uri, 26 loadUsingSystemPrincipal: true, 27 }).QueryInterface(Ci.nsIHttpChannel); 28 chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; 29 return chan; 30 } 31 32 function channelOpenPromise(chan, flags) { 33 return new Promise(resolve => { 34 function finish(req, buffer) { 35 resolve([req, buffer]); 36 } 37 chan.asyncOpen(new ChannelListener(finish, null, flags)); 38 }); 39 } 40 41 let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 42 let proxyHost; 43 let proxyPort; 44 let noResponsePort; 45 let proxyAuth; 46 let proxyFilter; 47 48 /** 49 * Sets up proxy filter to MASQUE H3 proxy 50 */ 51 async function setup_http3_proxy() { 52 Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); 53 Services.prefs.setBoolPref("network.dns.disableIPv6", true); 54 Services.prefs.setIntPref("network.webtransport.datagram_size", 1500); 55 Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); 56 Services.prefs.setIntPref("network.http.http3.max_gso_segments", 1); // TODO: fix underflow 57 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 58 Ci.nsIX509CertDB 59 ); 60 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 61 addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); 62 63 proxyHost = "foo.example.com"; 64 ({ masqueProxyPort: proxyPort, noResponsePort } = 65 await create_masque_proxy_server()); 66 proxyAuth = ""; 67 68 Assert.notEqual(proxyPort, null); 69 Assert.notEqual(proxyPort, ""); 70 71 // A dummy request to make sure AltSvcCache::mStorage is ready. 72 let chan = makeChan(`https://localhost`); 73 await channelOpenPromise(chan, CL_EXPECT_FAILURE); 74 75 proxyFilter = new Http3ProxyFilter( 76 proxyHost, 77 proxyPort, 78 0, 79 "/.well-known/masque/udp/{target_host}/{target_port}/", 80 proxyAuth 81 ); 82 pps.registerFilter(proxyFilter, 10); 83 84 registerCleanupFunction(() => { 85 Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); 86 }); 87 } 88 89 /** 90 * Tests HTTP connect through H3 proxy to HTTP, HTTPS and H2 servers 91 * Makes multiple requests. Expects success. 92 */ 93 async function test_http_connect() { 94 info("Running test_http_connect"); 95 await with_node_servers( 96 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 97 async server => { 98 info(`Proxying to ${server.constructor.name} server`); 99 await server.registerPathHandler("/first", (req, resp) => { 100 resp.writeHead(200); 101 resp.end("first"); 102 }); 103 await server.registerPathHandler("/second", (req, resp) => { 104 resp.writeHead(200); 105 resp.end("second"); 106 }); 107 await server.registerPathHandler("/third", (req, resp) => { 108 resp.writeHead(200); 109 resp.end("third"); 110 }); 111 let chan = makeChan( 112 `${server.protocol()}://alt1.example.com:${server.port()}/first` 113 ); 114 let [req, buf] = await channelOpenPromise( 115 chan, 116 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 117 ); 118 Assert.equal(req.status, Cr.NS_OK); 119 Assert.equal(buf, "first"); 120 chan = makeChan( 121 `${server.protocol()}://alt1.example.com:${server.port()}/second` 122 ); 123 [req, buf] = await channelOpenPromise( 124 chan, 125 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 126 ); 127 Assert.equal(req.status, Cr.NS_OK); 128 Assert.equal(buf, "second"); 129 130 chan = makeChan( 131 `${server.protocol()}://alt1.example.com:${server.port()}/third` 132 ); 133 [req, buf] = await channelOpenPromise( 134 chan, 135 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 136 ); 137 Assert.equal(req.status, Cr.NS_OK); 138 Assert.equal(buf, "third"); 139 } 140 ); 141 } 142 143 /** 144 * Test HTTP CONNECT authentication failure - tests behavior when proxy 145 * authentication is required but not provided or incorrect 146 */ 147 async function test_http_connect_auth_failure() { 148 info("Running test_http_connect_auth_failure"); 149 await with_node_servers( 150 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 151 async server => { 152 info(`Testing auth failure with ${server.constructor.name} server`); 153 // Register a handler that requires authentication 154 await server.registerPathHandler("/auth-required", (req, resp) => { 155 const auth = req.headers.authorization; 156 if (!auth || auth !== "Basic dGVzdDp0ZXN0") { 157 resp.writeHead(401, { 158 "WWW-Authenticate": 'Basic realm="Test Realm"', 159 "Content-Type": "text/plain", 160 }); 161 resp.end(""); 162 } else { 163 resp.writeHead(200); 164 resp.end("Authenticated"); 165 } 166 }); 167 168 let chan = makeChan( 169 `${server.protocol()}://alt1.example.com:${server.port()}/auth-required` 170 ); 171 let [req] = await channelOpenPromise( 172 chan, 173 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 174 ); 175 176 // Should receive 401 Unauthorized through the tunnel 177 Assert.equal(req.status, Cr.NS_OK); 178 Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 401); 179 } 180 ); 181 } 182 183 /** 184 * Test HTTP CONNECT with large request/response data - ensures the tunnel 185 * can handle substantial data transfer without corruption or truncation 186 */ 187 async function test_http_connect_large_data() { 188 info("Running test_http_connect_large_data"); 189 await with_node_servers( 190 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 191 async server => { 192 info( 193 `Testing large data transfer with ${server.constructor.name} server` 194 ); 195 // Create a large response payload (1MB of data) 196 const largeData = "x".repeat(1024 * 1024); 197 await server.registerPathHandler("/large", (req, resp) => { 198 const largeData = "x".repeat(1024 * 1024); 199 resp.writeHead(200, { "Content-Type": "text/plain" }); 200 resp.end(largeData); 201 }); 202 203 let chan = makeChan( 204 `${server.protocol()}://alt1.example.com:${server.port()}/large` 205 ); 206 let [req, buf] = await channelOpenPromise( 207 chan, 208 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 209 ); 210 211 Assert.equal(req.status, Cr.NS_OK); 212 Assert.equal(buf.length, largeData.length); 213 Assert.equal(buf, largeData); 214 } 215 ); 216 } 217 218 /** 219 * Test HTTP CONNECT tunnel connection refused - simulates target server 220 * being unreachable or refusing connections 221 */ 222 async function test_http_connect_connection_refused() { 223 info("Running test_http_connect_connection_refused"); 224 // Test connecting to a port that's definitely not in use 225 let chan = makeChan(`http://alt1.example.com:667/refused`); 226 let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); 227 228 // Should fail to establish tunnel connection 229 Assert.notEqual(req.status, Cr.NS_OK); 230 info(`Connection refused status: ${req.status}`); 231 } 232 233 /** 234 * Test HTTP CONNECT with invalid target host - verifies proper error handling 235 * when trying to tunnel to a non-existent hostname 236 */ 237 async function test_http_connect_invalid_host() { 238 info("Running test_http_connect_invalid_host"); 239 let chan = makeChan(`http://nonexistent.invalid.example/test`); 240 let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); 241 242 // Should fail DNS resolution for invalid hostname 243 Assert.notEqual(req.status, Cr.NS_OK); 244 info(`Invalid host status: ${req.status}`); 245 } 246 247 /** 248 * Test concurrent HTTP CONNECT tunnels - ensures multiple simultaneous 249 * requests can be established and used independently through the same H3 proxy 250 */ 251 async function test_concurrent_http_connect_tunnels() { 252 info("Running test_concurrent_http_connect_tunnels"); 253 await with_node_servers( 254 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 255 async server => { 256 info(`Testing concurrent tunnels with ${server.constructor.name} server`); 257 258 // Register multiple endpoints 259 await server.registerPathHandler("/concurrent1", (req, resp) => { 260 resp.writeHead(200); 261 resp.end("response1"); 262 }); 263 await server.registerPathHandler("/concurrent2", (req, resp) => { 264 resp.writeHead(200); 265 resp.end("response2"); 266 }); 267 await server.registerPathHandler("/concurrent3", (req, resp) => { 268 resp.writeHead(200); 269 resp.end("response3"); 270 }); 271 272 // Create multiple concurrent requests through the tunnel 273 const promises = []; 274 for (let i = 1; i <= 3; i++) { 275 let chan = makeChan( 276 `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` 277 ); 278 promises.push( 279 channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL) 280 ); 281 } 282 283 const results = await Promise.all(promises); 284 285 // Verify all requests succeeded with correct responses 286 for (let i = 0; i < 3; i++) { 287 const [req, buf] = results[i]; 288 Assert.equal(req.status, Cr.NS_OK); 289 Assert.equal(buf, `response${i + 1}`); 290 } 291 info("All concurrent tunnels completed successfully"); 292 } 293 ); 294 } 295 296 /** 297 * Test HTTP CONNECT tunnel stream closure handling - verifies proper cleanup 298 * when the tunnel connection is closed unexpectedly 299 */ 300 // eslint-disable-next-line no-unused-vars 301 async function test_http_connect_stream_closure() { 302 info("Running test_http_connect_stream_closure"); 303 await with_node_servers([NodeHTTPServer], async server => { 304 info(`Testing stream closure with ${server.constructor.name} server`); 305 306 await server.registerPathHandler("/close", (req, resp) => { 307 // Send partial response then close connection abruptly 308 resp.writeHead(200, { "Content-Type": "text/plain" }); 309 resp.write("partial"); 310 // Simulate connection closure 311 resp.destroy(); 312 }); 313 314 let chan = makeChan( 315 `${server.protocol()}://alt1.example.com:${server.port()}/close` 316 ); 317 let [req] = await channelOpenPromise(chan, CL_EXPECT_FAILURE); 318 319 // Should handle connection closure gracefully 320 Assert.notEqual(req.status, Cr.NS_OK); 321 info(`Stream closure status: ${req.status}`); 322 }); 323 } 324 325 /** 326 * Test connect-udp - SUCCESS case. 327 * Will use h3 proxy to connect to h3 server. 328 */ 329 async function test_connect_udp() { 330 info("Running test_connect_udp"); 331 let h3Port = Services.env.get("MOZHTTP3_PORT"); 332 info(`h3Port = ${h3Port}`); 333 334 Services.prefs.setCharPref( 335 "network.http.http3.alt-svc-mapping-for-testing", 336 `alt1.example.com;h3=:${h3Port}` 337 ); 338 339 { 340 let chan = makeChan(`https://alt1.example.com:${h3Port}/no_body`); 341 let [req] = await channelOpenPromise( 342 chan, 343 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 344 ); 345 Assert.equal(req.protocolVersion, "h3"); 346 Assert.equal(req.status, Cr.NS_OK); 347 Assert.equal(req.responseStatus, 200); 348 } 349 } 350 351 async function test_http_connect_fallback() { 352 info("Running test_http_connect_fallback"); 353 pps.unregisterFilter(proxyFilter); 354 355 Services.prefs.setCharPref( 356 "network.http.http3.alt-svc-mapping-for-testing", 357 "" 358 ); 359 360 let proxyPort = noResponsePort; 361 let proxy = new NodeHTTP2ProxyServer(); 362 await proxy.startWithoutProxyFilter(proxyPort); 363 Assert.equal(proxyPort, proxy.port()); 364 dump(`proxy port=${proxy.port()}\n`); 365 366 let server = new NodeHTTP2Server(); 367 await server.start(); 368 369 // Register multiple endpoints 370 await server.registerPathHandler("/concurrent1", (req, resp) => { 371 resp.writeHead(200); 372 resp.end("response1"); 373 }); 374 await server.registerPathHandler("/concurrent2", (req, resp) => { 375 resp.writeHead(200); 376 resp.end("response2"); 377 }); 378 await server.registerPathHandler("/concurrent3", (req, resp) => { 379 resp.writeHead(200); 380 resp.end("response3"); 381 }); 382 383 let filter = new Http3ProxyFilter( 384 proxyHost, 385 proxy.port(), 386 0, 387 "/.well-known/masque/udp/{target_host}/{target_port}/", 388 proxyAuth 389 ); 390 pps.registerFilter(filter, 10); 391 392 registerCleanupFunction(async () => { 393 await proxy.stop(); 394 await server.stop(); 395 }); 396 397 // Create multiple concurrent requests through the tunnel 398 const promises = []; 399 for (let i = 1; i <= 3; i++) { 400 let chan = makeChan( 401 `${server.protocol()}://alt1.example.com:${server.port()}/concurrent${i}` 402 ); 403 promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); 404 } 405 406 const results = await Promise.all(promises); 407 408 // Verify all requests succeeded with correct responses 409 for (let i = 0; i < 3; i++) { 410 const [req, buf] = results[i]; 411 Assert.equal(req.status, Cr.NS_OK); 412 Assert.equal(buf, `response${i + 1}`); 413 } 414 415 let h3Port = server.port(); 416 console.log(`h3Port = ${h3Port}`); 417 418 Services.prefs.setCharPref( 419 "network.http.http3.alt-svc-mapping-for-testing", 420 `alt1.example.com;h3=:${h3Port}` 421 ); 422 423 let chan = makeChan(`https://alt1.example.com:${h3Port}/concurrent1`); 424 let [req] = await channelOpenPromise( 425 chan, 426 CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL 427 ); 428 Assert.equal(req.status, Cr.NS_OK); 429 Assert.equal(req.responseStatus, 200); 430 431 await proxy.stop(); 432 pps.unregisterFilter(filter); 433 await server.stop(); 434 } 435 436 /** 437 * Helper function to open a WebSocket connection 438 */ 439 async function wsChannelOpenPromise(url, msg) { 440 let conn = new WebSocketConnection(); 441 let statusObj = await Promise.race([conn.open(url), conn.finished()]); 442 if (statusObj && statusObj.status != Cr.NS_OK) { 443 return [statusObj.status, ""]; 444 } 445 let finalStatusPromise = conn.finished(); 446 conn.send(msg); 447 let res = await conn.receiveMessages(); 448 conn.close(); 449 let finalStatus = await finalStatusPromise; 450 return [finalStatus.status, res]; 451 } 452 453 /** 454 * Test WebSocket through H3 proxy using H2 WebSocket server 455 */ 456 async function test_http_connect_websocket() { 457 info("Running test_http_connect_websocket"); 458 Services.prefs.setBoolPref("network.http.http2.websockets", true); 459 460 let wss = new NodeWebSocketHttp2Server(); 461 await wss.start(); 462 463 registerCleanupFunction(async () => { 464 await wss.stop(); 465 }); 466 467 Assert.notEqual(wss.port(), null); 468 await wss.registerMessageHandler((data, ws) => { 469 ws.send(data); 470 }); 471 472 let url = `wss://alt1.example.com:${wss.port()}`; 473 const msg = "test h2 websocket through h3 proxy"; 474 let [status, res] = await wsChannelOpenPromise(url, msg); 475 Assert.equal(status, Cr.NS_OK); 476 Assert.deepEqual(res, [msg]); 477 478 // Test multiple messages 479 let conn = new WebSocketConnection(); 480 await conn.open(url); 481 conn.send("message1"); 482 let mess1 = await conn.receiveMessages(); 483 Assert.deepEqual(mess1, ["message1"]); 484 485 conn.send("message2"); 486 conn.send("message3"); 487 let mess2 = []; 488 while (mess2.length < 2) { 489 mess2 = mess2.concat(await conn.receiveMessages()); 490 } 491 Assert.deepEqual(mess2, ["message2", "message3"]); 492 493 conn.close(); 494 let { status: finalStatus } = await conn.finished(); 495 Assert.equal(finalStatus, Cr.NS_OK); 496 497 await wss.stop(); 498 } 499 500 async function test_inner_connection_fallback() { 501 info("Running test_inner_connection_fallback"); 502 let h3Port = Services.env.get("MOZHTTP3_PORT_NO_RESPONSE"); 503 info(`h3Port = ${h3Port}`); 504 505 // Register the connect-udp proxy. 506 pps.registerFilter(proxyFilter, 10); 507 508 let server = new NodeHTTPSServer(); 509 await server.start(h3Port); 510 511 // Register multiple endpoints 512 await server.registerPathHandler("/concurrent1", (req, resp) => { 513 resp.writeHead(200); 514 resp.end("fallback1"); 515 }); 516 await server.registerPathHandler("/concurrent2", (req, resp) => { 517 resp.writeHead(200); 518 resp.end("fallback2"); 519 }); 520 await server.registerPathHandler("/concurrent3", (req, resp) => { 521 resp.writeHead(200); 522 resp.end("fallback3"); 523 }); 524 registerCleanupFunction(async () => { 525 await server.stop(); 526 }); 527 528 Services.prefs.setCharPref( 529 "network.http.http3.alt-svc-mapping-for-testing", 530 `alt1.example.com;h3=:${h3Port}` 531 ); 532 533 // Create multiple concurrent requests through the tunnel 534 const promises = []; 535 for (let i = 1; i <= 3; i++) { 536 let chan = makeChan( 537 `${server.protocol()}://alt1.example.com:${h3Port}/concurrent${i}` 538 ); 539 promises.push(channelOpenPromise(chan, CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL)); 540 } 541 542 const results = await Promise.all(promises); 543 544 // Verify all requests succeeded with correct responses 545 for (let i = 0; i < 3; i++) { 546 const [req, buf] = results[i]; 547 Assert.equal(req.status, Cr.NS_OK); 548 Assert.equal(buf, `fallback${i + 1}`); 549 } 550 await server.stop(); 551 }