Permalink
Browse files

Bug 484027 - Add a method providing minimally controlled arbitrary wr…

…ite 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

--HG--
extra : rebase_source : 2d61cccef9b076b2e5dbe1074af99f572d60b700
  • Loading branch information...
1 parent e666178 commit fa4f06df47999afa853c745ebc288e53a3429cbd @jswalden jswalden committed May 28, 2009
@@ -3332,6 +3332,13 @@ function Response(connection)
* 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 =
{
@@ -3351,7 +3358,7 @@ Response.prototype =
null);
this._bodyOutputStream = pipe.outputStream;
this._bodyInputStream = pipe.inputStream;
- if (this._processAsync)
+ if (this._processAsync || this._powerSeized)
this._startAsyncProcessor();
}
@@ -3375,7 +3382,7 @@ Response.prototype =
//
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();
@@ -3420,7 +3427,7 @@ Response.prototype =
//
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();
@@ -3434,8 +3441,11 @@ Response.prototype =
{
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;
@@ -3457,23 +3467,60 @@ Response.prototype =
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)
/**
@@ -3532,16 +3579,17 @@ Response.prototype =
/**
* 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;
},
/**
@@ -3551,8 +3599,12 @@ Response.prototype =
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?");
@@ -3566,9 +3618,11 @@ Response.prototype =
/**
* 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
@@ -3579,11 +3633,34 @@ Response.prototype =
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();
+ }
},
/**
@@ -3616,6 +3693,7 @@ Response.prototype =
dumpn("*** _sendHeaders()");
NS_ASSERT(this._headers);
+ NS_ASSERT(!this._powerSeized);
// request-line
var statusLine = "HTTP/" + this.httpVersion + " " +
@@ -3709,8 +3787,13 @@ Response.prototype =
// 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;
@@ -3732,15 +3815,19 @@ Response.prototype =
onStopRequest: function(request, cx, statusCode)
{
- dumpn("*** onStopRequest [status=" + statusCode.toString(16) + "]");
+ dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
- if (!Components.isSuccessCode(statusCode))
+ if (statusCode === Cr.NS_BINDING_ABORTED)
{
- dumpn("*** WARNING: non-success statusCode in onStopRequest: " +
- statusCode);
+ dumpn("*** terminating copy observer without ending the response");
}
+ else
+ {
+ if (!Components.isSuccessCode(statusCode))
+ dumpn("*** WARNING: non-success statusCode in onStopRequest");
- response.end();
+ response.end();
+ }
},
QueryInterface: function(aIID)
@@ -3784,8 +3871,9 @@ function notImplemented()
* @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
@@ -3847,7 +3935,10 @@ WriteThroughCopier.prototype =
dumpn("*** cancel(" + status.toString(16) + ")");
if (this._completed)
+ {
+ dumpn("*** ignoring cancel on already-canceled copier...");
return;
+ }
this._completed = true;
this.status = status;
@@ -3890,13 +3981,16 @@ WriteThroughCopier.prototype =
* 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();
@@ -3931,6 +4025,19 @@ WriteThroughCopier.prototype =
{
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;
}
};
Oops, something went wrong.

0 comments on commit fa4f06d

Please sign in to comment.