Bug 695636 - Update close steps to adhere to WS spec. r=smaug

This commit is contained in:
Jason Duell 2012-06-04 16:38:32 -07:00
parent 44fadb2cef
commit 7bd3d2eeae
3 changed files with 209 additions and 165 deletions

View File

@ -74,6 +74,27 @@ using namespace mozilla;
} \
PR_END_MACRO
class CallDispatchConnectionCloseEvents: public nsRunnable
{
public:
CallDispatchConnectionCloseEvents(nsWebSocket *aWebSocket)
: mWebSocket(aWebSocket)
{}
NS_IMETHOD Run()
{
mWebSocket->DispatchConnectionCloseEvents();
return NS_OK;
}
private:
nsRefPtr<nsWebSocket> mWebSocket;
};
//-----------------------------------------------------------------------------
// nsWebSocket
//-----------------------------------------------------------------------------
nsresult
nsWebSocket::PrintErrorOnConsole(const char *aBundleURI,
const PRUnichar *aError,
@ -124,37 +145,43 @@ nsWebSocket::PrintErrorOnConsole(const char *aBundleURI,
return NS_OK;
}
// when this is called the browser side wants no more part of it
nsresult
nsWebSocket::CloseConnection(PRUint16 aReasonCode,
const nsACString& aReasonString)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (mDisconnected)
return NS_OK;
// Disconnect() can release this object, so we keep a
// reference until the end of the method
nsRefPtr<nsWebSocket> kungfuDeathGrip = this;
if (mReadyState == nsIWebSocket::CONNECTING) {
SetReadyState(nsIWebSocket::CLOSED);
if (mChannel) {
mChannel->Close(aReasonCode, aReasonString);
}
Disconnect();
if (mReadyState == nsIWebSocket::CLOSING ||
mReadyState == nsIWebSocket::CLOSED) {
return NS_OK;
}
SetReadyState(nsIWebSocket::CLOSING);
if (mDisconnected) {
SetReadyState(nsIWebSocket::CLOSED);
Disconnect();
return NS_OK;
// The common case...
if (mChannel) {
mReadyState = nsIWebSocket::CLOSING;
return mChannel->Close(aReasonCode, aReasonString);
}
return mChannel->Close(aReasonCode, aReasonString);
// No channel, but not disconnected: canceled or failed early
//
MOZ_ASSERT(mReadyState == nsIWebSocket::CONNECTING,
"Should only get here for early websocket cancel/error");
// Server won't be sending us a close code, so use what's passed in here.
mCloseEventCode = aReasonCode;
CopyUTF8toUTF16(aReasonString, mCloseEventReason);
mReadyState = nsIWebSocket::CLOSING;
// Can be called from Cancel() or Init() codepaths, so need to dispatch
// onerror/onclose asynchronously
ScheduleConnectionCloseEvents(
nsnull,
(aReasonCode == nsIWebSocketChannel::CLOSE_NORMAL ||
aReasonCode == nsIWebSocketChannel::CLOSE_GOING_AWAY) ?
NS_OK : NS_ERROR_FAILURE,
false);
return NS_OK;
}
nsresult
@ -191,14 +218,11 @@ nsWebSocket::FailConnection(PRUint16 aReasonCode,
const nsACString& aReasonString)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
ConsoleError();
mFailed = true;
CloseConnection(aReasonCode, aReasonString);
nsresult rv = CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("error"));
if (NS_FAILED(rv))
NS_WARNING("Failed to dispatch the error event");
return NS_OK;
}
@ -240,7 +264,11 @@ nsresult
nsWebSocket::DoOnMessageAvailable(const nsACString & aMsg, bool isBinary)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
NS_ABORT_IF_FALSE(!mDisconnected, "Received message after disconnecting");
if (mReadyState == nsIWebSocket::CLOSED) {
NS_ERROR("Received message after CLOSED");
return NS_ERROR_UNEXPECTED;
}
if (mReadyState == nsIWebSocket::OPEN) {
// Dispatch New Message
@ -251,8 +279,8 @@ nsWebSocket::DoOnMessageAvailable(const nsACString & aMsg, bool isBinary)
} else {
// CLOSING should be the only other state where it's possible to get msgs
// from channel: Spec says to drop them.
NS_ASSERTION(mReadyState == nsIWebSocket::CLOSING,
"Received message while CONNECTING or CLOSED");
MOZ_ASSERT(mReadyState == nsIWebSocket::CLOSING,
"Received message while CONNECTING or CLOSED");
}
return NS_OK;
@ -275,13 +303,20 @@ NS_IMETHODIMP
nsWebSocket::OnStart(nsISupports *aContext)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (mDisconnected)
// This is the only function that sets OPEN, and should be called only once
MOZ_ASSERT(mReadyState != nsIWebSocket::OPEN,
"readyState already OPEN! OnStart called twice?");
// Nothing to do if we've already closed/closing
if (mReadyState != nsIWebSocket::CONNECTING) {
return NS_OK;
}
// Attempt to kill "ghost" websocket: but usually too early for check to fail
nsresult rv = CheckInnerWindowCorrectness();
if (NS_FAILED(rv)) {
FailConnectionQuietly();
CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
return rv;
}
@ -292,35 +327,63 @@ nsWebSocket::OnStart(nsISupports *aContext)
mChannel->GetExtensions(mEstablishedExtensions);
UpdateURI();
SetReadyState(nsIWebSocket::OPEN);
mReadyState = nsIWebSocket::OPEN;
// Call 'onopen'
rv = CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("open"));
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch the open event");
}
UpdateMustKeepAlive();
return NS_OK;
}
NS_IMETHODIMP
nsWebSocket::OnStop(nsISupports *aContext, nsresult aStatusCode)
{
// We can be CONNECTING here if connection failed.
// We can be OPEN if we have encountered a fatal protocol error
// We can be CLOSING if close() was called and/or server initiated close.
MOZ_ASSERT(mReadyState != nsIWebSocket::CLOSED,
"Shouldn't already be CLOSED when OnStop called");
// called by network stack, not JS, so can dispatch JS events synchronously
return ScheduleConnectionCloseEvents(aContext, aStatusCode, true);
}
nsresult
nsWebSocket::ScheduleConnectionCloseEvents(nsISupports *aContext,
nsresult aStatusCode,
bool sync)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (mDisconnected)
return NS_OK;
mCloseEventWasClean = NS_SUCCEEDED(aStatusCode);
// no-op if some other code has already initiated close event
if (!mOnCloseScheduled) {
mCloseEventWasClean = NS_SUCCEEDED(aStatusCode);
if (aStatusCode == NS_BASE_STREAM_CLOSED &&
mReadyState >= nsIWebSocket::CLOSING) {
// don't generate an error event just because of an unclean close
aStatusCode = NS_OK;
if (aStatusCode == NS_BASE_STREAM_CLOSED) {
// don't generate an error event just because of an unclean close
aStatusCode = NS_OK;
}
if (NS_FAILED(aStatusCode)) {
ConsoleError();
mFailed = true;
}
mOnCloseScheduled = true;
if (sync) {
DispatchConnectionCloseEvents();
} else {
NS_DispatchToMainThread(new CallDispatchConnectionCloseEvents(this),
NS_DISPATCH_NORMAL);
}
}
if (NS_FAILED(aStatusCode)) {
ConsoleError();
nsresult rv = CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("error"));
if (NS_FAILED(rv))
NS_WARNING("Failed to dispatch the error event");
}
SetReadyState(nsIWebSocket::CLOSED);
Disconnect();
return NS_OK;
}
@ -342,19 +405,17 @@ nsWebSocket::OnServerClose(nsISupports *aContext, PRUint16 aCode,
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
NS_ABORT_IF_FALSE(mReadyState != nsIWebSocket::CONNECTING,
"Received server close before connected?");
if (mReadyState == nsIWebSocket::CLOSED) {
NS_WARNING("Received server close after already closed!");
return NS_ERROR_UNEXPECTED;
}
MOZ_ASSERT(mReadyState != nsIWebSocket::CONNECTING,
"Received server close before connected?");
MOZ_ASSERT(mReadyState != nsIWebSocket::CLOSED,
"Received server close after already closed!");
// store code/string for onclose DOM event
mCloseEventCode = aCode;
CopyUTF8toUTF16(aReason, mCloseEventReason);
if (mReadyState == nsIWebSocket::OPEN) {
// Server initiating close.
// RFC 6455, 5.5.1: "When sending a Close frame in response, the endpoint
// typically echos the status code it received".
// But never send certain codes, per section 7.4.1
@ -364,9 +425,8 @@ nsWebSocket::OnServerClose(nsISupports *aContext, PRUint16 aCode,
CloseConnection(aCode, aReason);
}
} else {
// Nothing else to do: OnStop does the rest of the work.
NS_ASSERTION (mReadyState == nsIWebSocket::CLOSING, "unknown state");
NS_ASSERTION(!mDisconnected, "should not be disconnected during CLOSING");
// We initiated close, and server has replied: OnStop does rest of the work.
MOZ_ASSERT(mReadyState == nsIWebSocket::CLOSING, "unknown state");
}
return NS_OK;
@ -381,7 +441,7 @@ nsWebSocket::GetInterface(const nsIID &aIID, void **aResult)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (mDisconnected)
if (mReadyState == nsIWebSocket::CLOSED)
return NS_ERROR_FAILURE;
if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) ||
@ -411,7 +471,8 @@ nsWebSocket::GetInterface(const nsIID &aIID, void **aResult)
nsWebSocket::nsWebSocket() : mKeepingAlive(false),
mCheckMustKeepAlive(true),
mTriggeredCloseEvent(false),
mOnCloseScheduled(false),
mFailed(false),
mDisconnected(false),
mCloseEventWasClean(false),
mCloseEventCode(nsIWebSocketChannel::CLOSE_ABNORMAL),
@ -429,7 +490,10 @@ nsWebSocket::~nsWebSocket()
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
Disconnect();
// If we threw during Init we never called disconnect
if (!mDisconnected) {
Disconnect();
}
nsLayoutStatics::Release();
}
@ -511,7 +575,7 @@ nsWebSocket::DisconnectFromOwner()
NS_DISCONNECT_EVENT_HANDLER(Message)
NS_DISCONNECT_EVENT_HANDLER(Close)
NS_DISCONNECT_EVENT_HANDLER(Error)
FailConnectionQuietly();
CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
DontKeepAliveAnyMore();
}
@ -705,31 +769,30 @@ nsWebSocket::EstablishConnection()
return NS_OK;
}
class nsWSCloseEvent : public nsRunnable
void
nsWebSocket::DispatchConnectionCloseEvents()
{
public:
nsWSCloseEvent(nsWebSocket *aWebSocket, bool aWasClean,
PRUint16 aCode, const nsString &aReason)
: mWebSocket(aWebSocket),
mWasClean(aWasClean),
mCode(aCode),
mReason(aReason)
{}
nsresult rv;
NS_IMETHOD Run()
{
nsresult rv = mWebSocket->CreateAndDispatchCloseEvent(mWasClean,
mCode, mReason);
mWebSocket->UpdateMustKeepAlive();
return rv;
mReadyState = nsIWebSocket::CLOSED;
// Call 'onerror' if needed
if (mFailed) {
nsresult rv = CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("error"));
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch the error event");
}
}
private:
nsRefPtr<nsWebSocket> mWebSocket;
bool mWasClean;
PRUint16 mCode;
nsString mReason;
};
rv = CreateAndDispatchCloseEvent(mCloseEventWasClean, mCloseEventCode,
mCloseEventReason);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch the close event");
}
UpdateMustKeepAlive();
Disconnect();
}
nsresult
nsWebSocket::CreateAndDispatchSimpleEvent(const nsString& aName)
@ -855,8 +918,6 @@ nsWebSocket::CreateAndDispatchCloseEvent(bool aWasClean,
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
nsresult rv;
mTriggeredCloseEvent = true;
rv = CheckInnerWindowCorrectness();
if (NS_FAILED(rv)) {
return NS_OK;
@ -888,58 +949,6 @@ nsWebSocket::PrefEnabled()
return Preferences::GetBool("network.websocket.enabled", true);
}
void
nsWebSocket::SetReadyState(PRUint16 aNewReadyState)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
nsresult rv;
if (mReadyState == aNewReadyState) {
return;
}
NS_ABORT_IF_FALSE((aNewReadyState == nsIWebSocket::OPEN) ||
(aNewReadyState == nsIWebSocket::CLOSING) ||
(aNewReadyState == nsIWebSocket::CLOSED),
"unexpected readyState");
if (aNewReadyState == nsIWebSocket::OPEN) {
NS_ABORT_IF_FALSE(mReadyState == nsIWebSocket::CONNECTING,
"unexpected readyState transition");
mReadyState = aNewReadyState;
rv = CreateAndDispatchSimpleEvent(NS_LITERAL_STRING("open"));
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch the open event");
}
UpdateMustKeepAlive();
return;
}
if (aNewReadyState == nsIWebSocket::CLOSING) {
NS_ABORT_IF_FALSE((mReadyState == nsIWebSocket::CONNECTING) ||
(mReadyState == nsIWebSocket::OPEN),
"unexpected readyState transition");
mReadyState = aNewReadyState;
return;
}
if (aNewReadyState == nsIWebSocket::CLOSED) {
mReadyState = aNewReadyState;
// The close event must be dispatched asynchronously.
rv = NS_DispatchToMainThread(new nsWSCloseEvent(this, mCloseEventWasClean,
mCloseEventCode,
mCloseEventReason),
NS_DISPATCH_NORMAL);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch the close event");
mTriggeredCloseEvent = true;
UpdateMustKeepAlive();
}
}
}
nsresult
nsWebSocket::ParseURL(const nsString& aURL)
{
@ -1066,9 +1075,7 @@ nsWebSocket::UpdateMustKeepAlive()
case nsIWebSocket::CLOSED:
{
shouldKeepAlive =
(!mTriggeredCloseEvent &&
mListenerManager->HasListenersFor(NS_LITERAL_STRING("close")));
shouldKeepAlive = false;
}
}
}
@ -1093,14 +1100,6 @@ nsWebSocket::DontKeepAliveAnyMore()
mCheckMustKeepAlive = false;
}
void
nsWebSocket::FailConnectionQuietly()
{
// Fail without console error or JS onerror message: onmessage/onclose will
// also be blocked so long as CheckInnerWindowCorrectness is failing.
CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
}
nsresult
nsWebSocket::UpdateURI()
{
@ -1292,8 +1291,8 @@ nsWebSocket::Send(nsIVariant *aData, JSContext *aCx)
return NS_OK;
}
NS_ASSERTION(mReadyState == nsIWebSocket::OPEN,
"Unknown state in nsWebSocket::Send");
MOZ_ASSERT(mReadyState == nsIWebSocket::OPEN,
"Unknown state in nsWebSocket::Send");
if (msgStream) {
rv = mChannel->SendBinaryStream(msgStream, msgLen);
@ -1471,10 +1470,6 @@ nsWebSocket::Close(PRUint16 code, const nsAString & reason, PRUint8 argc)
}
if (mReadyState == nsIWebSocket::CONNECTING) {
// FailConnection() can release the object, so we keep a reference
// before calling it
nsRefPtr<nsWebSocket> kungfuDeathGrip = this;
FailConnection(closeCode, closeReason);
return NS_OK;
}
@ -1618,7 +1613,7 @@ nsWebSocket::Observe(nsISupports* aSubject,
if ((strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC) == 0) ||
(strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0))
{
FailConnectionQuietly();
CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
}
return NS_OK;
@ -1639,7 +1634,7 @@ nsWebSocket::GetName(nsACString &aName)
NS_IMETHODIMP
nsWebSocket::IsPending(bool *aValue)
{
*aValue = !mDisconnected;
*aValue = (mReadyState != nsIWebSocket::CLOSED);
return NS_OK;
}
@ -1656,10 +1651,12 @@ nsWebSocket::Cancel(nsresult aStatus)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (mDisconnected)
if (mReadyState == CLOSING || mReadyState == CLOSED) {
return NS_OK;
}
ConsoleError();
return CloseConnection(nsIWebSocketChannel::CLOSE_GOING_AWAY);
}

