test_proxy_cancel.js (13230B)
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 /* globals setTimeout */ 8 9 // We don't normally allow localhost channels to be proxied, but this 10 // is easier than updating all the certs and/or domains. 11 Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); 12 registerCleanupFunction(() => { 13 Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); 14 }); 15 16 const { TestUtils } = ChromeUtils.importESModule( 17 "resource://testing-common/TestUtils.sys.mjs" 18 ); 19 20 const { 21 NodeHTTPServer, 22 NodeHTTPSServer, 23 NodeHTTP2Server, 24 NodeHTTPProxyServer, 25 NodeHTTPSProxyServer, 26 NodeHTTP2ProxyServer, 27 with_node_servers, 28 } = ChromeUtils.importESModule("resource://testing-common/NodeServer.sys.mjs"); 29 30 function makeChan(uri) { 31 let chan = NetUtil.newChannel({ 32 uri, 33 loadUsingSystemPrincipal: true, 34 }).QueryInterface(Ci.nsIHttpChannel); 35 chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; 36 return chan; 37 } 38 39 add_setup(async function setup() { 40 // See Bug 1878505 41 Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); 42 registerCleanupFunction(async () => { 43 Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); 44 }); 45 }); 46 47 add_task(async function test_cancel_after_asyncOpen() { 48 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 49 Ci.nsIX509CertDB 50 ); 51 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 52 addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); 53 54 let proxies = [ 55 NodeHTTPProxyServer, 56 NodeHTTPSProxyServer, 57 NodeHTTP2ProxyServer, 58 ]; 59 for (let p of proxies) { 60 let proxy = new p(); 61 await proxy.start(); 62 registerCleanupFunction(async () => { 63 await proxy.stop(); 64 }); 65 await with_node_servers( 66 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 67 async server => { 68 info(`Testing ${p.name} with ${server.constructor.name}`); 69 await server.execute( 70 `global.server_name = "${server.constructor.name}";` 71 ); 72 73 await server.registerPathHandler("/test", (req, resp) => { 74 resp.writeHead(200); 75 resp.end(global.server_name); 76 }); 77 78 let chan = makeChan(`${server.origin()}/test`); 79 let openPromise = new Promise(resolve => { 80 chan.asyncOpen( 81 new ChannelListener( 82 (req, buff) => resolve({ req, buff }), 83 null, 84 CL_EXPECT_FAILURE 85 ) 86 ); 87 }); 88 chan.cancel(Cr.NS_ERROR_ABORT); 89 let { req } = await openPromise; 90 Assert.equal(req.status, Cr.NS_ERROR_ABORT); 91 } 92 ); 93 await proxy.stop(); 94 } 95 }); 96 97 // const NS_NET_STATUS_CONNECTING_TO = 0x4b0007; 98 // const NS_NET_STATUS_CONNECTED_TO = 0x4b0004; 99 // const NS_NET_STATUS_SENDING_TO = 0x4b0005; 100 const NS_NET_STATUS_WAITING_FOR = 0x4b000a; // 2152398858 101 const NS_NET_STATUS_RECEIVING_FROM = 0x4b0006; 102 // const NS_NET_STATUS_TLS_HANDSHAKE_STARTING = 0x4b000c; // 2152398860 103 // const NS_NET_STATUS_TLS_HANDSHAKE_ENDED = 0x4b000d; // 2152398861 104 105 add_task(async function test_cancel_after_connect_http2proxy() { 106 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 107 Ci.nsIX509CertDB 108 ); 109 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 110 addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); 111 112 await with_node_servers( 113 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 114 async server => { 115 // Set up a proxy for each server to make sure proxy state is clean 116 // for each test. 117 let proxy = new NodeHTTP2ProxyServer(); 118 await proxy.start(); 119 registerCleanupFunction(async () => { 120 await proxy.stop(); 121 }); 122 await proxy.execute(` 123 global.session_counter = 0; 124 global.proxy.on("session", () => { 125 global.session_counter++; 126 }); 127 `); 128 129 info(`Testing ${proxy.constructor.name} with ${server.constructor.name}`); 130 await server.execute( 131 `global.server_name = "${server.constructor.name}";` 132 ); 133 134 await server.registerPathHandler("/test", (req, resp) => { 135 global.reqCount = (global.reqCount || 0) + 1; 136 resp.writeHead(200); 137 resp.end(global.server_name); 138 }); 139 140 let chan = makeChan(`${server.origin()}/test`); 141 chan.notificationCallbacks = { 142 QueryInterface: ChromeUtils.generateQI([ 143 "nsIInterfaceRequestor", 144 "nsIProgressEventSink", 145 ]), 146 147 getInterface(iid) { 148 return this.QueryInterface(iid); 149 }, 150 151 onProgress() {}, 152 onStatus(request, status) { 153 info(`status = ${status}`); 154 // XXX(valentin): Is this the best status to be cancelling? 155 if (status == NS_NET_STATUS_WAITING_FOR) { 156 info("cancelling connected channel"); 157 chan.cancel(Cr.NS_ERROR_ABORT); 158 } 159 }, 160 }; 161 let openPromise = new Promise(resolve => { 162 chan.asyncOpen( 163 new ChannelListener( 164 (req, buff) => resolve({ req, buff }), 165 null, 166 CL_EXPECT_FAILURE 167 ) 168 ); 169 }); 170 let { req } = await openPromise; 171 Assert.equal(req.status, Cr.NS_ERROR_ABORT); 172 173 // Since we're cancelling just after connect, we'd expect that no 174 // requests are actually registered. But because we're cancelling on the 175 // main thread, and the request is being performed on the socket thread, 176 // it might actually reach the server, especially in chaos test mode. 177 // Assert.equal( 178 // await server.execute(`global.reqCount || 0`), 179 // 0, 180 // `No requests should have been made at this point` 181 // ); 182 Assert.equal(await proxy.execute(`global.session_counter`), 1); 183 184 chan = makeChan(`${server.origin()}/test`); 185 await new Promise(resolve => { 186 chan.asyncOpen( 187 new ChannelListener( 188 // eslint-disable-next-line no-shadow 189 (req, buff) => resolve({ req, buff }), 190 null, 191 CL_ALLOW_UNKNOWN_CL 192 ) 193 ); 194 }); 195 196 // Check that there's still only one session. 197 Assert.equal(await proxy.execute(`global.session_counter`), 1); 198 await proxy.stop(); 199 } 200 ); 201 }); 202 203 add_task(async function test_cancel_after_sending_request() { 204 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 205 Ci.nsIX509CertDB 206 ); 207 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 208 addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); 209 210 await with_node_servers( 211 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 212 async server => { 213 let proxies = [ 214 NodeHTTPProxyServer, 215 NodeHTTPSProxyServer, 216 NodeHTTP2ProxyServer, 217 ]; 218 for (let p of proxies) { 219 let proxy = new p(); 220 await proxy.start(); 221 registerCleanupFunction(async () => { 222 await proxy.stop(); 223 }); 224 225 await proxy.execute(` 226 global.session_counter = 0; 227 global.proxy.on("session", () => { 228 global.session_counter++; 229 }); 230 `); 231 info(`Testing ${p.name} with ${server.constructor.name}`); 232 await server.execute( 233 `global.server_name = "${server.constructor.name}";` 234 ); 235 236 await server.registerPathHandler("/test", (req, resp) => { 237 // Here we simmulate a slow response to give the test time to 238 // cancel the channel before receiving the response. 239 global.request_count = (global.request_count || 0) + 1; 240 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 241 setTimeout(() => { 242 resp.writeHead(200); 243 resp.end(global.server_name); 244 }, 2000); 245 }); 246 await server.registerPathHandler("/instant", (req, resp) => { 247 resp.writeHead(200); 248 resp.end(global.server_name); 249 }); 250 251 // It seems proxy.on("session") only gets emitted after a full request. 252 // So we first load a simple request, then the long lasting request 253 // that we then cancel before it has the chance to complete. 254 let chan = makeChan(`${server.origin()}/instant`); 255 await new Promise(resolve => { 256 chan.asyncOpen( 257 new ChannelListener(resolve, null, CL_ALLOW_UNKNOWN_CL) 258 ); 259 }); 260 261 chan = makeChan(`${server.origin()}/test`); 262 let openPromise = new Promise(resolve => { 263 chan.asyncOpen( 264 new ChannelListener( 265 (req, buff) => resolve({ req, buff }), 266 null, 267 CL_EXPECT_FAILURE 268 ) 269 ); 270 }); 271 // XXX(valentin) This might be a little racy 272 await TestUtils.waitForCondition(async () => { 273 return (await server.execute("global.request_count")) > 0; 274 }); 275 276 chan.cancel(Cr.NS_ERROR_ABORT); 277 278 let { req } = await openPromise; 279 Assert.equal(req.status, Cr.NS_ERROR_ABORT); 280 281 async function checkSessionCounter() { 282 if (p.name == "NodeHTTP2ProxyServer") { 283 Assert.equal(await proxy.execute(`global.session_counter`), 1); 284 } 285 } 286 287 await checkSessionCounter(); 288 289 chan = makeChan(`${server.origin()}/instant`); 290 await new Promise(resolve => { 291 chan.asyncOpen( 292 new ChannelListener( 293 // eslint-disable-next-line no-shadow 294 (req, buff) => resolve({ req, buff }), 295 null, 296 CL_ALLOW_UNKNOWN_CL 297 ) 298 ); 299 }); 300 await checkSessionCounter(); 301 302 await proxy.stop(); 303 } 304 } 305 ); 306 }); 307 308 add_task(async function test_cancel_during_response() { 309 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 310 Ci.nsIX509CertDB 311 ); 312 addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); 313 addCertFromFile(certdb, "proxy-ca.pem", "CTu,u,u"); 314 315 await with_node_servers( 316 [NodeHTTPServer, NodeHTTPSServer, NodeHTTP2Server], 317 async server => { 318 let proxies = [ 319 NodeHTTPProxyServer, 320 NodeHTTPSProxyServer, 321 NodeHTTP2ProxyServer, 322 ]; 323 for (let p of proxies) { 324 let proxy = new p(); 325 await proxy.start(); 326 registerCleanupFunction(async () => { 327 await proxy.stop(); 328 }); 329 330 await proxy.execute(` 331 global.session_counter = 0; 332 global.proxy.on("session", () => { 333 global.session_counter++; 334 }); 335 `); 336 337 info(`Testing ${p.name} with ${server.constructor.name}`); 338 await server.execute( 339 `global.server_name = "${server.constructor.name}";` 340 ); 341 342 await server.registerPathHandler("/test", (req, resp) => { 343 resp.writeHead(200); 344 resp.write("a".repeat(1000)); 345 // Here we send the response back in two chunks. 346 // The channel should be cancelled after the first one. 347 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 348 setTimeout(() => { 349 resp.write("a".repeat(1000)); 350 resp.end(global.server_name); 351 }, 2000); 352 }); 353 await server.registerPathHandler("/instant", (req, resp) => { 354 resp.writeHead(200); 355 resp.end(global.server_name); 356 }); 357 358 let chan = makeChan(`${server.origin()}/test`); 359 360 chan.notificationCallbacks = { 361 QueryInterface: ChromeUtils.generateQI([ 362 "nsIInterfaceRequestor", 363 "nsIProgressEventSink", 364 ]), 365 366 getInterface(iid) { 367 return this.QueryInterface(iid); 368 }, 369 370 onProgress(request, progress, progressMax) { 371 info(`progress: ${progress}/${progressMax}`); 372 // Check that we never get more than 1000 bytes. 373 Assert.equal(progress, 1000); 374 }, 375 onStatus(request, status) { 376 if (status == NS_NET_STATUS_RECEIVING_FROM) { 377 info("cancelling when receiving request"); 378 chan.cancel(Cr.NS_ERROR_ABORT); 379 } 380 }, 381 }; 382 383 let openPromise = new Promise(resolve => { 384 chan.asyncOpen( 385 new ChannelListener( 386 (req, buff) => resolve({ req, buff }), 387 null, 388 CL_EXPECT_FAILURE 389 ) 390 ); 391 }); 392 393 let { req } = await openPromise; 394 Assert.equal(req.status, Cr.NS_ERROR_ABORT); 395 396 async function checkSessionCounter() { 397 if (p.name == "NodeHTTP2ProxyServer") { 398 Assert.equal(await proxy.execute(`global.session_counter`), 1); 399 } 400 } 401 402 await checkSessionCounter(); 403 404 chan = makeChan(`${server.origin()}/instant`); 405 await new Promise(resolve => { 406 chan.asyncOpen( 407 new ChannelListener( 408 // eslint-disable-next-line no-shadow 409 (req, buff) => resolve({ req, buff }), 410 null, 411 CL_ALLOW_UNKNOWN_CL 412 ) 413 ); 414 }); 415 416 await checkSessionCounter(); 417 418 await proxy.stop(); 419 } 420 } 421 ); 422 });