Bug 484027 - Add a method providing minimally controlled arbitrary write access to the connection within a response, allowing arbitrary information (even data which is not a syntactically valid HTTP response) to be sent in responses. r=sayrer UPDATE_PACKAGING_R8
authorJeff Walden <jwalden@mit.edu>
Thu, 28 May 2009 14:54:42 -0700
changeset 28828 fe9cc55b8db7f56f7e68a246acba363743854979
parent 28827 591ad6f4cc76089c4565e1acfa10ea5acc163e98
child 28829 bb4148c7ff615d706c26feb24c879cb6b665155c
push idunknown
push userunknown
push dateunknown
reviewerssayrer
bugs484027
milestone1.9.2a1pre
Bug 484027 - Add a method providing minimally controlled arbitrary write access to the connection within a response, allowing arbitrary information (even data which is not a syntactically valid HTTP response) to be sent in responses. r=sayrer
netwerk/test/httpserver/httpd.js
netwerk/test/httpserver/nsIHttpServer.idl
netwerk/test/httpserver/test/test_processasync.js
netwerk/test/httpserver/test/test_seizepower.js
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -3327,16 +3327,23 @@ function Response(connection)
    */
   this._processAsync = false;
 
   /**
    * True iff finish() has been called on this, signaling that no more changes
    * to this may be made.
    */
   this._finished = false;
+
+  /**
+   * True iff powerSeized() has been called on this, signaling that this
+   * response is to be handled manually by the response handler (which may then
+   * send arbitrary data in response, even non-HTTP responses).
+   */
+  this._powerSeized = false;
 }
 Response.prototype =
 {
   // PUBLIC CONSTRUCTION API
 
   //
   // see nsIHttpResponse.bodyOutputStream
   //
@@ -3346,17 +3353,17 @@ Response.prototype =
       throw Cr.NS_ERROR_NOT_AVAILABLE;
 
     if (!this._bodyOutputStream)
     {
       var pipe = new Pipe(false, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
                           null);
       this._bodyOutputStream = pipe.outputStream;
       this._bodyInputStream = pipe.inputStream;
-      if (this._processAsync)
+      if (this._processAsync || this._powerSeized)
         this._startAsyncProcessor();
     }
 
     return this._bodyOutputStream;
   },
 
   //
   // see nsIHttpResponse.write