View File

@ -35,7 +35,7 @@
#define NS_WEBSOCKET_CONTRACTID "@mozilla.org/websocket;1"
class nsWSCloseEvent;
class CallDispatchConnectionCloseEvents;
class nsAutoCloseWS;
class nsWebSocket: public nsDOMEventTargetHelper,
@ -47,7 +47,7 @@ class nsWebSocket: public nsDOMEventTargetHelper,
public nsSupportsWeakReference,
public nsIRequest
{
friend class nsWSCloseEvent;
friend class CallDispatchConnectionCloseEvents;
friend class nsAutoCloseWS;
public:
@ -87,7 +87,6 @@ protected:
// These methods when called can release the WebSocket object
nsresult FailConnection(PRUint16 reasonCode,
const nsACString& aReasonString = EmptyCString());
void FailConnectionQuietly();
nsresult CloseConnection(PRUint16 reasonCode,
const nsACString& aReasonString = EmptyCString());
nsresult Disconnect();
@ -107,6 +106,18 @@ protected:
JSContext *aCx);
nsresult DoOnMessageAvailable(const nsACString & aMsg, bool isBinary);
// ConnectionCloseEvents: 'error' event if needed, then 'close' event.
// - These must not be dispatched while we are still within an incoming call
// from JS (ex: close()). Set 'sync' to false in that case to dispatch in a
// separate new event.
nsresult ScheduleConnectionCloseEvents(nsISupports *aContext,
nsresult aStatusCode,
bool sync);
// 2nd half of ScheduleConnectionCloseEvents, sometimes run in its own event.
void DispatchConnectionCloseEvents();
// These methods actually do the dispatch for various events.
nsresult CreateAndDispatchSimpleEvent(const nsString& aName);
nsresult CreateAndDispatchMessageEvent(const nsACString& aData,
bool isBinary);
@ -115,8 +126,6 @@ protected:
nsresult CreateResponseBlob(const nsACString& aData, JSContext *aCx,
jsval &jsData);
void SetReadyState(PRUint16 aNewReadyState);
// if there are "strong event listeners" (see comment in nsWebSocket.cpp) or
// outgoing not sent messages then this method keeps the object alive
// when js doesn't have strong references to it.
@ -142,7 +151,8 @@ protected:
bool mKeepingAlive;
bool mCheckMustKeepAlive;
bool mTriggeredCloseEvent;
bool mOnCloseScheduled;
bool mFailed;
bool mDisconnected;
// Set attributes of DOM 'onclose' message

