test_speculative_connect.js (13346B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ 2 /* vim: set ts=4 sts=4 et sw=4 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 "use strict"; 8 9 var CC = Components.Constructor; 10 const ServerSocket = CC( 11 "@mozilla.org/network/server-socket;1", 12 "nsIServerSocket", 13 "init" 14 ); 15 var serv; 16 var ios; 17 18 /** 19 * Example local IP addresses (literal IP address hostname). 20 * 21 * Note: for IPv6 Unique Local and Link Local, a wider range of addresses is 22 * set aside than those most commonly used. Technically, link local addresses 23 * include those beginning with fe80:: through febf::, although in practise 24 * only fe80:: is used. Necko code blocks speculative connections for the wider 25 * range; hence, this test considers that range too. 26 */ 27 var localIPv4Literals = [ 28 // IPv4 RFC1918 \ 29 "10.0.0.1", 30 "10.10.10.10", 31 "10.255.255.255", // 10/8 32 "172.16.0.1", 33 "172.23.172.12", 34 "172.31.255.255", // 172.16/20 35 "192.168.0.1", 36 "192.168.192.168", 37 "192.168.255.255", // 192.168/16 38 // IPv4 Link Local 39 "169.254.0.1", 40 "169.254.192.154", 41 "169.254.255.255", // 169.254/16 42 ]; 43 var localIPv6Literals = [ 44 // IPv6 Unique Local fc00::/7 45 "fc00::1", 46 "fdfe:dcba:9876:abcd:ef01:2345:6789:abcd", 47 "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 48 // IPv6 Link Local fe80::/10 49 "fe80::1", 50 "fe80::abcd:ef01:2345:6789", 51 "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 52 ]; 53 var localIPLiterals = localIPv4Literals.concat(localIPv6Literals); 54 55 /** 56 * Test function list and descriptions. 57 */ 58 var testList = [ 59 test_localhost_http_speculative_connect, 60 test_localhost_https_speculative_connect, 61 test_hostnames_resolving_to_local_addresses, 62 test_proxies_with_local_addresses, 63 test_speculative_connect_with_proxy_filter, 64 ]; 65 66 var testDescription = [ 67 "Expect pass with localhost, http", 68 "Expect pass with localhost, https", 69 "Expect failure with resolved local IPs", 70 "Expect failure for proxies with local IPs", 71 "Expect failure without notification callbacks", 72 ]; 73 74 var testIdx = 0; 75 var hostIdx = 0; 76 77 /** 78 * TestServer 79 * 80 * Implements nsIServerSocket for test_speculative_connect. 81 */ 82 function TestServer() { 83 this.listener = ServerSocket(-1, true, -1); 84 this.listener.asyncListen(this); 85 } 86 87 TestServer.prototype = { 88 QueryInterface: ChromeUtils.generateQI(["nsIServerSocket"]), 89 onSocketAccepted() { 90 try { 91 this.listener.close(); 92 } catch (e) {} 93 Assert.ok(true); 94 next_test(); 95 }, 96 97 onStopListening() {}, 98 }; 99 100 /** 101 * TestFailedStreamCallback 102 * 103 * Implements nsI[Input|Output]StreamCallback for socket layer tests. 104 * Expect failure in all cases 105 */ 106 function TestFailedStreamCallback(transport, hostname, next) { 107 this.transport = transport; 108 this.hostname = hostname; 109 this.next = next; 110 this.dummyContent = "G"; 111 this.closed = false; 112 } 113 114 TestFailedStreamCallback.prototype = { 115 QueryInterface: ChromeUtils.generateQI([ 116 "nsIInputStreamCallback", 117 "nsIOutputStreamCallback", 118 ]), 119 processException(e) { 120 if (this.closed) { 121 return; 122 } 123 do_check_instanceof(e, Ci.nsIException); 124 // A refusal to connect speculatively should throw an error. 125 Assert.equal(e.result, Cr.NS_ERROR_CONNECTION_REFUSED); 126 this.closed = true; 127 this.transport.close(Cr.NS_BINDING_ABORTED); 128 this.next(); 129 }, 130 onOutputStreamReady(outstream) { 131 info("outputstream handler."); 132 Assert.notEqual(typeof outstream, undefined); 133 try { 134 outstream.write(this.dummyContent, this.dummyContent.length); 135 } catch (e) { 136 this.processException(e); 137 return; 138 } 139 info("no exception on write. Wait for read."); 140 }, 141 onInputStreamReady(instream) { 142 info("inputstream handler."); 143 Assert.notEqual(typeof instream, undefined); 144 try { 145 instream.available(); 146 } catch (e) { 147 this.processException(e); 148 return; 149 } 150 do_throw("Speculative Connect should have failed for " + this.hostname); 151 this.transport.close(Cr.NS_BINDING_ABORTED); 152 this.next(); 153 }, 154 }; 155 156 /** 157 * test_localhost_http_speculative_connect 158 * 159 * Tests a basic positive case using nsIOService.SpeculativeConnect: 160 * connecting to localhost via http. 161 */ 162 function test_localhost_http_speculative_connect() { 163 serv = new TestServer(); 164 var ssm = Services.scriptSecurityManager; 165 var URI = ios.newURI( 166 "http://localhost:" + serv.listener.port + "/just/a/test" 167 ); 168 var principal = ssm.createContentPrincipal(URI, {}); 169 170 ios 171 .QueryInterface(Ci.nsISpeculativeConnect) 172 .speculativeConnect(URI, principal, null, false); 173 } 174 175 /** 176 * test_localhost_https_speculative_connect 177 * 178 * Tests a basic positive case using nsIOService.SpeculativeConnect: 179 * connecting to localhost via https. 180 */ 181 function test_localhost_https_speculative_connect() { 182 serv = new TestServer(); 183 var ssm = Services.scriptSecurityManager; 184 var URI = ios.newURI( 185 "https://localhost:" + serv.listener.port + "/just/a/test" 186 ); 187 var principal = ssm.createContentPrincipal(URI, {}); 188 189 ios 190 .QueryInterface(Ci.nsISpeculativeConnect) 191 .speculativeConnect(URI, principal, null, false); 192 } 193 194 /* Speculative connections should not be allowed for hosts with local IP 195 * addresses (Bug 853423). That list includes: 196 * -- IPv4 RFC1918 and Link Local Addresses. 197 * -- IPv6 Unique and Link Local Addresses. 198 * 199 * Two tests are required: 200 * 1. Verify IP Literals passed to the SpeculativeConnect API. 201 * 2. Verify hostnames that need to be resolved at the socket layer. 202 */ 203 204 /** 205 * test_hostnames_resolving_to_addresses 206 * 207 * Common test function for resolved hostnames. Takes a list of hosts, a 208 * boolean to determine if the test is expected to succeed or fail, and a 209 * function to call the next test case. 210 */ 211 function test_hostnames_resolving_to_addresses(host, next) { 212 info(host); 213 var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( 214 Ci.nsISocketTransportService 215 ); 216 Assert.notEqual(typeof sts, undefined); 217 var transport = sts.createTransport([], host, 80, null, null); 218 Assert.notEqual(typeof transport, undefined); 219 220 transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; 221 transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); 222 transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1); 223 Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT)); 224 225 var outStream = transport.openOutputStream( 226 Ci.nsITransport.OPEN_UNBUFFERED, 227 0, 228 0 229 ); 230 var inStream = transport.openInputStream(0, 0, 0); 231 Assert.notEqual(typeof outStream, undefined); 232 Assert.notEqual(typeof inStream, undefined); 233 234 var callback = new TestFailedStreamCallback(transport, host, next); 235 Assert.notEqual(typeof callback, undefined); 236 237 // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait 238 // adds callback to ns*StreamReadyEvent on main thread, and doesn't 239 // addref off the main thread. 240 var gThreadManager = Services.tm; 241 var mainThread = gThreadManager.currentThread; 242 243 try { 244 outStream 245 .QueryInterface(Ci.nsIAsyncOutputStream) 246 .asyncWait(callback, 0, 0, mainThread); 247 inStream 248 .QueryInterface(Ci.nsIAsyncInputStream) 249 .asyncWait(callback, 0, 0, mainThread); 250 } catch (e) { 251 do_throw("asyncWait should not fail!"); 252 } 253 } 254 255 /** 256 * test_hostnames_resolving_to_local_addresses 257 * 258 * Creates an nsISocketTransport and simulates a speculative connect request 259 * for a hostname that resolves to a local IP address. 260 * Runs asynchronously; on test success (i.e. failure to connect), the callback 261 * will call this function again until all hostnames in the test list are done. 262 * 263 * Note: This test also uses an IP literal for the hostname. This should be ok, 264 * as the socket layer will ask for the hostname to be resolved anyway, and DNS 265 * code should return a numerical version of the address internally. 266 */ 267 function test_hostnames_resolving_to_local_addresses() { 268 if (hostIdx >= localIPLiterals.length) { 269 // No more local IP addresses; move on. 270 next_test(); 271 return; 272 } 273 var host = localIPLiterals[hostIdx++]; 274 // Test another local IP address when the current one is done. 275 var next = test_hostnames_resolving_to_local_addresses; 276 test_hostnames_resolving_to_addresses(host, next); 277 } 278 279 /** 280 * test_speculative_connect_with_host_list 281 * 282 * Common test function for resolved proxy hosts. Takes a list of hosts, a 283 * boolean to determine if the test is expected to succeed or fail, and a 284 * function to call the next test case. 285 */ 286 function test_proxies(proxyHost, next) { 287 info("Proxy: " + proxyHost); 288 var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( 289 Ci.nsISocketTransportService 290 ); 291 Assert.notEqual(typeof sts, undefined); 292 var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 293 Assert.notEqual(typeof pps, undefined); 294 295 var proxyInfo = pps.newProxyInfo("http", proxyHost, 8080, "", "", 0, 1, null); 296 Assert.notEqual(typeof proxyInfo, undefined); 297 298 var transport = sts.createTransport([], "dummyHost", 80, proxyInfo, null); 299 Assert.notEqual(typeof transport, undefined); 300 301 transport.connectionFlags = Ci.nsISocketTransport.DISABLE_RFC1918; 302 303 transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 1); 304 Assert.equal(1, transport.getTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT)); 305 transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, 1); 306 307 var outStream = transport.openOutputStream( 308 Ci.nsITransport.OPEN_UNBUFFERED, 309 0, 310 0 311 ); 312 var inStream = transport.openInputStream(0, 0, 0); 313 Assert.notEqual(typeof outStream, undefined); 314 Assert.notEqual(typeof inStream, undefined); 315 316 var callback = new TestFailedStreamCallback(transport, proxyHost, next); 317 Assert.notEqual(typeof callback, undefined); 318 319 // Need to get main thread pointer to ensure nsSocketTransport::AsyncWait 320 // adds callback to ns*StreamReadyEvent on main thread, and doesn't 321 // addref off the main thread. 322 var gThreadManager = Services.tm; 323 var mainThread = gThreadManager.currentThread; 324 325 try { 326 outStream 327 .QueryInterface(Ci.nsIAsyncOutputStream) 328 .asyncWait(callback, 0, 0, mainThread); 329 inStream 330 .QueryInterface(Ci.nsIAsyncInputStream) 331 .asyncWait(callback, 0, 0, mainThread); 332 } catch (e) { 333 do_throw("asyncWait should not fail!"); 334 } 335 } 336 337 /** 338 * test_proxies_with_local_addresses 339 * 340 * Creates an nsISocketTransport and simulates a speculative connect request 341 * for a proxy that resolves to a local IP address. 342 * Runs asynchronously; on test success (i.e. failure to connect), the callback 343 * will call this function again until all proxies in the test list are done. 344 * 345 * Note: This test also uses an IP literal for the proxy. This should be ok, 346 * as the socket layer will ask for the proxy to be resolved anyway, and DNS 347 * code should return a numerical version of the address internally. 348 */ 349 function test_proxies_with_local_addresses() { 350 if (hostIdx >= localIPLiterals.length) { 351 // No more local IP addresses; move on. 352 next_test(); 353 return; 354 } 355 var host = localIPLiterals[hostIdx++]; 356 // Test another local IP address when the current one is done. 357 var next = test_proxies_with_local_addresses; 358 test_proxies(host, next); 359 } 360 361 class ProxyFilter { 362 constructor(type, host, port, flags) { 363 this._type = type; 364 this._host = host; 365 this._port = port; 366 this._flags = flags; 367 this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); 368 } 369 applyFilter(uri, pi, cb) { 370 const pps = 371 Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 372 cb.onProxyFilterResult( 373 pps.newProxyInfo( 374 this._type, 375 this._host, 376 this._port, 377 "", 378 "", 379 this._flags, 380 1000, 381 null 382 ) 383 ); 384 } 385 } 386 387 function test_speculative_connect_with_proxy_filter() { 388 let filter = new ProxyFilter("https", "localhost", 80, 0); 389 let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); 390 pps.registerFilter(filter, 10); 391 let URI = ios.newURI("https://not-exist-dommain.com"); 392 let principal = Services.scriptSecurityManager.createContentPrincipal( 393 URI, 394 {} 395 ); 396 397 Assert.throws( 398 () => 399 ios 400 .QueryInterface(Ci.nsISpeculativeConnect) 401 .speculativeConnect(URI, principal, null, false), 402 /NS_ERROR_FAILURE/, 403 "speculativeConnect should throw when no callback is provided and a proxy filter is registered" 404 ); 405 pps.unregisterFilter(filter); 406 next_test(); 407 } 408 409 /** 410 * next_test 411 * 412 * Calls the next test in testList. Each test is responsible for calling this 413 * function when its test cases are complete. 414 */ 415 function next_test() { 416 if (testIdx >= testList.length) { 417 // No more tests; we're done. 418 do_test_finished(); 419 return; 420 } 421 info("SpeculativeConnect: " + testDescription[testIdx]); 422 hostIdx = 0; 423 // Start next test in list. 424 testList[testIdx++](); 425 } 426 427 /** 428 * run_test 429 * 430 * Main entry function for test execution. 431 */ 432 function run_test() { 433 ios = Services.io; 434 435 Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); 436 registerCleanupFunction(() => { 437 Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); 438 }); 439 440 do_test_pending(); 441 next_test(); 442 }