summaryrefslogtreecommitdiffstats
path: root/netwerk/test/unit/head_channels.js
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/test/unit/head_channels.js')
-rw-r--r--netwerk/test/unit/head_channels.js527
1 files changed, 527 insertions, 0 deletions
diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js
new file mode 100644
index 0000000000..791284bdce
--- /dev/null
+++ b/netwerk/test/unit/head_channels.js
@@ -0,0 +1,527 @@
+/**
+ * Read count bytes from stream and return as a String object
+ */
+
+/* import-globals-from head_cache.js */
+/* import-globals-from head_cookies.js */
+
+function read_stream(stream, count) {
+ /* assume stream has non-ASCII data */
+ var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ wrapper.setInputStream(stream);
+ /* JS methods can be called with a maximum of 65535 arguments, and input
+ streams don't have to return all the data they make .available() when
+ asked to .read() that number of bytes. */
+ var data = [];
+ while (count > 0) {
+ var bytes = wrapper.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (!bytes.length) {
+ do_throw("Nothing read from input stream!");
+ }
+ }
+ return data.join("");
+}
+
+const CL_EXPECT_FAILURE = 0x1;
+const CL_EXPECT_GZIP = 0x2;
+const CL_EXPECT_3S_DELAY = 0x4;
+const CL_SUSPEND = 0x8;
+const CL_ALLOW_UNKNOWN_CL = 0x10;
+const CL_EXPECT_LATE_FAILURE = 0x20;
+const CL_FROM_CACHE = 0x40; // Response must be from the cache
+const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache
+const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length
+const CL_IGNORE_DELAYS = 0x200; // don't throw if channel returns after a long delay
+
+const SUSPEND_DELAY = 3000;
+
+/**
+ * A stream listener that calls a callback function with a specified
+ * context and the received data when the channel is loaded.
+ *
+ * Signature of the closure:
+ * void closure(in nsIRequest request, in ACString data, in JSObject context);
+ *
+ * This listener makes sure that various parts of the channel API are
+ * implemented correctly and that the channel's status is a success code
+ * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags
+ * to allow a failure code)
+ *
+ * Note that it also requires a valid content length on the channel and
+ * is thus not fully generic.
+ */
+function ChannelListener(closure, ctx, flags) {
+ this._closure = closure;
+ this._closurectx = ctx;
+ this._flags = flags;
+ this._isFromCache = false;
+ this._cacheEntryId = undefined;
+}
+ChannelListener.prototype = {
+ _closure: null,
+ _closurectx: null,
+ _buffer: "",
+ _got_onstartrequest: false,
+ _got_onstoprequest: false,
+ _contentLen: -1,
+ _lastEvent: 0,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ onStartRequest(request) {
+ try {
+ if (this._got_onstartrequest) {
+ do_throw("Got second onStartRequest event!");
+ }
+ this._got_onstartrequest = true;
+ this._lastEvent = Date.now();
+
+ try {
+ this._isFromCache = request
+ .QueryInterface(Ci.nsICacheInfoChannel)
+ .isFromCache();
+ } catch (e) {}
+
+ var thrown = false;
+ try {
+ this._cacheEntryId = request
+ .QueryInterface(Ci.nsICacheInfoChannel)
+ .getCacheEntryId();
+ } catch (e) {
+ thrown = true;
+ }
+ if (this._isFromCache && thrown) {
+ do_throw("Should get a CacheEntryId");
+ } else if (!this._isFromCache && !thrown) {
+ do_throw("Shouldn't get a CacheEntryId");
+ }
+
+ request.QueryInterface(Ci.nsIChannel);
+ try {
+ this._contentLen = request.contentLength;
+ } catch (ex) {
+ if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) {
+ do_throw("Could not get contentLength");
+ }
+ }
+ if (!request.isPending()) {
+ do_throw("request reports itself as not pending from onStartRequest!");
+ }
+ if (
+ this._contentLen == -1 &&
+ !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))
+ ) {
+ do_throw("Content length is unknown in onStartRequest!");
+ }
+
+ if (this._flags & CL_FROM_CACHE) {
+ request.QueryInterface(Ci.nsICachingChannel);
+ if (!request.isFromCache()) {
+ do_throw("Response is not from the cache (CL_FROM_CACHE)");
+ }
+ }
+ if (this._flags & CL_NOT_FROM_CACHE) {
+ request.QueryInterface(Ci.nsICachingChannel);
+ if (request.isFromCache()) {
+ do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
+ }
+ }
+
+ if (this._flags & CL_SUSPEND) {
+ request.suspend();
+ do_timeout(SUSPEND_DELAY, function () {
+ request.resume();
+ });
+ }
+ } catch (ex) {
+ do_throw("Error in onStartRequest: " + ex);
+ }
+ },
+
+ onDataAvailable(request, stream, offset, count) {
+ try {
+ let current = Date.now();
+
+ if (!this._got_onstartrequest) {
+ do_throw("onDataAvailable without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("onDataAvailable after onStopRequest event!");
+ }
+ if (!request.isPending()) {
+ do_throw("request reports itself as not pending from onDataAvailable!");
+ }
+ if (this._flags & CL_EXPECT_FAILURE) {
+ do_throw("Got data despite expecting a failure");
+ }
+
+ if (
+ !(this._flags & CL_IGNORE_DELAYS) &&
+ current - this._lastEvent >= SUSPEND_DELAY &&
+ !(this._flags & CL_EXPECT_3S_DELAY)
+ ) {
+ do_throw("Data received after significant unexpected delay");
+ } else if (
+ current - this._lastEvent < SUSPEND_DELAY &&
+ this._flags & CL_EXPECT_3S_DELAY
+ ) {
+ do_throw("Data received sooner than expected");
+ } else if (
+ current - this._lastEvent >= SUSPEND_DELAY &&
+ this._flags & CL_EXPECT_3S_DELAY
+ ) {
+ this._flags &= ~CL_EXPECT_3S_DELAY;
+ } // No more delays expected
+
+ this._buffer = this._buffer.concat(read_stream(stream, count));
+ this._lastEvent = current;
+ } catch (ex) {
+ do_throw("Error in onDataAvailable: " + ex);
+ }
+ },
+
+ onStopRequest(request, status) {
+ try {
+ var success = Components.isSuccessCode(status);
+ if (!this._got_onstartrequest) {
+ do_throw("onStopRequest without onStartRequest event!");
+ }
+ if (this._got_onstoprequest) {
+ do_throw("Got second onStopRequest event!");
+ }
+ this._got_onstoprequest = true;
+ if (
+ this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) &&
+ success
+ ) {
+ do_throw(
+ "Should have failed to load URL (status is " +
+ status.toString(16) +
+ ")"
+ );
+ } else if (
+ !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) &&
+ !success
+ ) {
+ do_throw("Failed to load URL: " + status.toString(16));
+ }
+ if (status != request.status) {
+ do_throw("request.status does not match status arg to onStopRequest!");
+ }
+ if (request.isPending()) {
+ do_throw("request reports itself as pending from onStopRequest!");
+ }
+ if (
+ !(
+ this._flags &
+ (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)
+ ) &&
+ !(this._flags & CL_EXPECT_GZIP) &&
+ this._contentLen != -1
+ ) {
+ Assert.equal(this._buffer.length, this._contentLen);
+ }
+ } catch (ex) {
+ do_throw("Error in onStopRequest: " + ex);
+ }
+ try {
+ this._closure(
+ request,
+ this._buffer,
+ this._closurectx,
+ this._isFromCache,
+ this._cacheEntryId
+ );
+ this._closurectx = null;
+ } catch (ex) {
+ do_throw("Error in closure function: " + ex);
+ }
+ },
+};
+
+var ES_ABORT_REDIRECT = 0x01;
+
+function ChannelEventSink(flags) {
+ this._flags = flags;
+}
+
+ChannelEventSink.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIChannelEventSink)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ if (this._flags & ES_ABORT_REDIRECT) {
+ throw Components.Exception("", Cr.NS_BINDING_ABORTED);
+ }
+
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ },
+};
+
+/**
+ * A helper class to construct origin attributes.
+ */
+function OriginAttributes(inIsolatedMozBrowser, privateId) {
+ this.inIsolatedMozBrowser = inIsolatedMozBrowser;
+ this.privateBrowsingId = privateId;
+}
+OriginAttributes.prototype = {
+ inIsolatedMozBrowser: false,
+ privateBrowsingId: 0,
+};
+
+function readFile(file) {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+ return data;
+}
+
+function addCertFromFile(certdb, filename, trustString) {
+ let certFile = do_get_file(filename, false);
+ let pem = readFile(certFile)
+ .replace(/-----BEGIN CERTIFICATE-----/, "")
+ .replace(/-----END CERTIFICATE-----/, "")
+ .replace(/[\r\n]/g, "");
+ certdb.addCertFromBase64(pem, trustString);
+}
+
+// Helper code to test nsISerializable
+function serialize_to_escaped_string(obj) {
+ let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ objectOutStream.setOutputStream(pipe.outputStream);
+ objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true);
+ objectOutStream.close();
+
+ let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+ let data = [];
+ // This reads all the data from the stream until an error occurs.
+ while (true) {
+ try {
+ let bytes = objectInStream.readByteArray(1);
+ data.push(String.fromCharCode.apply(null, bytes));
+ } catch (e) {
+ break;
+ }
+ }
+ return escape(data.join(""));
+}
+
+function deserialize_from_escaped_string(str) {
+ let payload = unescape(str);
+ let data = [];
+ let i = 0;
+ while (i < payload.length) {
+ data.push(payload.charCodeAt(i++));
+ }
+
+ let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIObjectOutputStream
+ );
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ objectOutStream.setOutputStream(pipe.outputStream);
+ objectOutStream.writeByteArray(data);
+ objectOutStream.close();
+
+ let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIObjectInputStream
+ );
+ objectInStream.setInputStream(pipe.inputStream);
+ return objectInStream.readObject(true);
+}
+
+async function asyncStartTLSTestServer(
+ serverBinName,
+ certsPath,
+ addDefaultRoot = true
+) {
+ const { HttpServer } = ChromeUtils.import(
+ "resource://testing-common/httpd.js"
+ );
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ // The trusted CA that is typically used for "good" certificates.
+ if (addDefaultRoot) {
+ addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u");
+ }
+
+ const CALLBACK_PORT = 8444;
+
+ let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ Services.env.set("DYLD_LIBRARY_PATH", greBinDir.path);
+ // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
+ // does not return this path on Android, so hard code it here.
+ Services.env.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
+ Services.env.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
+ Services.env.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
+
+ let httpServer = new HttpServer();
+ let serverReady = new Promise(resolve => {
+ httpServer.registerPathHandler(
+ "/",
+ function handleServerCallback(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+ aResponse.setHeader("Content-Type", "text/plain");
+ let responseBody = "OK!";
+ aResponse.bodyOutputStream.write(responseBody, responseBody.length);
+ executeSoon(function () {
+ httpServer.stop(resolve);
+ });
+ }
+ );
+ httpServer.start(CALLBACK_PORT);
+ });
+
+ let serverBin = _getBinaryUtil(serverBinName);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(serverBin);
+ let certDir = do_get_file(certsPath, false);
+ Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`);
+ // Using "sql:" causes the SQL DB to be used so we can run tests on Android.
+ process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2);
+
+ registerCleanupFunction(function () {
+ process.kill();
+ });
+
+ await serverReady;
+}
+
+function _getBinaryUtil(binaryUtilName) {
+ let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // On macOS, GreD is .../Contents/Resources, and most binary utilities
+ // are located there, but certutil is in GreBinD (or .../Contents/MacOS),
+ // so we have to change the path accordingly.
+ if (binaryUtilName === "certutil") {
+ utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ }
+ utilBin.append(binaryUtilName + mozinfo.bin_suffix);
+ // If we're testing locally, the above works. If not, the server executable
+ // is in another location.
+ if (!utilBin.exists()) {
+ utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ while (utilBin.path.includes("xpcshell")) {
+ utilBin = utilBin.parent;
+ }
+ utilBin.append("bin");
+ utilBin.append(binaryUtilName + mozinfo.bin_suffix);
+ }
+ // But maybe we're on Android, where binaries are in /data/local/xpcb.
+ if (!utilBin.exists()) {
+ utilBin.initWithPath("/data/local/xpcb/");
+ utilBin.append(binaryUtilName);
+ }
+ Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`);
+ return utilBin;
+}
+
+function promiseAsyncOpen(chan) {
+ return new Promise(resolve => {
+ chan.asyncOpen(
+ new ChannelListener((req, buf, ctx, isCache, cacheId) => {
+ resolve({ req, buf, ctx, isCache, cacheId });
+ })
+ );
+ });
+}
+
+function hexStringToBytes(hex) {
+ let bytes = [];
+ for (let hexByteStr of hex.split(/(..)/)) {
+ if (hexByteStr.length) {
+ bytes.push(parseInt(hexByteStr, 16));
+ }
+ }
+ return bytes;
+}
+
+function stringToBytes(str) {
+ return Array.from(str, chr => chr.charCodeAt(0));
+}
+
+function BinaryHttpResponse(status, headerNames, headerValues, content) {
+ this.status = status;
+ this.headerNames = headerNames;
+ this.headerValues = headerValues;
+ this.content = content;
+}
+
+BinaryHttpResponse.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIBinaryHttpResponse"]),
+};
+
+function bytesToString(bytes) {
+ return String.fromCharCode.apply(null, bytes);
+}
+
+function check_http_info(request, expected_httpVersion, expected_proxy) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+
+ request.QueryInterface(Ci.nsIProxiedChannel);
+ var httpProxyConnectResponseCode = request.httpProxyConnectResponseCode;
+
+ Assert.equal(expected_httpVersion, httpVersion);
+ if (expected_proxy) {
+ Assert.equal(httpProxyConnectResponseCode, 200);
+ } else {
+ Assert.equal(httpProxyConnectResponseCode, -1);
+ }
+}
+
+function makeHTTPChannel(url, with_proxy) {
+ function createPrincipal(uri) {
+ var ssm = Services.scriptSecurityManager;
+ try {
+ return ssm.createContentPrincipal(Services.io.newURI(uri), {});
+ } catch (e) {
+ return null;
+ }
+ }
+
+ if (with_proxy) {
+ return Services.io
+ .newChannelFromURIWithProxyFlags(
+ Services.io.newURI(url),
+ null,
+ Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL,
+ null,
+ createPrincipal(url),
+ createPrincipal(url),
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ )
+ .QueryInterface(Ci.nsIHttpChannel);
+ }
+ return NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+}