Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / netwerk / test / unit / head_channels.js
blob91c1d8e622618cabc9b68a2b4445bfc9e4d7ebf6
1 /**
2  * Read count bytes from stream and return as a String object
3  */
5 /* import-globals-from head_cache.js */
6 /* import-globals-from head_cookies.js */
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("");
29 const CL_EXPECT_FAILURE = 0x1;
30 const CL_EXPECT_GZIP = 0x2;
31 const CL_EXPECT_3S_DELAY = 0x4;
32 const CL_SUSPEND = 0x8;
33 const CL_ALLOW_UNKNOWN_CL = 0x10;
34 const CL_EXPECT_LATE_FAILURE = 0x20;
35 const CL_FROM_CACHE = 0x40; // Response must be from the cache
36 const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache
37 const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length
39 const SUSPEND_DELAY = 3000;
41 /**
42  * A stream listener that calls a callback function with a specified
43  * context and the received data when the channel is loaded.
44  *
45  * Signature of the closure:
46  *   void closure(in nsIRequest request, in ACString data, in JSObject context);
47  *
48  * This listener makes sure that various parts of the channel API are
49  * implemented correctly and that the channel's status is a success code
50  * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags
51  * to allow a failure code)
52  *
53  * Note that it also requires a valid content length on the channel and
54  * is thus not fully generic.
55  */
56 function ChannelListener(closure, ctx, flags) {
57   this._closure = closure;
58   this._closurectx = ctx;
59   this._flags = flags;
60   this._isFromCache = false;
61   this._cacheEntryId = undefined;
63 ChannelListener.prototype = {
64   _closure: null,
65   _closurectx: null,
66   _buffer: "",
67   _got_onstartrequest: false,
68   _got_onstoprequest: false,
69   _contentLen: -1,
70   _lastEvent: 0,
72   QueryInterface: ChromeUtils.generateQI([
73     "nsIStreamListener",
74     "nsIRequestObserver",
75   ]),
77   onStartRequest(request) {
78     try {
79       if (this._got_onstartrequest) {
80         do_throw("Got second onStartRequest event!");
81       }
82       this._got_onstartrequest = true;
83       this._lastEvent = Date.now();
85       try {
86         this._isFromCache = request
87           .QueryInterface(Ci.nsICacheInfoChannel)
88           .isFromCache();
89       } catch (e) {}
91       var thrown = false;
92       try {
93         this._cacheEntryId = request
94           .QueryInterface(Ci.nsICacheInfoChannel)
95           .getCacheEntryId();
96       } catch (e) {
97         thrown = true;
98       }
99       if (this._isFromCache && thrown) {
100         do_throw("Should get a CacheEntryId");
101       } else if (!this._isFromCache && !thrown) {
102         do_throw("Shouldn't get a CacheEntryId");
103       }
105       request.QueryInterface(Ci.nsIChannel);
106       try {
107         this._contentLen = request.contentLength;
108       } catch (ex) {
109         if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) {
110           do_throw("Could not get contentLength");
111         }
112       }
113       if (!request.isPending()) {
114         do_throw("request reports itself as not pending from onStartRequest!");
115       }
116       if (
117         this._contentLen == -1 &&
118         !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))
119       ) {
120         do_throw("Content length is unknown in onStartRequest!");
121       }
123       if (this._flags & CL_FROM_CACHE) {
124         request.QueryInterface(Ci.nsICachingChannel);
125         if (!request.isFromCache()) {
126           do_throw("Response is not from the cache (CL_FROM_CACHE)");
127         }
128       }
129       if (this._flags & CL_NOT_FROM_CACHE) {
130         request.QueryInterface(Ci.nsICachingChannel);
131         if (request.isFromCache()) {
132           do_throw("Response is from the cache (CL_NOT_FROM_CACHE)");
133         }
134       }
136       if (this._flags & CL_SUSPEND) {
137         request.suspend();
138         do_timeout(SUSPEND_DELAY, function() {
139           request.resume();
140         });
141       }
142     } catch (ex) {
143       do_throw("Error in onStartRequest: " + ex);
144     }
145   },
147   onDataAvailable(request, stream, offset, count) {
148     try {
149       let current = Date.now();
151       if (!this._got_onstartrequest) {
152         do_throw("onDataAvailable without onStartRequest event!");
153       }
154       if (this._got_onstoprequest) {
155         do_throw("onDataAvailable after onStopRequest event!");
156       }
157       if (!request.isPending()) {
158         do_throw("request reports itself as not pending from onDataAvailable!");
159       }
160       if (this._flags & CL_EXPECT_FAILURE) {
161         do_throw("Got data despite expecting a failure");
162       }
164       if (
165         current - this._lastEvent >= SUSPEND_DELAY &&
166         !(this._flags & CL_EXPECT_3S_DELAY)
167       ) {
168         do_throw("Data received after significant unexpected delay");
169       } else if (
170         current - this._lastEvent < SUSPEND_DELAY &&
171         this._flags & CL_EXPECT_3S_DELAY
172       ) {
173         do_throw("Data received sooner than expected");
174       } else if (
175         current - this._lastEvent >= SUSPEND_DELAY &&
176         this._flags & CL_EXPECT_3S_DELAY
177       ) {
178         this._flags &= ~CL_EXPECT_3S_DELAY;
179       } // No more delays expected
181       this._buffer = this._buffer.concat(read_stream(stream, count));
182       this._lastEvent = current;
183     } catch (ex) {
184       do_throw("Error in onDataAvailable: " + ex);
185     }
186   },
188   onStopRequest(request, status) {
189     try {
190       var success = Components.isSuccessCode(status);
191       if (!this._got_onstartrequest) {
192         do_throw("onStopRequest without onStartRequest event!");
193       }
194       if (this._got_onstoprequest) {
195         do_throw("Got second onStopRequest event!");
196       }
197       this._got_onstoprequest = true;
198       if (
199         this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) &&
200         success
201       ) {
202         do_throw(
203           "Should have failed to load URL (status is " +
204             status.toString(16) +
205             ")"
206         );
207       } else if (
208         !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) &&
209         !success
210       ) {
211         do_throw("Failed to load URL: " + status.toString(16));
212       }
213       if (status != request.status) {
214         do_throw("request.status does not match status arg to onStopRequest!");
215       }
216       if (request.isPending()) {
217         do_throw("request reports itself as pending from onStopRequest!");
218       }
219       if (
220         !(
221           this._flags &
222           (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL)
223         ) &&
224         !(this._flags & CL_EXPECT_GZIP) &&
225         this._contentLen != -1
226       ) {
227         Assert.equal(this._buffer.length, this._contentLen);
228       }
229     } catch (ex) {
230       do_throw("Error in onStopRequest: " + ex);
231     }
232     try {
233       this._closure(
234         request,
235         this._buffer,
236         this._closurectx,
237         this._isFromCache,
238         this._cacheEntryId
239       );
240       this._closurectx = null;
241     } catch (ex) {
242       do_throw("Error in closure function: " + ex);
243     }
244   },
247 var ES_ABORT_REDIRECT = 0x01;
249 function ChannelEventSink(flags) {
250   this._flags = flags;
253 ChannelEventSink.prototype = {
254   QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
256   getInterface(iid) {
257     if (iid.equals(Ci.nsIChannelEventSink)) {
258       return this;
259     }
260     throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
261   },
263   asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
264     if (this._flags & ES_ABORT_REDIRECT) {
265       throw Components.Exception("", Cr.NS_BINDING_ABORTED);
266     }
268     callback.onRedirectVerifyCallback(Cr.NS_OK);
269   },
273  * A helper class to construct origin attributes.
274  */
275 function OriginAttributes(inIsolatedMozBrowser, privateId) {
276   this.inIsolatedMozBrowser = inIsolatedMozBrowser;
277   this.privateBrowsingId = privateId;
279 OriginAttributes.prototype = {
280   inIsolatedMozBrowser: false,
281   privateBrowsingId: 0,
284 function readFile(file) {
285   let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
286     Ci.nsIFileInputStream
287   );
288   fstream.init(file, -1, 0, 0);
289   let data = NetUtil.readInputStreamToString(fstream, fstream.available());
290   fstream.close();
291   return data;
294 function addCertFromFile(certdb, filename, trustString) {
295   let certFile = do_get_file(filename, false);
296   let pem = readFile(certFile)
297     .replace(/-----BEGIN CERTIFICATE-----/, "")
298     .replace(/-----END CERTIFICATE-----/, "")
299     .replace(/[\r\n]/g, "");
300   certdb.addCertFromBase64(pem, trustString);
303 // Helper code to test nsISerializable
304 function serialize_to_escaped_string(obj) {
305   let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
306     Ci.nsIObjectOutputStream
307   );
308   let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
309   pipe.init(false, false, 0, 0xffffffff, null);
310   objectOutStream.setOutputStream(pipe.outputStream);
311   objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true);
312   objectOutStream.close();
314   let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
315     Ci.nsIObjectInputStream
316   );
317   objectInStream.setInputStream(pipe.inputStream);
318   let data = [];
319   // This reads all the data from the stream until an error occurs.
320   while (true) {
321     try {
322       let bytes = objectInStream.readByteArray(1);
323       data.push(String.fromCharCode.apply(null, bytes));
324     } catch (e) {
325       break;
326     }
327   }
328   return escape(data.join(""));
331 function deserialize_from_escaped_string(str) {
332   let payload = unescape(str);
333   let data = [];
334   let i = 0;
335   while (i < payload.length) {
336     data.push(payload.charCodeAt(i++));
337   }
339   let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
340     Ci.nsIObjectOutputStream
341   );
342   let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
343   pipe.init(false, false, 0, 0xffffffff, null);
344   objectOutStream.setOutputStream(pipe.outputStream);
345   objectOutStream.writeByteArray(data);
346   objectOutStream.close();
348   let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
349     Ci.nsIObjectInputStream
350   );
351   objectInStream.setInputStream(pipe.inputStream);
352   return objectInStream.readObject(true);
355 // Copied from head_psm.js.
356 function add_tls_server_setup(serverBinName, certsPath, addDefaultRoot = true) {
357   add_test(function() {
358     _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot);
359   });
362 // Do not call this directly; use add_tls_server_setup
363 function _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot) {
364   asyncStartTLSTestServer(serverBinName, certsPath, addDefaultRoot).then(
365     run_next_test
366   );
369 async function asyncStartTLSTestServer(
370   serverBinName,
371   certsPath,
372   addDefaultRoot
373 ) {
374   const { HttpServer } = ChromeUtils.import(
375     "resource://testing-common/httpd.js"
376   );
377   let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
378     Ci.nsIX509CertDB
379   );
380   // The trusted CA that is typically used for "good" certificates.
381   if (addDefaultRoot) {
382     addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u");
383   }
385   const CALLBACK_PORT = 8444;
387   let envSvc = Cc["@mozilla.org/process/environment;1"].getService(
388     Ci.nsIEnvironment
389   );
390   let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile);
391   envSvc.set("DYLD_LIBRARY_PATH", greBinDir.path);
392   // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD"
393   // does not return this path on Android, so hard code it here.
394   envSvc.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb");
395   envSvc.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3");
396   envSvc.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT);
398   let httpServer = new HttpServer();
399   let serverReady = new Promise(resolve => {
400     httpServer.registerPathHandler("/", function handleServerCallback(
401       aRequest,
402       aResponse
403     ) {
404       aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
405       aResponse.setHeader("Content-Type", "text/plain");
406       let responseBody = "OK!";
407       aResponse.bodyOutputStream.write(responseBody, responseBody.length);
408       executeSoon(function() {
409         httpServer.stop(resolve);
410       });
411     });
412     httpServer.start(CALLBACK_PORT);
413   });
415   let serverBin = _getBinaryUtil(serverBinName);
416   let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
417   process.init(serverBin);
418   let certDir = do_get_file(certsPath, false);
419   Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`);
420   // Using "sql:" causes the SQL DB to be used so we can run tests on Android.
421   process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2);
423   registerCleanupFunction(function() {
424     process.kill();
425   });
427   await serverReady;
430 function _getBinaryUtil(binaryUtilName) {
431   let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile);
432   // On macOS, GreD is .../Contents/Resources, and most binary utilities
433   // are located there, but certutil is in GreBinD (or .../Contents/MacOS),
434   // so we have to change the path accordingly.
435   if (binaryUtilName === "certutil") {
436     utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile);
437   }
438   utilBin.append(binaryUtilName + mozinfo.bin_suffix);
439   // If we're testing locally, the above works. If not, the server executable
440   // is in another location.
441   if (!utilBin.exists()) {
442     utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
443     while (utilBin.path.includes("xpcshell")) {
444       utilBin = utilBin.parent;
445     }
446     utilBin.append("bin");
447     utilBin.append(binaryUtilName + mozinfo.bin_suffix);
448   }
449   // But maybe we're on Android, where binaries are in /data/local/xpcb.
450   if (!utilBin.exists()) {
451     utilBin.initWithPath("/data/local/xpcb/");
452     utilBin.append(binaryUtilName);
453   }
454   Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`);
455   return utilBin;
458 function promiseAsyncOpen(chan) {
459   return new Promise(resolve => {
460     chan.asyncOpen(
461       new ChannelListener((req, buf, ctx, isCache, cacheId) => {
462         resolve({ req, buf, ctx, isCache, cacheId });
463       })
464     );
465   });