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

--HG--
extra : rebase_source : 2d61cccef9b076b2e5dbe1074af99f572d60b700
This commit is contained in:
Jeff Walden 2009-05-28 14:54:42 -07:00
Родитель 612169811b
Коммит 7d6ee6ce79
4 изменённых файлов: 503 добавлений и 49 удалений

Просмотреть файл

@ -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,8 +3579,9 @@ 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
@ -3541,7 +3589,7 @@ Response.prototype =
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;
}
};

Просмотреть файл

@ -365,9 +365,17 @@ 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
@ -504,7 +512,8 @@ interface nsIHttpResponse : nsISupports
* 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,
@ -530,23 +539,29 @@ interface nsIHttpResponse : nsISupports
* 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
*/
@ -578,16 +593,43 @@ interface nsIHttpResponse : nsISupports
* @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();
};

Просмотреть файл

@ -299,12 +299,8 @@ 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",

Просмотреть файл

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