head_channels.js (17537B)
1 /** 2 * Read count bytes from stream and return as a String object 3 */ 4 5 /* import-globals-from head_cache.js */ 6 /* import-globals-from head_cookies.js */ 7 8 function read_stream(stream, count) { 9 /* assume stream has non-ASCII data */ 10 var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 11 Ci.nsIBinaryInputStream 12 ); 13 wrapper.setInputStream(stream); 14 /* JS methods can be called with a maximum of 65535 arguments, and input 15 streams don't have to return all the data they make .available() when 16 asked to .read() that number of bytes. */ 17 var data = []; 18 while (count > 0) { 19 var bytes = wrapper.readByteArray(Math.min(65535, count)); 20 data.push(String.fromCharCode.apply(null, bytes)); 21 count -= bytes.length; 22 if (!bytes.length) { 23 do_throw("Nothing read from input stream!"); 24 } 25 } 26 return data.join(""); 27 } 28 29 // CL_ stands for ChannelListener 30 const CL_EXPECT_FAILURE = 0x1; 31 const CL_EXPECT_GZIP = 0x2; 32 const CL_EXPECT_3S_DELAY = 0x4; 33 const CL_SUSPEND = 0x8; 34 const CL_ALLOW_UNKNOWN_CL = 0x10; // Response can contain no or invalid content-length header 35 const CL_EXPECT_LATE_FAILURE = 0x20; 36 const CL_FROM_CACHE = 0x40; // Response must be from the cache 37 const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache 38 const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length 39 const CL_IGNORE_DELAYS = 0x200; // don't throw if channel returns after a long delay 40 41 const SUSPEND_DELAY = 3000; 42 43 /** 44 * A stream listener that calls a callback function with a specified 45 * context and the received data when the channel is loaded. 46 * 47 * Signature of the closure: 48 * void closure(in nsIRequest request, in ACString data, in JSObject context); 49 * 50 * This listener makes sure that various parts of the channel API are 51 * implemented correctly and that the channel's status is a success code 52 * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags 53 * to allow a failure code) 54 * 55 * Note that it also requires a valid content length on the channel and 56 * is thus not fully generic. 57 */ 58 function ChannelListener(closure, ctx, flags) { 59 this._closure = closure; 60 this._closurectx = ctx; 61 this._flags = flags; 62 this._isFromCache = false; 63 this._hasCacheEntry = false; 64 this._cacheEntryId = undefined; 65 } 66 ChannelListener.prototype = { 67 _closure: null, 68 _closurectx: null, 69 _buffer: "", 70 _got_onstartrequest: false, 71 _got_onstoprequest: false, 72 _contentLen: -1, 73 _lastEvent: 0, 74 75 QueryInterface: ChromeUtils.generateQI([ 76 "nsIStreamListener", 77 "nsIRequestObserver", 78 ]), 79 80 onStartRequest(request) { 81 try { 82 if (this._got_onstartrequest) { 83 do_throw("Got second onStartRequest event!"); 84 } 85 this._got_onstartrequest = true; 86 this._lastEvent = Date.now(); 87 88 try { 89 this._isFromCache = request 90 .QueryInterface(Ci.nsICacheInfoChannel) 91 .isFromCache(); 92 } catch (e) {} 93 94 try { 95 this._hasCacheEntry = request 96 .QueryInterface(Ci.nsICacheInfoChannel) 97 .hasCacheEntry(); 98 } catch (e) {} 99 100 var thrown = false; 101 try { 102 this._cacheEntryId = request 103 .QueryInterface(Ci.nsICacheInfoChannel) 104 .getCacheEntryId(); 105 } catch (e) { 106 thrown = true; 107 } 108 if (this._hasCacheEntry && thrown) { 109 do_throw("Should get a CacheEntryId"); 110 } else if (!this._hasCacheEntry && !thrown) { 111 do_throw("Shouldn't get a CacheEntryId"); 112 } 113 114 request.QueryInterface(Ci.nsIChannel); 115 try { 116 this._contentLen = request.contentLength; 117 } catch (ex) { 118 if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) { 119 do_throw("Could not get contentLength"); 120 } 121 } 122 if (!request.isPending()) { 123 do_throw("request reports itself as not pending from onStartRequest!"); 124 } 125 if ( 126 this._contentLen == -1 && 127 !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)) 128 ) { 129 do_throw("Content length is unknown in onStartRequest!"); 130 } 131 132 if (this._flags & CL_FROM_CACHE) { 133 request.QueryInterface(Ci.nsICachingChannel); 134 if (!request.isFromCache()) { 135 do_throw("Response is not from the cache (CL_FROM_CACHE)"); 136 } 137 } 138 if (this._flags & CL_NOT_FROM_CACHE) { 139 request.QueryInterface(Ci.nsICachingChannel); 140 if (request.isFromCache()) { 141 do_throw("Response is from the cache (CL_NOT_FROM_CACHE)"); 142 } 143 } 144 145 if (this._flags & CL_SUSPEND) { 146 request.suspend(); 147 do_timeout(SUSPEND_DELAY, function () { 148 request.resume(); 149 }); 150 } 151 } catch (ex) { 152 do_throw("Error in onStartRequest: " + ex); 153 } 154 }, 155 156 onDataAvailable(request, stream, offset, count) { 157 try { 158 let current = Date.now(); 159 160 if (!this._got_onstartrequest) { 161 do_throw("onDataAvailable without onStartRequest event!"); 162 } 163 if (this._got_onstoprequest) { 164 do_throw("onDataAvailable after onStopRequest event!"); 165 } 166 if (!request.isPending()) { 167 do_throw("request reports itself as not pending from onDataAvailable!"); 168 } 169 if (this._flags & CL_EXPECT_FAILURE) { 170 do_throw("Got data despite expecting a failure"); 171 } 172 173 if ( 174 !(this._flags & CL_IGNORE_DELAYS) && 175 current - this._lastEvent >= SUSPEND_DELAY && 176 !(this._flags & CL_EXPECT_3S_DELAY) 177 ) { 178 do_throw("Data received after significant unexpected delay"); 179 } else if ( 180 current - this._lastEvent < SUSPEND_DELAY && 181 this._flags & CL_EXPECT_3S_DELAY 182 ) { 183 do_throw("Data received sooner than expected"); 184 } else if ( 185 current - this._lastEvent >= SUSPEND_DELAY && 186 this._flags & CL_EXPECT_3S_DELAY 187 ) { 188 this._flags &= ~CL_EXPECT_3S_DELAY; 189 } // No more delays expected 190 191 this._buffer = this._buffer.concat(read_stream(stream, count)); 192 this._lastEvent = current; 193 } catch (ex) { 194 do_throw("Error in onDataAvailable: " + ex); 195 } 196 }, 197 198 onStopRequest(request, status) { 199 try { 200 var success = Components.isSuccessCode(status); 201 if (!this._got_onstartrequest) { 202 do_throw("onStopRequest without onStartRequest event!"); 203 } 204 if (this._got_onstoprequest) { 205 do_throw("Got second onStopRequest event!"); 206 } 207 this._got_onstoprequest = true; 208 if ( 209 this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) && 210 success 211 ) { 212 do_throw( 213 "Should have failed to load URL (status is " + 214 status.toString(16) + 215 ")" 216 ); 217 } else if ( 218 !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) && 219 !success 220 ) { 221 do_throw("Failed to load URL: " + status.toString(16)); 222 } 223 if (status != request.status) { 224 do_throw("request.status does not match status arg to onStopRequest!"); 225 } 226 if (request.isPending()) { 227 do_throw("request reports itself as pending from onStopRequest!"); 228 } 229 if ( 230 !( 231 this._flags & 232 (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL) 233 ) && 234 !(this._flags & CL_EXPECT_GZIP) && 235 this._contentLen != -1 236 ) { 237 Assert.equal(this._buffer.length, this._contentLen); 238 } 239 } catch (ex) { 240 do_throw("Error in onStopRequest: " + ex); 241 } 242 try { 243 this._closure( 244 request, 245 this._buffer, 246 this._closurectx, 247 this._isFromCache, 248 this._cacheEntryId 249 ); 250 this._closurectx = null; 251 } catch (ex) { 252 do_throw("Error in closure function: " + ex); 253 } 254 }, 255 }; 256 257 var ES_ABORT_REDIRECT = 0x01; 258 259 function ChannelEventSink(flags) { 260 this._flags = flags; 261 } 262 263 ChannelEventSink.prototype = { 264 QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), 265 266 getInterface(iid) { 267 if (iid.equals(Ci.nsIChannelEventSink)) { 268 return this; 269 } 270 throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); 271 }, 272 273 asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { 274 if (this._flags & ES_ABORT_REDIRECT) { 275 throw Components.Exception("", Cr.NS_BINDING_ABORTED); 276 } 277 278 callback.onRedirectVerifyCallback(Cr.NS_OK); 279 }, 280 }; 281 282 /** 283 * A helper class to construct origin attributes. 284 */ 285 function OriginAttributes(privateId) { 286 this.privateBrowsingId = privateId; 287 } 288 OriginAttributes.prototype = { 289 privateBrowsingId: 0, 290 }; 291 292 function readFile(file) { 293 let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( 294 Ci.nsIFileInputStream 295 ); 296 fstream.init(file, -1, 0, 0); 297 let data = NetUtil.readInputStreamToString(fstream, fstream.available()); 298 fstream.close(); 299 return data; 300 } 301 302 function addCertFromFile(certdb, filename, trustString) { 303 let certFile = do_get_file(filename, false); 304 let pem = readFile(certFile) 305 .replace(/-----BEGIN CERTIFICATE-----/, "") 306 .replace(/-----END CERTIFICATE-----/, "") 307 .replace(/[\r\n]/g, ""); 308 certdb.addCertFromBase64(pem, trustString); 309 } 310 311 // Helper code to test nsISerializable 312 function serialize_to_escaped_string(obj) { 313 let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( 314 Ci.nsIObjectOutputStream 315 ); 316 let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 317 pipe.init(false, false, 0, 0xffffffff, null); 318 objectOutStream.setOutputStream(pipe.outputStream); 319 objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true); 320 objectOutStream.close(); 321 322 let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 323 Ci.nsIObjectInputStream 324 ); 325 objectInStream.setInputStream(pipe.inputStream); 326 let data = []; 327 // This reads all the data from the stream until an error occurs. 328 while (true) { 329 try { 330 let bytes = objectInStream.readByteArray(1); 331 data.push(String.fromCharCode.apply(null, bytes)); 332 } catch (e) { 333 break; 334 } 335 } 336 return escape(data.join("")); 337 } 338 339 function deserialize_from_escaped_string(str) { 340 let payload = unescape(str); 341 let data = []; 342 let i = 0; 343 while (i < payload.length) { 344 data.push(payload.charCodeAt(i++)); 345 } 346 347 let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( 348 Ci.nsIObjectOutputStream 349 ); 350 let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 351 pipe.init(false, false, 0, 0xffffffff, null); 352 objectOutStream.setOutputStream(pipe.outputStream); 353 objectOutStream.writeByteArray(data); 354 objectOutStream.close(); 355 356 let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 357 Ci.nsIObjectInputStream 358 ); 359 objectInStream.setInputStream(pipe.inputStream); 360 return objectInStream.readObject(true); 361 } 362 363 async function asyncStartTLSTestServer( 364 serverBinName, 365 certsPath, 366 addDefaultRoot = true 367 ) { 368 const { HttpServer } = ChromeUtils.importESModule( 369 "resource://testing-common/httpd.sys.mjs" 370 ); 371 let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( 372 Ci.nsIX509CertDB 373 ); 374 // The trusted CA that is typically used for "good" certificates. 375 if (addDefaultRoot) { 376 addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u"); 377 } 378 379 const CALLBACK_PORT = 8444; 380 381 let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile); 382 Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path); 383 // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD" 384 // does not return this path on Android, so hard code it here. 385 Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb"); 386 Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3"); 387 Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT); 388 Services.env.set("MOZ_TLS_ECH_ALPN_FLAG", "1"); 389 390 let httpServer = new HttpServer(); 391 let serverReady = new Promise(resolve => { 392 httpServer.registerPathHandler( 393 "/", 394 function handleServerCallback(aRequest, aResponse) { 395 aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); 396 aResponse.setHeader("Content-Type", "text/plain"); 397 let responseBody = "OK!"; 398 aResponse.bodyOutputStream.write(responseBody, responseBody.length); 399 executeSoon(function () { 400 httpServer.stop(resolve); 401 }); 402 } 403 ); 404 httpServer.start(CALLBACK_PORT); 405 }); 406 407 let serverBin = _getBinaryUtil(serverBinName); 408 let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); 409 process.init(serverBin); 410 let certDir = do_get_file(certsPath, false); 411 Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`); 412 // Using "sql:" causes the SQL DB to be used so we can run tests on Android. 413 process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2); 414 415 registerCleanupFunction(function () { 416 process.kill(); 417 }); 418 419 await serverReady; 420 } 421 422 function _getBinaryUtil(binaryUtilName) { 423 let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile); 424 // On macOS, GreD is .../Contents/Resources, and most binary utilities 425 // are located there, but certutil is in GreBinD (or .../Contents/MacOS), 426 // so we have to change the path accordingly. 427 if (binaryUtilName === "certutil") { 428 utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile); 429 } 430 utilBin.append(binaryUtilName + mozinfo.bin_suffix); 431 // If we're testing locally, the above works. If not, the server executable 432 // is in another location. 433 if (!utilBin.exists()) { 434 utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile); 435 while (utilBin.path.includes("xpcshell")) { 436 utilBin = utilBin.parent; 437 } 438 utilBin.append("bin"); 439 utilBin.append(binaryUtilName + mozinfo.bin_suffix); 440 } 441 // But maybe we're on Android, where binaries are in /data/local/xpcb. 442 if (!utilBin.exists()) { 443 utilBin.initWithPath("/data/local/xpcb/"); 444 utilBin.append(binaryUtilName); 445 } 446 Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`); 447 return utilBin; 448 } 449 450 function promiseAsyncOpen(chan) { 451 return new Promise(resolve => { 452 chan.asyncOpen( 453 new ChannelListener((req, buf, ctx, isCache, cacheId) => { 454 resolve({ req, buf, ctx, isCache, cacheId }); 455 }) 456 ); 457 }); 458 } 459 460 function hexStringToBytes(hex) { 461 let bytes = []; 462 for (let hexByteStr of hex.split(/(..)/)) { 463 if (hexByteStr.length) { 464 bytes.push(parseInt(hexByteStr, 16)); 465 } 466 } 467 return bytes; 468 } 469 470 function stringToBytes(str) { 471 return Array.from(str, chr => chr.charCodeAt(0)); 472 } 473 474 function BinaryHttpResponse(status, headerNames, headerValues, content) { 475 this.status = status; 476 this.headerNames = headerNames; 477 this.headerValues = headerValues; 478 this.content = content; 479 } 480 481 BinaryHttpResponse.prototype = { 482 QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]), 483 }; 484 485 function bytesToString(bytes) { 486 return String.fromCharCode.apply(null, bytes); 487 } 488 489 function check_http_info(request, expected_httpVersion, expected_proxy) { 490 let httpVersion = ""; 491 try { 492 httpVersion = request.QueryInterface(Ci.nsIHttpChannel).protocolVersion; 493 } catch (e) {} 494 495 request.QueryInterface(Ci.nsIProxiedChannel); 496 var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode; 497 498 Assert.equal(expected_httpVersion, httpVersion); 499 if (expected_proxy) { 500 Assert.equal(httpProxyConnectResponseCode, 200); 501 } else { 502 Assert.equal(httpProxyConnectResponseCode, -1); 503 } 504 } 505 506 function makeHTTPChannel(url, with_proxy) { 507 function createPrincipal(uri) { 508 var ssm = Services.scriptSecurityManager; 509 try { 510 return ssm.createContentPrincipal(Services.io.newURI(uri), {}); 511 } catch (e) { 512 return null; 513 } 514 } 515 516 if (with_proxy) { 517 return Services.io 518 .newChannelFromURIWithProxyFlags( 519 Services.io.newURI(url), 520 null, 521 Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL, 522 null, 523 createPrincipal(url), 524 createPrincipal(url), 525 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, 526 Ci.nsIContentPolicy.TYPE_OTHER 527 ) 528 .QueryInterface(Ci.nsIHttpChannel); 529 } 530 return NetUtil.newChannel({ 531 uri: url, 532 loadUsingSystemPrincipal: true, 533 }).QueryInterface(Ci.nsIHttpChannel); 534 } 535 536 // Like ChannelListener but does not throw an exception if something 537 // goes wrong. Callback is supposed to do all the work. 538 class SimpleChannelListener { 539 constructor(callback) { 540 this._onStopCallback = callback; 541 this._buffer = ""; 542 } 543 get QueryInterface() { 544 return ChromeUtils.generateQI(["nsIStreamListener", "nsIRequestObserver"]); 545 } 546 547 onStartRequest() {} 548 549 onDataAvailable(request, stream, offset, count) { 550 this._buffer = this._buffer.concat(read_stream(stream, count)); 551 } 552 553 onStopRequest(request) { 554 if (this._onStopCallback) { 555 this._onStopCallback(request, this._buffer); 556 } 557 } 558 } 559 560 // nsITLSServerSocket needs a certificate with a corresponding private key 561 // available. xpcshell tests can import the test file "client-cert.p12" using 562 // the password "password", resulting in a certificate with the common name 563 // "Test End-entity" being available with a corresponding private key. 564 function getTestServerCertificate() { 565 const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( 566 Ci.nsIX509CertDB 567 ); 568 const certFile = do_get_file("client-cert.p12"); 569 certDB.importPKCS12File(certFile, "password"); 570 for (const cert of certDB.getCerts()) { 571 if (cert.commonName == "Test End-entity") { 572 return cert; 573 } 574 } 575 return null; 576 }