View File

@ -69,10 +69,11 @@
* 44. Test sending/receving binary ArrayBuffer
* 45. Test sending/receving binary Blob
* 46. Test that we don't dispatch incoming msgs once in CLOSING state
* 47. Make sure onerror/onclose aren't called during close()
*/
var first_test = 1;
var last_test = 46;
var last_test = 47;
// Set this to >1 if you want to run the suite multiple times to probe for
@ -282,6 +283,7 @@ function test3()
{
shouldCloseNotCleanly(e);
ok(hasError, "rcvd onerror event");
ok(e.code == 1006, "test-3 close code should be 1006 but is:" + e.code);
doTest(4);
};
}
@ -1435,6 +1437,41 @@ function test46()
}
}
function test47()
{
var hasError = false;
var ws = CreateTestWS("ws://another.websocket.server.that.probably.does.not.exist");
ws.onopen = shouldNotOpen;
ws.onerror = function (e)
{
ok(ws.readyState == 3, "test-47: readyState should be CLOSED(3) in onerror: got "
+ ws.readyState);
ok(!ws._withinClose, "onerror() called during close()!");
hasError = true;
}
ws.onclose = function(e)
{
shouldCloseNotCleanly(e);
ok(hasError, "test-47: should have called onerror before onclose");
ok(ws.readyState == 3, "test-47: readyState should be CLOSED(3) in onclose: got "
+ ws.readyState);
ok(!ws._withinClose, "onclose() called during close()!");
ok(e.code == 1006, "test-47 close code should be 1006 but is:" + e.code);
doTest(48);
};
// Call close before we're connected: throws error
// Make sure we call onerror/onclose asynchronously
ws._withinClose = 1;
ws.close(3333, "Closed before we were open: error");
ws._withinClose = 0;
ok(ws.readyState == 2, "test-47: readyState should be CLOSING(2) after close(): got "
+ ws.readyState);
}
var ranAllTests = false;
function maybeFinished()