Skip to content
This repository has been archived by the owner on Oct 3, 2020. It is now read-only.

Commit

Permalink
Bug 484027 - Add a method providing minimally controlled arbitrary wr…
Browse files Browse the repository at this point in the history
…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
jswalden committed May 28, 2009
1 parent e666178 commit fa4f06d
Show file tree
Hide file tree
Showing 4 changed files with 503 additions and 49 deletions.
157 changes: 132 additions & 25 deletions netwerk/test/httpserver/httpd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
{
Expand All @@ -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();
}

Expand All @@ -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();

Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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)

/**
Expand Down Expand Up @@ -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;
},

/**
Expand All @@ -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?");

Expand All @@ -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
Expand All @@ -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();
}
},

/**
Expand Down Expand Up @@ -3616,6 +3693,7 @@ Response.prototype =
dumpn("*** _sendHeaders()");

NS_ASSERT(this._headers);
NS_ASSERT(!this._powerSeized);

// request-line
var statusLine = "HTTP/" + this.httpVersion + " " +
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
};

Expand Down
Loading

0 comments on commit fa4f06d

Please sign in to comment.