/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /* globals setTimeout */ const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); function makeChan(uri) { let chan = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; return chan; } add_setup(async function setup() { // See Bug 1878505 Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); registerCleanupFunction(async () => { Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); }); }); add_task(async function test_cancel_after_asyncOpen() { let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); let proxies = [ NodeHTTPProxyServer, NodeHTTPSProxyServer, NodeHTTP2ProxyServer, ]; for (let p of proxies) { let proxy = new p(); await proxy.start(); registerCleanupFunction(async () => { await proxy.stop(); }); await with_node_servers( [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], async server => { info(`Testing ${p.name} with ${server.constructor.name}`); await server.execute( `global.server_name = "${server.constructor.name}";` ); await server.registerPathHandler("/test", (req, resp) => { resp.writeHead(200); resp.end(global.server_name); }); let chan = makeChan(`${server.origin()}/test`); let openPromise = new Promise(resolve => { chan.asyncOpen( new ChannelListener( (req, buff) => resolve({ req, buff }), null, CL_EXPECT_FAILURE ) ); }); chan.cancel(Cr.NS_ERROR_ABORT); let { req } = await openPromise; Assert.equal(req.status, Cr.NS_ERROR_ABORT); } ); await proxy.stop(); } }); // const NS_NET_STATUS_CONNECTING_TO = 0x4b0007; // const NS_NET_STATUS_CONNECTED_TO = 0x4b0004; // const NS_NET_STATUS_SENDING_TO = 0x4b0005; const NS_NET_STATUS_WAITING_FOR = 0x4b000a; // 2152398858 const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006; // const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c; // 2152398860 // const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d; // 2152398861 add_task(async function test_cancel_after_connect_http2proxy() { let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); await with_node_servers( [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], async server => { // Set up a proxy for each server to make sure proxy state is clean // for each test. let proxy = new NodeHTTP2ProxyServer(); await proxy.start(); registerCleanupFunction(async () => { await proxy.stop(); }); await proxy.execute(` global.session_counter = 0; global.proxy.on("session", () => { global.session_counter++; }); `); info(`Testing ${proxy.constructor.name} with ${server.constructor.name}`); await server.execute( `global.server_name = "${server.constructor.name}";` ); await server.registerPathHandler("/test", (req, resp) => { global.reqCount = (global.reqCount || 0) + 1; resp.writeHead(200); resp.end(global.server_name); }); let chan = makeChan(`${server.origin()}/test`); chan.notificationCallbacks = { QueryInterface: ChromeUtils.generateQI([ "nsIInterfaceRequestor", "nsIProgressEventSink", ]), getInterface(iid) { return this.QueryInterface(iid); }, onProgress() {}, onStatus(request, status) { info(`status = ${status}`); // XXX(valentin): Is this the best status to be cancelling? if (status == NS_NET_STATUS_WAITING_FOR) { info("cancelling connected channel"); chan.cancel(Cr.NS_ERROR_ABORT); } }, }; let openPromise = new Promise(resolve => { chan.asyncOpen( new ChannelListener( (req, buff) => resolve({ req, buff }), null, CL_EXPECT_FAILURE ) ); }); let { req } = await openPromise; Assert.equal(req.status, Cr.NS_ERROR_ABORT); // Since we're cancelling just after connect, we'd expect that no // requests are actually registered. But because we're cancelling on the // main thread, and the request is being performed on the socket thread, // it might actually reach the server, especially in chaos test mode. // Assert.equal( // await server.execute(`global.reqCount || 0`), // 0, // `No requests should have been made at this point` // ); Assert.equal(await proxy.execute(`global.session_counter`), 1); chan = makeChan(`${server.origin()}/test`); await new Promise(resolve => { chan.asyncOpen( new ChannelListener( // eslint-disable-next-line no-shadow (req, buff) => resolve({ req, buff }), null, CL_ALLOW_UNKNOWN_CL ) ); }); // Check that there's still only one session. Assert.equal(await proxy.execute(`global.session_counter`), 1); await proxy.stop(); } ); }); add_task(async function test_cancel_after_sending_request() { let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); await with_node_servers( [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], async server => { let proxies = [ NodeHTTPProxyServer, NodeHTTPSProxyServer, NodeHTTP2ProxyServer, ]; for (let p of proxies) { let proxy = new p(); await proxy.start(); registerCleanupFunction(async () => { await proxy.stop(); }); await proxy.execute(` global.session_counter = 0; global.proxy.on("session", () => { global.session_counter++; }); `); info(`Testing ${p.name} with ${server.constructor.name}`); await server.execute( `global.server_name = "${server.constructor.name}";` ); await server.registerPathHandler("/test", (req, resp) => { // Here we simmulate a slow response to give the test time to // cancel the channel before receiving the response. global.request_count = (global.request_count || 0) + 1; // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(() => { resp.writeHead(200); resp.end(global.server_name); }, 2000); }); await server.registerPathHandler("/instant", (req, resp) => { resp.writeHead(200); resp.end(global.server_name); }); // It seems proxy.on("session") only gets emitted after a full request. // So we first load a simple request, then the long lasting request // that we then cancel before it has the chance to complete. let chan = makeChan(`${server.origin()}/instant`); await new Promise(resolve => { chan.asyncOpen( new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL) ); }); chan = makeChan(`${server.origin()}/test`); let openPromise = new Promise(resolve => { chan.asyncOpen( new ChannelListener( (req, buff) => resolve({ req, buff }), null, CL_EXPECT_FAILURE ) ); }); // XXX(valentin) This might be a little racy await TestUtils.waitForCondition(async () => { return (await server.execute("global.request_count")) > 0; }); chan.cancel(Cr.NS_ERROR_ABORT); let { req } = await openPromise; Assert.equal(req.status, Cr.NS_ERROR_ABORT); async function checkSessionCounter() { if (p.name == "NodeHTTP2ProxyServer") { Assert.equal(await proxy.execute(`global.session_counter`), 1); } } await checkSessionCounter(); chan = makeChan(`${server.origin()}/instant`); await new Promise(resolve => { chan.asyncOpen( new ChannelListener( // eslint-disable-next-line no-shadow (req, buff) => resolve({ req, buff }), null, CL_ALLOW_UNKNOWN_CL ) ); }); await checkSessionCounter(); await proxy.stop(); } } ); }); add_task(async function test_cancel_during_response() { let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( Ci.nsIX509CertDB ); addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); await with_node_servers( [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], async server => { let proxies = [ NodeHTTPProxyServer, NodeHTTPSProxyServer, NodeHTTP2ProxyServer, ]; for (let p of proxies) { let proxy = new p(); await proxy.start(); registerCleanupFunction(async () => { await proxy.stop(); }); await proxy.execute(` global.session_counter = 0; global.proxy.on("session", () => { global.session_counter++; }); `); info(`Testing ${p.name} with ${server.constructor.name}`); await server.execute( `global.server_name = "${server.constructor.name}";` ); await server.registerPathHandler("/test", (req, resp) => { resp.writeHead(200); resp.write("a".repeat(1000)); // Here we send the response back in two chunks. // The channel should be cancelled after the first one. // eslint-disable-next-line mozilla/no-arbitrary-setTimeout setTimeout(() => { resp.write("a".repeat(1000)); resp.end(global.server_name); }, 2000); }); await server.registerPathHandler("/instant", (req, resp) => { resp.writeHead(200); resp.end(global.server_name); }); let chan = makeChan(`${server.origin()}/test`); chan.notificationCallbacks = { QueryInterface: ChromeUtils.generateQI([ "nsIInterfaceRequestor", "nsIProgressEventSink", ]), getInterface(iid) { return this.QueryInterface(iid); }, onProgress(request, progress, progressMax) { info(`progress: ${progress}/${progressMax}`); // Check that we never get more than 1000 bytes. Assert.equal(progress, 1000); }, onStatus(request, status) { if (status == NS_NET_STATUS_RECEIVING_FROM) { info("cancelling when receiving request"); chan.cancel(Cr.NS_ERROR_ABORT); } }, }; let openPromise = new Promise(resolve => { chan.asyncOpen( new ChannelListener( (req, buff) => resolve({ req, buff }), null, CL_EXPECT_FAILURE ) ); }); let { req } = await openPromise; Assert.equal(req.status, Cr.NS_ERROR_ABORT); async function checkSessionCounter() { if (p.name == "NodeHTTP2ProxyServer") { Assert.equal(await proxy.execute(`global.session_counter`), 1); } } await checkSessionCounter(); chan = makeChan(`${server.origin()}/instant`); await new Promise(resolve => { chan.asyncOpen( new ChannelListener( // eslint-disable-next-line no-shadow (req, buff) => resolve({ req, buff }), null, CL_ALLOW_UNKNOWN_CL ) ); }); await checkSessionCounter(); await proxy.stop(); } } ); });