@@ -3370,17 +3377,17 @@ Response.prototype =
     this.bodyOutputStream.write(dataAsString, dataAsString.length);
   },
 
   //
   // see nsIHttpResponse.setStatusLine
   //
   setStatusLine: function(httpVersion, code, description)
   {
-    if (!this._headers || this._finished)
+    if (!this._headers || this._finished || this._powerSeized)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
     this._ensureAlive();
 
     if (!(code >= 0 && code < 1000))
       throw Cr.NS_ERROR_INVALID_ARG;
 
     try
     {
@@ -3415,32 +3422,35 @@ Response.prototype =
     this._httpVersion = httpVer;
   },
 
   //
   // see nsIHttpResponse.setHeader
   //
   setHeader: function(name, value, merge)
   {
-    if (!this._headers || this._finished)
+    if (!this._headers || this._finished || this._powerSeized)
       throw Cr.NS_ERROR_NOT_AVAILABLE;
     this._ensureAlive();
 
     this._headers.setHeader(name, value, merge);
   },
 
   //
   // see nsIHttpResponse.processAsync
   //
   processAsync: function()
   {
     if (this._finished)
       throw Cr.NS_ERROR_UNEXPECTED;
+    if (this._powerSeized)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
     if (this._processAsync)
       return;
+    this._ensureAlive();
 
     dumpn("*** processing connection " + this._connection.number + " async");
     this._processAsync = true;
 
     /*
      * Either the bodyOutputStream getter or this method is responsible for
      * starting the asynchronous processor and catching writes of data to the
      * response body of async responses as they happen, for the purpose of
@@ -3453,32 +3463,69 @@ Response.prototype =
      * until finish() is called.  Since that delay is easily avoided by simply
      * getting bodyOutputStream or calling write(""), we don't worry about it.
      */
     if (this._bodyOutputStream && !this._asyncCopier)
       this._startAsyncProcessor();
   },
 
   //
+  // see nsIHttpResponse.seizePower
+  //
+  seizePower: function()
+  {
+    if (this._processAsync)
+      throw Cr.NS_ERROR_NOT_AVAILABLE;
+    if (this._finished)
+      throw Cr.NS_ERROR_UNEXPECTED;
+    if (this._powerSeized)
+      return;
+    this._ensureAlive();
+
+    dumpn("*** forcefully seizing power over connection " +
+          this._connection.number + "...");
+
+    // Purge any already-written data without sending it.  We could as easily
+    // swap out the streams entirely, but that makes it possible to acquire and
+    // unknowingly use a stale reference, so we require there only be one of
+    // each stream ever for any response to avoid this complication.
+    if (this._asyncCopier)
+      this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
+    this._asyncCopier = null;
+    if (this._bodyOutputStream)
+    {
+      var input = new BinaryInputStream(this._bodyInputStream);
+      var avail;
+      while ((avail = input.available()) > 0)
+        input.readByteArray(avail);
+    }
+
+    this._powerSeized = true;
+    if (this._bodyOutputStream)
+      this._startAsyncProcessor();
+  },
+
+  //
   // see nsIHttpResponse.finish
   //
   finish: function()
   {
-    if (!this._processAsync)
+    if (!this._processAsync && !this._powerSeized)
       throw Cr.NS_ERROR_UNEXPECTED;
     if (this._finished)
       return;
 
-    dumpn("*** finishing async connection " + this._connection.number);
+    dumpn("*** finishing connection " + this._connection.number);
     this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
     if (this._bodyOutputStream)
       this._bodyOutputStream.close();
     this._finished = true;
   },
 
+
   // POST-CONSTRUCTION API (not exposed externally)
 
   /**
    * The HTTP version number of this, as a string (e.g. "1.1").
    */
   get httpVersion()
   {
     this._ensureAlive();
@@ -3527,68 +3574,98 @@ Response.prototype =
   {
     this._ensureAlive();
 
     return this._headers.getHeader(name);
   },
 
   /**
    * Determines whether this response may be abandoned in favor of a newly
-   * constructed response, as determined by whether any of this response's data
-   * has been written to the network.
+   * constructed response.  A response may be abandoned only if it is not being
+   * sent asynchronously and if raw control over it has not been taken from the
+   * server.
    *
    * @returns boolean
    *   true iff no data has been written to the network
    */
   partiallySent: function()
   {
     dumpn("*** partiallySent()");
-    return this._headers === null;
+    return this._processAsync || this._powerSeized;
   },
 
   /**
    * If necessary, kicks off the remaining request processing needed to be done
    * after a request handler performs its initial work upon this response.
    */
   complete: function()
   {
     dumpn("*** complete()");
-    if (this._processAsync)
+    if (this._processAsync || this._powerSeized)
+    {
+      NS_ASSERT(this._processAsync ^ this._powerSeized,
+                "can't both send async and relinquish power");
       return;
+    }
 
     NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
 
     this._startAsyncProcessor();
 
     // Now make sure we finish processing this request!
     if (this._bodyOutputStream)
       this._bodyOutputStream.close();
   },
 
   /**
    * Abruptly ends processing of this response, usually due to an error in an
    * incoming request but potentially due to a bad error handler.  Since we
-   * cannot handle the error in the usual way (giving an HTTP error page in response)
-   * because data may already have been sent, we stop processing this response
-   * and abruptly close the connection.
+   * cannot handle the error in the usual way (giving an HTTP error page in
+   * response) because data may already have been sent (or because the response
+   * might be expected to have been generated asynchronously or completely from
+   * scratch by the handler), we stop processing this response and abruptly
+   * close the connection.
    *
    * @param e : Error
    *   the exception which precipitated this abort, or null if no such exception
    *   was generated
    */
   abort: function(e)
   {
     dumpn("*** abort(<" + e + ">)");
 
     // This response will be ended by the processor if one was created.
-    var processor = this._asyncCopier;
-    if (processor)
-      processor.cancel(Cr.NS_BINDING_ABORTED);
+    var copier = this._asyncCopier;
+    if (copier)
+    {
+      // We dispatch asynchronously here so that any pending writes of data to
+      // the connection will be deterministically written.  This makes it easier
+      // to specify exact behavior, and it makes observable behavior more
+      // predictable for clients.  Note that the correctness of this depends on
+      // callbacks in response to _waitForData in WriteThroughCopier happening
+      // asynchronously with respect to the actual writing of data to
+      // bodyOutputStream, as they currently do; if they happened synchronously,
+      // an event which ran before this one could write more data to the
+      // response body before we get around to canceling the copier.  We have
+      // tests for this in test_seizepower.js, however, and I can't think of a
+      // way to handle both cases without removing bodyOutputStream access and
+      // moving its effective write(data, length) method onto Response, which
+      // would be slower and require more code than this anyway.
+      gThreadManager.currentThread.dispatch({
+        run: function()
+        {
+          dumpn("*** canceling copy asynchronously...");
+          copier.cancel(Cr.NS_ERROR_UNEXPECTED);
+        }
+      }, Ci.nsIThreadManager.DISPATCH_NORMAL);
+    }
     else
+    {
       this.end();
+    }
   },
 
   /**
    * Closes this response's network connection, marks the response as finished,
    * and notifies the server handler that the request is done being processed.
    */
   end: function()
   {
@@ -3611,16 +3688,17 @@ Response.prototype =
    * in this response cannot be sent -- the only possible action in case of
    * error is to abort the response and close the connection.
    */
   _sendHeaders: function()
   {
     dumpn("*** _sendHeaders()");
 
     NS_ASSERT(this._headers);
+    NS_ASSERT(!this._powerSeized);
 
     // request-line
     var statusLine = "HTTP/" + this.httpVersion + " " +
                      this.httpCode + " " +
                      this.httpDescription + "\r\n";
 
     // header post-processing
 
@@ -3704,18 +3782,23 @@ Response.prototype =
     if (this._asyncCopier || this._ended)
     {
       dumpn("*** ignoring second call to _startAsyncProcessor");
       return;
     }
 
     // Send headers if they haven't been sent already.
     if (this._headers)
-      this._sendHeaders();
-    NS_ASSERT(this._headers === null, "flushHeaders() failed?");
+    {
+      if (this._powerSeized)
+        this._headers = null;
+      else
+        this._sendHeaders();
+      NS_ASSERT(this._headers === null, "_sendHeaders() failed?");
+    }
 
     var response = this;
     var connection = this._connection;
 
     // If no body data was written, we're done
     if (!this._bodyInputStream)
     {
       dumpn("*** empty body, response finished");
@@ -3727,25 +3810,29 @@ Response.prototype =
       {
         onStartRequest: function(request, context)
         {
           dumpn("*** onStartRequest");
         },
 
         onStopRequest: function(request, cx, statusCode)
         {
-          dumpn("*** onStopRequest [status=" + statusCode.toString(16) + "]");
-
-          if (!Components.isSuccessCode(statusCode))
+          dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
+
+          if (statusCode === Cr.NS_BINDING_ABORTED)
           {
-            dumpn("*** WARNING: non-success statusCode in onStopRequest: " +
-                  statusCode);
+            dumpn("*** terminating copy observer without ending the response");
           }
-
-          response.end();
+          else
+          {
+            if (!Components.isSuccessCode(statusCode))
+              dumpn("*** WARNING: non-success statusCode in onStopRequest");
+
+            response.end();
+          }
         },
 
         QueryInterface: function(aIID)
         {
           if (aIID.equals(Ci.nsIRequestObserver) ||
               aIID.equals(Ci.nsISupports))
             return this;
 
@@ -3779,18 +3866,19 @@ function notImplemented()
 }
 
 /**
  * Copies data from input to output as it becomes available.
  *
  * @param input : nsIAsyncInputStream
  *   the stream from which data is to be read
  * @param output : nsIOutputStream
+ *   the stream to which data is to be copied
  * @param observer : nsIRequestObserver
- *   an observer which will be notified when
+ *   an observer which will be notified when the copy starts and finishes
  * @param context : nsISupports
  *   context passed to observer when notified of start/stop
  * @throws NS_ERROR_NULL_POINTER
  *   if input, output, or observer are null
  */
 function WriteThroughCopier(input, output, observer, context)
 {
   if (!input || !output || !observer)
@@ -3842,17 +3930,20 @@ WriteThroughCopier.prototype =
    * @param status : nsresult
    *   the status to pass to the observer when data copying has been canceled
    */
   cancel: function(status)
   {
     dumpn("*** cancel(" + status.toString(16) + ")");
 
     if (this._completed)
+    {
+      dumpn("*** ignoring cancel on already-canceled copier...");
       return;
+    }
 
     this._completed = true;
     this.status = status;
 
     var self = this;
     var cancelEvent =
       {
         run: function()
@@ -3885,23 +3976,26 @@ WriteThroughCopier.prototype =
   suspend: notImplemented,
   /** Not implemented, don't use! */
   resume: notImplemented,
 
   /**
    * Receives a more-data-in-input notification and writes the corresponding
    * data to the output.
    */
-  onInputStreamReady: function()
+  onInputStreamReady: function(input)
   {
     dumpn("*** onInputStreamReady");
     if (this._completed)
+    {
+      dumpn("*** ignoring stream-ready callback on a canceled copier...");
       return;
-
-    var input = new BinaryInputStream(this._input);
+    }
+
+    input = new BinaryInputStream(input);
     try
     {
       var avail = input.available();
       var data = input.readByteArray(avail);
       this._output.writeByteArray(data, data.length);
     }
     catch (e)
     {
@@ -3926,16 +4020,29 @@ WriteThroughCopier.prototype =
 
   /**
    * Kicks off another wait for more data to be available from the input stream.
    */
   _waitForData: function()
   {
     dumpn("*** _waitForData");
     this._input.asyncWait(this, 0, 1, gThreadManager.mainThread);
+  },
+
+  /** nsISupports implementation */
+  QueryInterface: function(iid)
+  {
+    if (iid.equals(Ci.nsIRequest) ||
+        iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIInputStreamCallback))
+    {
+      return this;
+    }
+
+    throw Cr.NS_ERROR_NO_INTERFACE;
   }
 };
 
 
 /**
  * A container for utility functions used with HTTP headers.
  */
 const headerUtils =
--- a/netwerk/test/httpserver/nsIHttpServer.idl
+++ b/netwerk/test/httpserver/nsIHttpServer.idl
@@ -360,19 +360,27 @@ interface nsIHttpServerIdentity : nsISup
  */
 [scriptable, function, uuid(2bbb4db7-d285-42b3-a3ce-142b8cc7e139)]
 interface nsIHttpRequestHandler : nsISupports
 {
   /**
    * Processes the HTTP request represented by metadata and initializes the
    * passed-in response to reflect the correct HTTP response.
    *
-   * Note that in some uses of nsIHttpRequestHandler, this method is required to
-   * not throw an exception; in the general case, however, this method may throw
-   * an exception (causing an HTTP 500 response to occur).
+   * If this method throws an exception, externally observable behavior depends
+   * upon whether is being processed asynchronously and the connection has had
+   * any data written to it (even an explicit zero bytes of data being written)
+   * or whether seizePower() has been called on it.  If such has happened, sent
+   * data will be exactly that data written at the time the exception was
+   * thrown.  If no data has been written, the response has not had seizePower()
+   * called on it, and it is not being asynchronously created, an error handler
+   * will be invoked (usually 500 unless otherwise specified).  Note that some
+   * uses of nsIHttpRequestHandler may require this method to never throw an
+   * exception; in the general case, however, this method may throw an exception
+   * (causing an HTTP 500 response to occur).
    *
    * @param metadata
    *   data representing an HTTP request
    * @param response
    *   an initially-empty response which must be modified to reflect the data
    *   which should be sent as the response to the request described by metadata
    */
   void handle(in nsIHttpRequestMetadata metadata, in nsIHttpResponse response);
@@ -499,17 +507,18 @@ interface nsIHttpResponse : nsISupports
    * @param description
    *   a human-readable description of code; may be null if no description is
    *   desired
    * @throws NS_ERROR_INVALID_ARG
    *   if httpVersion is not a valid HTTP version string, statusCode is greater
    *   than 999, or description contains invalid characters
    * @throws NS_ERROR_NOT_AVAILABLE
    *   if this response is being processed asynchronously and data has been
-   *   written to this response's body
+   *   written to this response's body, or if seizePower() has been called on
+   *   this
    */
   void setStatusLine(in string httpVersion,
                      in unsigned short statusCode,
                      in string description);
 
   /**
    * Sets the specified header in this.
    *
@@ -525,33 +534,39 @@ interface nsIHttpResponse : nsISupports
    *   Proxy-Authenticate headers, which will treat each such merged header as
    *   an additional instance of the header, for real-world compatibility
    *   reasons); when false, replaces any existing header of the given name (if
    *   any exists) with a new header with the specified value
    * @throws NS_ERROR_INVALID_ARG
    *   if name or value is not a valid header component
    * @throws NS_ERROR_NOT_AVAILABLE
    *   if this response is being processed asynchronously and data has been
-   *   written to this response's body
+   *   written to this response's body, or if seizePower() has been called on
+   *   this
    */
   void setHeader(in string name, in string value, in boolean merge);
 
   /**
-   * A stream to which data appearing in the body of this response should be
-   * written.  After this response has been designated as being processed
-   * asynchronously, subsequent writes will be synchronously written to the
-   * underlying transport.  However, immediate write-through visible to the HTTP
-   * client cannot be guaranteed, as intermediate buffers both in the server
-   * socket and in the client may delay written data; be prepared for potential
-   * delays.
+   * A stream to which data appearing in the body of this response (or in the
+   * totality of the response if seizePower() is called) should be written.
+   * After this response has been designated as being processed asynchronously,
+   * or after seizePower() has been called on this, subsequent writes will no
+   * longer be buffered and will be written to the underlying transport without
+   * delaying until the entire response is constructed.  Write-through may or
+   * may not be synchronous in the implementation, and in any case particular
+   * behavior may not be observable to the HTTP client as intermediate buffers
+   * both in the server socket and in the client may delay written data; be
+   * prepared for delays at any time.
    *
    * @note
-   *   As writes to the underlying transport are synchronous, care must be taken
-   *   not to block on these writes; it is even possible for deadlock to occur
-   *   in the case that the server and the client reside in the same process.
+   *   Although in the asynchronous cases writes to the underlying transport
+   *   are not buffered, care must still be taken not to block for too long on
+   *   any such writes; it is even possible for deadlock to occur in the case
+   *   that the server and the client reside in the same process.  Write data in
+   *   small chunks if necessary to avoid this problem.
    * @throws NS_ERROR_NOT_AVAILABLE
    *   if accessed after this response is fully constructed
    */
   readonly attribute nsIOutputStream bodyOutputStream;
 
   /**
    * Writes a string to the response's output stream.  This method is merely a
    * convenient shorthand for writing the same data to bodyOutputStream
@@ -573,21 +588,48 @@ interface nsIHttpResponse : nsISupports
    * only has this effect when called during nsIHttpRequestHandler.handle;
    * behavior is undefined if it is called at a later time.  It may be called
    * multiple times with no ill effect, so long as each call occurs before
    * finish() is called.
    *
    * @throws NS_ERROR_UNEXPECTED
    *   if not initially called within a nsIHttpRequestHandler.handle call or if
    *   called after this response has been finished
+   * @throws NS_ERROR_NOT_AVAILABLE
+   *   if seizePower() has been called on this
    */
   void processAsync();
 
   /**
+   * Seizes complete control of this response (and its connection) from the
+   * server, allowing raw and unfettered access to data being sent in the HTTP
+   * response.  Once this method has been called the only property which may be
+   * accessed without an exception being thrown is bodyOutputStream, and the
+   * only methods which may be accessed without an exception being thrown are
+   * write(), finish(), and seizePower() (which may be called multiple times
+   * without ill effect so long as all calls are otherwise allowed).
+   *
+   * After a successful call, all data subsequently written to the body of this
+   * response is written directly to the corresponding connection.  (Previously-
+   * written data is silently discarded.)  No status line or headers are sent
+   * before doing so; if the response handler wishes to write such data, it must
+   * do so manually.  Data generation completes only when finish() is called; it
+   * is not enough to simply call close() on bodyOutputStream.
+   *
+   * @throws NS_ERROR_NOT_AVAILABLE
+   *   if processAsync() has been called on this
+   * @throws NS_ERROR_UNEXPECTED
+   *   if finish() has been called on this
+   */
+  void seizePower();
+
+  /**
    * Signals that construction of this response is complete and that it may be
-   * sent over the network to the client.  This method may only be called after
-   * processAsync() has been called.  This method is idempotent.
+   * sent over the network to the client, or if seizePower() has been called
+   * signals that all data has been written and that the underlying connection
+   * may be closed.  This method may only be called after processAsync() or
+   * seizePower() has been called.  This method is idempotent.
    *
    * @throws NS_ERROR_UNEXPECTED
-   *   if processAsync() has not already been properly called
+   *   if processAsync() or seizePower() has not already been properly called
    */
   void finish();
 };
--- a/netwerk/test/httpserver/test/test_processasync.js
+++ b/netwerk/test/httpserver/test/test_processasync.js
@@ -294,22 +294,18 @@ function start_handleAsyncError(ch, cx)
   do_check_eq(ch.getResponseHeader("X-Foo"), "header value");
 }
 
 function stop_handleAsyncError(ch, cx, status, data)
 {
   // Lies!  But not really!
   do_check_true(ch.requestSucceeded);
 
-  // There's no way server APIs will ever guarantee exactly what data will show
-  // up here, but they will guarantee sending a (not necessarily strict) prefix
-  // of what was written.
-  do_check_true(data.length <= ASYNC_ERROR_BODY.length);
-  for (var i = 0, sz = data.length; i < sz; i++)
-    do_check_eq(data[i] == ASYNC_ERROR_BODY.charCodeAt(i));
+  do_check_eq(data.length, ASYNC_ERROR_BODY.length);
+  do_check_eq(String.fromCharCode.apply(null, data), ASYNC_ERROR_BODY);
 }
 
 test = new Test(PREPATH + "/handleAsyncError",
                 null, start_handleAsyncError, stop_handleAsyncError);
 tests.push(test);
 
 
 /*
new file mode 100644
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_seizepower.js
@@ -0,0 +1,309 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is httpd.js code.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jeff Walden <jwalden+code@mit.edu> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Tests that the seizePower API works correctly.
+ */
+
+const PORT = 4444;
+
+var srv;
+
+function run_test()
+{
+  srv = createServer();
+
+  srv.registerPathHandler("/raw-data", handleRawData);
+  srv.registerPathHandler("/called-too-late", handleTooLate);
+  srv.registerPathHandler("/exceptions", handleExceptions);
+  srv.registerPathHandler("/async-seizure", handleAsyncSeizure);
+  srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync);
+  srv.registerPathHandler("/thrown-exception", handleThrownException);
+  srv.registerPathHandler("/asap-later-write", handleASAPLaterWrite);
+  srv.registerPathHandler("/asap-later-finish", handleASAPLaterFinish);
+
+  srv.start(PORT);
+
+  runRawTests(tests, testComplete(srv));
+}
+
+
+function checkException(fun, err, msg)
+{
+  try
+  {
+    fun();
+  }
+  catch (e)
+  {
+    if (e !== err && e.result !== err)
+      do_throw(msg);
+    return;
+  }
+  do_throw(msg);
+}
+
+function callASAPLater(fun)
+{
+  gThreadManager.currentThread.dispatch({
+    run: function()
+    {
+      fun();
+    }
+  }, Ci.nsIThreadManager.DISPATCH_NORMAL);
+}
+
+
+/*****************
+ * PATH HANDLERS *
+ *****************/
+
+function handleRawData(request, response)
+{
+  response.seizePower();
+  response.write("Raw data!");
+  response.finish();
+}
+
+function handleTooLate(request, response)
+{
+  response.write("DO NOT WANT");
+  var output = response.bodyOutputStream;
+
+  response.seizePower();
+
+  if (response.bodyOutputStream !== output)
+    response.write("bodyOutputStream changed!");
+  else
+    response.write("too-late passed");
+  response.finish();
+}
+
+function handleExceptions(request, response)
+{
+  response.seizePower();
+  checkException(function() { response.setStatusLine("1.0", 500, "ISE"); },
+                 Cr.NS_ERROR_NOT_AVAILABLE,
+                 "setStatusLine should throw not-available after seizePower");
+  checkException(function() { response.setHeader("X-Fail", "FAIL", false); },
+                 Cr.NS_ERROR_NOT_AVAILABLE,
+                 "setHeader should throw not-available after seizePower");
+  checkException(function() { response.processAsync(); },
+                 Cr.NS_ERROR_NOT_AVAILABLE,
+                 "processAsync should throw not-available after seizePower");
+  var out = response.bodyOutputStream;
+  var data = "exceptions test passed";
+  out.write(data, data.length);
+  response.seizePower(); // idempotency test of seizePower
+  response.finish();
+  response.finish(); // idempotency test of finish after seizePower
+  checkException(function() { response.seizePower(); },
+                 Cr.NS_ERROR_UNEXPECTED,
+                 "seizePower should throw unexpected after finish");
+}
+
+function handleAsyncSeizure(request, response)
+{
+  response.seizePower();
+  callLater(1, function()
+  {
+    response.write("async seizure passed");
+    response.bodyOutputStream.close();
+    callLater(1, function()
+    {
+      response.finish();
+    });
+  });
+}
+
+function handleSeizeAfterAsync(request, response)
+{
+  response.setStatusLine(request.httpVersion, 200, "async seizure pass");
+  response.processAsync();
+  checkException(function() { response.seizePower(); },
+                 Cr.NS_ERROR_NOT_AVAILABLE,
+                 "seizePower should throw not-available after processAsync");
+  callLater(1, function()
+  {
+    response.finish();
+  });
+}
+
+function handleThrownException(request, response)
+{
+  if (request.queryString === "writeBefore")
+    response.write("ignore this");
+  else if (request.queryString === "writeBeforeEmpty")
+    response.write("");
+  else if (request.queryString !== "")
+    throw "query string FAIL";
+  response.seizePower();
+  response.write("preparing to throw...");
+  throw "badness 10000";
+}
+
+function handleASAPLaterWrite(request, response)
+{
+  response.seizePower();
+  response.write("should only ");
+  response.write("see this");
+
+  callASAPLater(function()
+  {
+    response.write("...and not this");
+    callASAPLater(function()
+    {
+      response.write("...or this");
+      response.finish();
+    });
+  });
+
+  throw "opening pitch of the ballgame";
+}
+
+function handleASAPLaterFinish(request, response)
+{
+  response.seizePower();
+  response.write("should only see this");
+
+  callASAPLater(function()
+  {
+    response.finish();
+  });
+
+  throw "out the bum!";
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+var test, data;
+var tests = [];
+
+data = "GET /raw-data HTTP/1.0\r\n" +
+       "\r\n";
+function checkRawData(data)
+{
+  do_check_eq(data, "Raw data!");
+}
+test = new RawTest("localhost", PORT, data, checkRawData),
+tests.push(test);
+
+data = "GET /called-too-late HTTP/1.0\r\n" +
+       "\r\n";
+function checkTooLate(data)
+{
+  do_check_eq(LineIterator(data).next(), "too-late passed");
+}
+test = new RawTest("localhost", PORT, data, checkTooLate),
+tests.push(test);
+
+data = "GET /exceptions HTTP/1.0\r\n" +
+       "\r\n";
+function checkExceptions(data)
+{
+  do_check_eq("exceptions test passed", data);
+}
+test = new RawTest("localhost", PORT, data, checkExceptions),
+tests.push(test);
+
+data = "GET /async-seizure HTTP/1.0\r\n" +
+       "\r\n";
+function checkAsyncSeizure(data)
+{
+  do_check_eq(data, "async seizure passed");
+}
+test = new RawTest("localhost", PORT, data, checkAsyncSeizure),
+tests.push(test);
+
+data = "GET /seize-after-async HTTP/1.0\r\n" +
+       "\r\n";
+function checkSeizeAfterAsync(data)
+{
+  do_check_eq(LineIterator(data).next(), "HTTP/1.0 200 async seizure pass");
+}
+test = new RawTest("localhost", PORT, data, checkSeizeAfterAsync),
+tests.push(test);
+
+data = "GET /thrown-exception?writeBefore HTTP/1.0\r\n" +
+       "\r\n";
+function checkThrownExceptionWriteBefore(data)
+{
+  do_check_eq(data, "preparing to throw...");
+}
+test = new RawTest("localhost", PORT, data, checkThrownExceptionWriteBefore),
+tests.push(test);
+
+data = "GET /thrown-exception?writeBeforeEmpty HTTP/1.0\r\n" +
+       "\r\n";
+function checkThrownExceptionWriteBefore(data)
+{
+  do_check_eq(data, "preparing to throw...");
+}
+test = new RawTest("localhost", PORT, data, checkThrownExceptionWriteBefore),
+tests.push(test);
+
+data = "GET /thrown-exception HTTP/1.0\r\n" +
+       "\r\n";
+function checkThrownException(data)
+{
+  do_check_eq(data, "preparing to throw...");
+}
+test = new RawTest("localhost", PORT, data, checkThrownException),
+tests.push(test);
+
+data = "GET /asap-later-write HTTP/1.0\r\n" +
+       "\r\n";
+function checkASAPLaterWrite(data)
+{
+  do_check_eq(data, "should only see this");
+}
+test = new RawTest("localhost", PORT, data, checkASAPLaterWrite),
+tests.push(test);
+
+data = "GET /asap-later-finish HTTP/1.0\r\n" +
+       "\r\n";
+function checkASAPLaterFinish(data)
+{
+  do_check_eq(data, "should only see this");
+}
+test = new RawTest("localhost", PORT, data, checkASAPLaterFinish),
+tests.push(test);