# HG changeset patch # Parent 4ee1e944c2c3d7f002d51737f459ce43ddc00dcf # User Patrick McManus bug 528222 Basic SPDY web transport support. r=honzab sr=biesi patch 0 diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -788,16 +788,22 @@ pref("network.http.qos", 0); // to wait before trying a different connection. 0 means do not use a second // connection. pref("network.http.connection-retry-timeout", 250); // Disable IPv6 for backup connections to workaround problems about broken // IPv6 connectivity. pref("network.http.fast-fallback-to-IPv4", false); +// Try and use SPDY when using SSL +pref("network.http.spdy.enabled", false); +pref("network.http.spdy.chunk-size", 4096); +pref("network.http.spdy.timeout", 180); +pref("network.http.spdy.coalesce-hostnames", true); + // default values for FTP // in a DSCP environment this should be 40 (0x28, or AF11), per RFC-4594, // Section 4.8 "High-Throughput Data Service Class", and 80 (0x50, or AF22) // per Section 4.7 "Low-Latency Data Service Class". pref("network.ftp.data.qos", 0); pref("network.ftp.control.qos", 0); // diff --git a/netwerk/base/src/nsSocketTransportService2.cpp b/netwerk/base/src/nsSocketTransportService2.cpp --- a/netwerk/base/src/nsSocketTransportService2.cpp +++ b/netwerk/base/src/nsSocketTransportService2.cpp @@ -59,16 +59,18 @@ using namespace mozilla; #if defined(PR_LOGGING) PRLogModuleInfo *gSocketTransportLog = nsnull; #endif nsSocketTransportService *gSocketTransportService = nsnull; PRThread *gSocketThread = nsnull; #define SEND_BUFFER_PREF "network.tcp.sendbuffer" +#define SPDY_PREF "network.http.spdy.enabled" + #define SOCKET_LIMIT_TARGET 550U #define SOCKET_LIMIT_MIN 50U PRUint32 nsSocketTransportService::gMaxCount; PRCallOnceType nsSocketTransportService::gMaxCountInitOnce; //----------------------------------------------------------------------------- // ctor/dtor (called on the main/UI thread by the service manager) @@ -81,16 +83,17 @@ nsSocketTransportService::nsSocketTransp , mInitialized(false) , mShuttingDown(false) , mActiveListSize(SOCKET_LIMIT_MIN) , mIdleListSize(SOCKET_LIMIT_MIN) , mActiveCount(0) , mIdleCount(0) , mSendBufferSize(0) , mProbedMaxCount(false) + , mSpdyEnabled(false) { #if defined(PR_LOGGING) gSocketTransportLog = PR_NewLogModule("nsSocketTransport"); #endif NS_ASSERTION(NS_IsMainThread(), "wrong thread"); PR_CallOnce(&gMaxCountInitOnce, DiscoverMaxCount); @@ -359,16 +362,25 @@ nsSocketTransportService::GrowIdleList() mIdleList = (SocketContext *) moz_xrealloc(mIdleList, sizeof(SocketContext) * mIdleListSize); return true; } PRIntervalTime nsSocketTransportService::PollTimeout() { + if (mSpdyEnabled) { + // This truly awful hack is because the sslthread force handshake + // implementation is broken and needed by spdy. + // Keeping this won't matter much for speed, but its a non-starter for + // battery consumption. Removing it also means there is no + // need for tracking the spdy.enabled pref in this module + return PR_MillisecondsToInterval(80); + } + if (mActiveCount == 0) return NS_SOCKET_POLL_TIMEOUT; // compute minimum time before any socket timeout expires. PRUint32 minR = PR_UINT16_MAX; for (PRUint32 i=0; i tmpPrefService = do_GetService(NS_PREFSERVICE_CONTRACTID); - if (tmpPrefService) + if (tmpPrefService) { tmpPrefService->AddObserver(SEND_BUFFER_PREF, this, false); + tmpPrefService->AddObserver(SPDY_PREF, this, false); + } + UpdatePrefs(); NS_TIME_FUNCTION_MARK("UpdatePrefs"); mInitialized = true; return NS_OK; } @@ -527,18 +542,20 @@ nsSocketTransportService::Shutdown() { MutexAutoLock lock(mLock); // Drop our reference to mThread and make sure that any concurrent // readers are excluded mThread = nsnull; } nsCOMPtr tmpPrefService = do_GetService(NS_PREFSERVICE_CONTRACTID); - if (tmpPrefService) + if (tmpPrefService) { tmpPrefService->RemoveObserver(SEND_BUFFER_PREF, this); + tmpPrefService->RemoveObserver(SPDY_PREF, this); + } mInitialized = false; mShuttingDown = false; return NS_OK; } NS_IMETHODIMP @@ -807,16 +824,20 @@ nsSocketTransportService::UpdatePrefs() mSendBufferSize = 0; nsCOMPtr tmpPrefService = do_GetService(NS_PREFSERVICE_CONTRACTID); if (tmpPrefService) { PRInt32 bufferSize; nsresult rv = tmpPrefService->GetIntPref(SEND_BUFFER_PREF, &bufferSize); if (NS_SUCCEEDED(rv) && bufferSize > 0) mSendBufferSize = bufferSize; + bool cVar = false; + rv = tmpPrefService->GetBoolPref(SPDY_PREF, &cVar); + if (NS_SUCCEEDED(rv)) + mSpdyEnabled = cVar; } return NS_OK; } NS_IMETHODIMP nsSocketTransportService::Observe(nsISupports *subject, const char *topic, diff --git a/netwerk/base/src/nsSocketTransportService2.h b/netwerk/base/src/nsSocketTransportService2.h --- a/netwerk/base/src/nsSocketTransportService2.h +++ b/netwerk/base/src/nsSocketTransportService2.h @@ -197,23 +197,25 @@ private: // duration in seconds. //------------------------------------------------------------------------- // pending socket queue - see NotifyWhenCanAttachSocket //------------------------------------------------------------------------- nsEventQueue mPendingSocketQ; // queue of nsIRunnable objects - // Preference Monitor for SendBufferSize + // Preference Monitor for SendBufferSize and spdy.enabled nsresult UpdatePrefs(); PRInt32 mSendBufferSize; // Socket thread only for dynamically adjusting max socket size #if defined(XP_WIN) void ProbeMaxCount(); #endif bool mProbedMaxCount; + + bool mSpdyEnabled; }; extern nsSocketTransportService *gSocketTransportService; extern PRThread *gSocketThread; #endif // !nsSocketTransportService_h__ diff --git a/netwerk/protocol/http/Makefile.in b/netwerk/protocol/http/Makefile.in --- a/netwerk/protocol/http/Makefile.in +++ b/netwerk/protocol/http/Makefile.in @@ -105,16 +105,18 @@ CPPSRCS = \ HttpBaseChannel.cpp \ nsHttpChannel.cpp \ nsHttpPipeline.cpp \ nsHttpActivityDistributor.cpp \ nsHttpChannelAuthProvider.cpp \ HttpChannelParent.cpp \ HttpChannelChild.cpp \ HttpChannelParentListener.cpp \ + SpdySession.cpp \ + SpdyStream.cpp \ $(NULL) LOCAL_INCLUDES = \ -I$(srcdir)/../../base/src \ -I$(topsrcdir)/xpcom/ds \ -I$(topsrcdir)/content/base/src \ -I$(topsrcdir)/content/events/src \ $(NULL) diff --git a/netwerk/protocol/http/SpdySession.cpp b/netwerk/protocol/http/SpdySession.cpp new file mode 100644 --- /dev/null +++ b/netwerk/protocol/http/SpdySession.cpp @@ -0,0 +1,1765 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* ***** 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 Mozilla. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick McManus + * + * Alternatively, the contents of this file may be used under the terms of + * either of 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 ***** */ + +#include "nsHttp.h" +#include "SpdySession.h" +#include "SpdyStream.h" +#include "nsHttpConnection.h" +#include "prnetdb.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Preferences.h" + +#ifdef DEBUG +// defined by the socket transport service while active +extern PRThread *gSocketThread; +#endif + +namespace mozilla { +namespace net { + +// SpdySession has multiple inheritance of things that implement +// nsISupports, so this magic is taken from nsHttpPipeline that +// implements some of the same abstract classes. +NS_IMPL_THREADSAFE_ADDREF(SpdySession) +NS_IMPL_THREADSAFE_RELEASE(SpdySession) +NS_INTERFACE_MAP_BEGIN(SpdySession) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsAHttpConnection) +NS_INTERFACE_MAP_END + +SpdySession::SpdySession(nsAHttpTransaction *aHttpTransaction, + nsISocketTransport *aSocketTransport, + PRInt32 firstPriority) + : mSocketTransport(aSocketTransport), + mSegmentReader(nsnull), + mSegmentWriter(nsnull), + mSendingChunkSize(kSendingChunkSize), + mNextStreamID(1), + mConcurrentHighWater(0), + mDownstreamState(BUFFERING_FRAME_HEADER), + mPartialFrame(nsnull), + mFrameBufferSize(kDefaultBufferSize), + mFrameBufferUsed(0), + mFrameDataLast(false), + mFrameDataStream(nsnull), + mNeedsCleanup(nsnull), + mDecompressBufferSize(kDefaultBufferSize), + mDecompressBufferUsed(0), + mShouldGoAway(false), + mClosed(false), + mCleanShutdown(false), + mGoAwayID(0), + mMaxConcurrent(kDefaultMaxConcurrent), + mConcurrent(0), + mServerPushedResources(0), + mOutputQueueSize(kDefaultQueueSize), + mOutputQueueUsed(0), + mOutputQueueSent(0) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + LOG(("SpdySession::SpdySession %p transaction 1 = %p", + this, aHttpTransaction)); + + mStreamIDHash.Init(); + mStreamTransactionHash.Init(); + mConnection = aHttpTransaction->Connection(); + mFrameBuffer = new char[mFrameBufferSize]; + mDecompressBuffer = new char[mDecompressBufferSize]; + mOutputQueueBuffer = new char[mOutputQueueSize]; + zlibInit(); + + mSendingChunkSize = + Preferences::GetInt("network.http.spdy.chunk-size", kSendingChunkSize); + AddStream(aHttpTransaction, firstPriority); +} + +PLDHashOperator +SpdySession::Shutdown(nsAHttpTransaction *key, + nsAutoPtr &stream, + void *closure) +{ + SpdySession *self = static_cast(closure); + + if (self->mCleanShutdown && + self->mGoAwayID < stream->StreamID()) + stream->Close(NS_ERROR_NET_RESET); // can be restarted + else + stream->Close(NS_ERROR_ABORT); + + return PL_DHASH_NEXT; +} + +SpdySession::~SpdySession() +{ + LOG(("SpdySession::~SpdySession %p", this)); + + inflateEnd(&mDownstreamZlib); + deflateEnd(&mUpstreamZlib); + + mStreamTransactionHash.Enumerate(Shutdown, this); + Telemetry::Accumulate(Telemetry::SPDY_PARALLEL_STREAMS, mConcurrentHighWater); + Telemetry::Accumulate(Telemetry::SPDY_TOTAL_STREAMS, (mNextStreamID - 1) / 2); + Telemetry::Accumulate(Telemetry::SPDY_SERVER_INITIATED_STREAMS, + mServerPushedResources); +} + +typedef nsresult (*Control_FX) (SpdySession *self); +static Control_FX sControlFunctions[] = +{ + nsnull, + SpdySession::HandleSynStream, + SpdySession::HandleSynReply, + SpdySession::HandleRstStream, + SpdySession::HandleSettings, + SpdySession::HandleNoop, + SpdySession::HandlePing, + SpdySession::HandleGoAway, + SpdySession::HandleHeaders, + SpdySession::HandleWindowUpdate +}; + +bool +SpdySession::RoomForMoreConcurrent() +{ + return (mConcurrent < mMaxConcurrent); +} + +bool +SpdySession::RoomForMoreStreams() +{ + if (mNextStreamID + mStreamTransactionHash.Count() * 2 > kMaxStreamID) + return false; + + return !mShouldGoAway; +} + +PRUint32 +SpdySession::RegisterStreamID(SpdyStream *stream) +{ + LOG(("SpdySession::RegisterStreamID session=%p stream=%p id=0x%X concurrent=%d", + this, stream, mNextStreamID, mConcurrent)); + + NS_ABORT_IF_FALSE(mNextStreamID < 0xfffffff0, + "should have stopped admitting streams"); + + PRUint32 result = mNextStreamID; + mNextStreamID += 2; + + // We've used up plenty of ID's on this session. Start + // moving to a new one before there is a crunch involving + // server push streams or concurrent non-registered submits + if (mNextStreamID >= kMaxStreamID) + mShouldGoAway = true; + + mStreamIDHash.Put(result, stream); + return result; +} + +bool +SpdySession::AddStream(nsAHttpTransaction *aHttpTransaction, + PRInt32 aPriority) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(!mStreamTransactionHash.Get(aHttpTransaction), + "AddStream duplicate transaction pointer"); + + aHttpTransaction->SetConnection(this); + SpdyStream *stream = new SpdyStream(aHttpTransaction, + this, + mSocketTransport, + mSendingChunkSize, + &mUpstreamZlib, + aPriority); + + + LOG(("SpdySession::AddStream %p stream %p NextID=0x%X (tentative)", + this, stream, mNextStreamID)); + + mStreamTransactionHash.Put(aHttpTransaction, stream); + + if (RoomForMoreConcurrent()) { + LOG(("SpdySession::AddStream %p stream %p activated immediately.", + this, stream)); + ActivateStream(stream); + } + else { + LOG(("SpdySession::AddStream %p stream %p queued.", + this, stream)); + mQueuedStreams.Push(stream); + } + + return true; +} + +void +SpdySession::ActivateStream(SpdyStream *stream) +{ + mConcurrent++; + if (mConcurrent > mConcurrentHighWater) + mConcurrentHighWater = mConcurrent; + LOG(("SpdySession::AddStream %p activating stream %p Currently %d" + "streams in session, high water mark is %d", + this, stream, mConcurrent, mConcurrentHighWater)); + + mReadyForWrite.Push(stream); + SetWriteCallbacks(stream->Transaction()); + + // Kick off the SYN transmit without waiting for the poll loop + PRUint32 countRead; + ReadSegments(nsnull, kDefaultBufferSize, &countRead); +} + +void +SpdySession::ProcessPending() +{ + while (RoomForMoreConcurrent()) { + SpdyStream *stream = static_cast(mQueuedStreams.PopFront()); + if (!stream) + return; + LOG(("SpdySession::ProcessPending %p stream %p activated from queue.", + this, stream)); + ActivateStream(stream); + } +} + +void +SpdySession::SetWriteCallbacks(nsAHttpTransaction *aTrans) +{ + if (mConnection && (WriteQueueSize() || mOutputQueueUsed)) + mConnection->ResumeSend(aTrans); +} + +void +SpdySession::FlushOutputQueue() +{ + if (!mSegmentReader || !mOutputQueueUsed) + return; + + nsresult rv; + PRUint32 countRead; + PRUint32 avail = mOutputQueueUsed - mOutputQueueSent; + + rv = mSegmentReader-> + OnReadSegment(mOutputQueueBuffer.get() + mOutputQueueSent, avail, + &countRead); + LOG(("SpdySession::FlushOutputQueue %p sz=%d rv=%x actual=%d", + this, avail, rv, countRead)); + + // Dont worry about errors on write, we will pick this up as a read error too + if (NS_FAILED(rv)) + return; + + if (countRead == avail) { + mOutputQueueUsed = 0; + mOutputQueueSent = 0; + return; + } + + mOutputQueueSent += countRead; + if (mOutputQueueSize - mOutputQueueUsed < kQueueTailRoom) { + // The output queue is filling up and we just sent some data out, so + // this is a good time to rearrange the output queue. + + mOutputQueueUsed -= mOutputQueueSent; + memmove(mOutputQueueBuffer.get(), + mOutputQueueBuffer.get() + mOutputQueueSent, + mOutputQueueUsed); + mOutputQueueSent = 0; + } +} + +void +SpdySession::DontReuse() +{ + mShouldGoAway = true; + if(!mStreamTransactionHash.Count()) + Close(NS_OK); +} + +PRUint32 +SpdySession::WriteQueueSize() +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + PRUint32 count = mUrgentForWrite.GetSize() + mReadyForWrite.GetSize(); + + if (mPartialFrame) + ++count; + return count; +} + +void +SpdySession::ChangeDownstreamState(enum stateType newState) +{ + LOG(("SpdyStream::ChangeDownstreamState() %p from %X to %X", + this, mDownstreamState, newState)); + mDownstreamState = newState; + + if (mDownstreamState == BUFFERING_FRAME_HEADER) { + if (mFrameDataLast && mFrameDataStream) { + mFrameDataLast = 0; + if (!mFrameDataStream->RecvdFin()) { + mFrameDataStream->SetRecvdFin(true); + --mConcurrent; + ProcessPending(); + } + } + mFrameBufferUsed = 0; + mFrameDataStream = nsnull; + } + + return; +} + +void +SpdySession::EnsureBuffer(nsAutoArrayPtr &buf, + PRUint32 newSize, + PRUint32 preserve, + PRUint32 &objSize) +{ + if (objSize >= newSize) + return; + + objSize = newSize; + nsAutoArrayPtr tmp(new char[objSize]); + memcpy (tmp, buf, preserve); + buf = tmp; +} + +void +SpdySession::zlibInit() +{ + mDownstreamZlib.zalloc = SpdyStream::zlib_allocator; + mDownstreamZlib.zfree = SpdyStream::zlib_destructor; + mDownstreamZlib.opaque = Z_NULL; + + inflateInit(&mDownstreamZlib); + + mUpstreamZlib.zalloc = SpdyStream::zlib_allocator; + mUpstreamZlib.zfree = SpdyStream::zlib_destructor; + mUpstreamZlib.opaque = Z_NULL; + + deflateInit(&mUpstreamZlib, Z_DEFAULT_COMPRESSION); + deflateSetDictionary(&mUpstreamZlib, + reinterpret_cast + (SpdyStream::kDictionary), + strlen(SpdyStream::kDictionary) + 1); + +} + +nsresult +SpdySession::DownstreamUncompress(char *blockStart, PRUint32 blockLen) +{ + mDecompressBufferUsed = 0; + + mDownstreamZlib.avail_in = blockLen; + mDownstreamZlib.next_in = reinterpret_cast(blockStart); + + do { + mDownstreamZlib.next_out = + reinterpret_cast(mDecompressBuffer.get()) + + mDecompressBufferUsed; + mDownstreamZlib.avail_out = mDecompressBufferSize - mDecompressBufferUsed; + int zlib_rv = inflate(&mDownstreamZlib, Z_NO_FLUSH); + + if (zlib_rv == Z_NEED_DICT) + inflateSetDictionary(&mDownstreamZlib, + reinterpret_cast + (SpdyStream::kDictionary), + strlen(SpdyStream::kDictionary) + 1); + + if (zlib_rv == Z_DATA_ERROR || zlib_rv == Z_MEM_ERROR) + return NS_ERROR_FAILURE; + + mDecompressBufferUsed += mDecompressBufferSize -mDecompressBufferUsed - + mDownstreamZlib.avail_out; + + if (mDownstreamZlib.avail_out) { + LOG(("SpdySession::DownstreamUncompress %p Large Headers - so far %d", + this, mDecompressBufferSize)); + EnsureBuffer(mDecompressBuffer, + mDecompressBufferSize + 4096, + mDecompressBufferUsed, + mDecompressBufferSize); + } + } + while (mDownstreamZlib.avail_in); + return NS_OK; +} + +nsresult +SpdySession::FindHeader(nsCString name, + nsDependentCSubstring &value) +{ + const unsigned char *nvpair = reinterpret_cast + (mDecompressBuffer.get()) + 2; + const unsigned char *lastHeaderByte = reinterpret_cast + (mDecompressBuffer.get()) + mDecompressBufferUsed; + if (lastHeaderByte < nvpair) + return NS_ERROR_ILLEGAL_VALUE; + PRUint16 numPairs = + PR_ntohs(reinterpret_cast(mDecompressBuffer.get())[0]); + for (PRUint16 index = 0; index < numPairs; ++index) { + if (lastHeaderByte < nvpair + 2) + return NS_ERROR_ILLEGAL_VALUE; + PRUint32 nameLen = (nvpair[0] << 8) + nvpair[1]; + if (lastHeaderByte < nvpair + 2 + nameLen) + return NS_ERROR_ILLEGAL_VALUE; + nsDependentCSubstring nameString = + Substring (reinterpret_cast(nvpair) + 2, + reinterpret_cast(nvpair) + 2 + nameLen); + if (lastHeaderByte < nvpair + 4 + nameLen) + return NS_ERROR_ILLEGAL_VALUE; + PRUint16 valueLen = (nvpair[2 + nameLen] << 8) + nvpair[3 + nameLen]; + if (lastHeaderByte < nvpair + 4 + nameLen + valueLen) + return NS_ERROR_ILLEGAL_VALUE; + if (nameString.Equals(name)) { + value.Assign(((char *)nvpair) + 4 + nameLen, valueLen); + return NS_OK; + } + nvpair += 4 + nameLen + valueLen; + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult +SpdySession::ConvertHeaders(nsDependentCSubstring &status, + nsDependentCSubstring &version) +{ + + mFlatHTTPResponseHeaders.Truncate(); + mFlatHTTPResponseHeadersOut = 0; + mFlatHTTPResponseHeaders.SetCapacity(mDecompressBufferUsed + 64); + + // Connection, Keep-Alive and chunked transfer encodings are to be + // removed. + + // Content-Length is 'advisory'.. we will not strip it because it can + // create UI feedback. + + mFlatHTTPResponseHeaders.Append(version); + mFlatHTTPResponseHeaders.Append(NS_LITERAL_CSTRING(" ")); + mFlatHTTPResponseHeaders.Append(status); + mFlatHTTPResponseHeaders.Append(NS_LITERAL_CSTRING("\r\n")); + + const unsigned char *nvpair = reinterpret_cast + (mDecompressBuffer.get()) + 2; + const unsigned char *lastHeaderByte = reinterpret_cast + (mDecompressBuffer.get()) + mDecompressBufferUsed; + + if (lastHeaderByte < nvpair) + return NS_ERROR_ILLEGAL_VALUE; + + PRUint16 numPairs = + PR_ntohs(reinterpret_cast(mDecompressBuffer.get())[0]); + + for (PRUint16 index = 0; index < numPairs; ++index) { + if (lastHeaderByte < nvpair + 2) + return NS_ERROR_ILLEGAL_VALUE; + + PRUint32 nameLen = (nvpair[0] << 8) + nvpair[1]; + if (lastHeaderByte < nvpair + 2 + nameLen) + return NS_ERROR_ILLEGAL_VALUE; + + nsDependentCSubstring nameString = + Substring (reinterpret_cast(nvpair) + 2, + reinterpret_cast(nvpair) + 2 + nameLen); + + // a null in the name string is particularly wrong because it will + // break the fix-up-nulls-in-value-string algorithm. + if (nameString.FindChar(0) != -1) + return NS_ERROR_ILLEGAL_VALUE; + + if (lastHeaderByte < nvpair + 4 + nameLen) + return NS_ERROR_ILLEGAL_VALUE; + PRUint16 valueLen = (nvpair[2 + nameLen] << 8) + nvpair[3 + nameLen]; + if (lastHeaderByte < nvpair + 4 + nameLen + valueLen) + return NS_ERROR_ILLEGAL_VALUE; + + if (!nameString.Equals(NS_LITERAL_CSTRING("version")) && + !nameString.Equals(NS_LITERAL_CSTRING("status")) && + !nameString.Equals(NS_LITERAL_CSTRING("connection")) && + !nameString.Equals(NS_LITERAL_CSTRING("transfer-encoding")) && + !nameString.Equals(NS_LITERAL_CSTRING("keep-alive"))) { + nsDependentCSubstring valueString = + Substring (reinterpret_cast(nvpair) + 4 + nameLen, + reinterpret_cast(nvpair) + 4 + nameLen + + valueLen); + + mFlatHTTPResponseHeaders.Append(nameString); + mFlatHTTPResponseHeaders.Append(NS_LITERAL_CSTRING(": ")); + + PRInt32 valueIndex; + // NULLs are really "\r\nhdr: " + while ((valueIndex = valueString.FindChar(0)) != -1) { + nsCString replacement = NS_LITERAL_CSTRING("\r\n"); + replacement.Append(nameString); + replacement.Append(NS_LITERAL_CSTRING(": ")); + valueString.Replace(valueIndex, 1, replacement); + } + + mFlatHTTPResponseHeaders.Append(valueString); + mFlatHTTPResponseHeaders.Append(NS_LITERAL_CSTRING("\r\n")); + } + nvpair += 4 + nameLen + valueLen; + } + + mFlatHTTPResponseHeaders.Append( + NS_LITERAL_CSTRING("X-Firefox-Spdy: 1\r\n\r\n")); + LOG (("decoded response headers are:\n%s", + mFlatHTTPResponseHeaders.get())); + + return NS_OK; +} + +void +SpdySession::GeneratePing(PRUint32 aID) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdySession::GeneratePing %p 0x%X\n", this, aID)); + + EnsureBuffer(mOutputQueueBuffer, mOutputQueueUsed + 12, + mOutputQueueUsed, mOutputQueueSize); + char *packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + mOutputQueueUsed += 12; + + packet[0] = kFlag_Control; + packet[1] = 2; /* version 2 */ + packet[2] = 0; + packet[3] = CONTROL_TYPE_PING; + packet[4] = 0; /* flags */ + packet[5] = 0; + packet[6] = 0; + packet[7] = 4; /* length */ + + aID = PR_htonl(aID); + memcpy (packet + 8, &aID, 4); + + FlushOutputQueue(); +} + +void +SpdySession::GenerateRstStream(PRUint32 aStatusCode, PRUint32 aID) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdySession::GenerateRst %p 0x%X %d\n", this, aID, aStatusCode)); + + EnsureBuffer(mOutputQueueBuffer, mOutputQueueUsed + 16, + mOutputQueueUsed, mOutputQueueSize); + char *packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + mOutputQueueUsed += 16; + + packet[0] = kFlag_Control; + packet[1] = 2; /* version 2 */ + packet[2] = 0; + packet[3] = CONTROL_TYPE_RST_STREAM; + packet[4] = 0; /* flags */ + packet[5] = 0; + packet[6] = 0; + packet[7] = 8; /* length */ + + aID = PR_htonl(aID); + memcpy (packet + 8, &aID, 4); + aStatusCode = PR_htonl(aStatusCode); + memcpy (packet + 12, &aStatusCode, 4); + + FlushOutputQueue(); +} + +void +SpdySession::GenerateGoAway() +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdySession::GenerateGoAway %p\n", this)); + + EnsureBuffer(mOutputQueueBuffer, mOutputQueueUsed + 12, + mOutputQueueUsed, mOutputQueueSize); + char *packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + mOutputQueueUsed += 12; + + memset (packet, 0, 12); + packet[0] = kFlag_Control; + packet[1] = 2; /* version 2 */ + packet[3] = CONTROL_TYPE_GOAWAY; + packet[7] = 4; /* data length */ + + // last-good-stream-id are bytes 8-11, when we accept server push this will + // need to be set non zero + + FlushOutputQueue(); +} + +void +SpdySession::CleanupStream(SpdyStream *aStream, nsresult aResult) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdySession::CleanupStream %p %p 0x%x %X\n", + this, aStream, aStream->StreamID(), aResult)); + + nsresult abortCode = NS_OK; + + if (!aStream->RecvdFin() && aStream->StreamID()) { + LOG(("Stream had not processed recv FIN, sending RST")); + GenerateRstStream(RST_CANCEL, aStream->StreamID()); + --mConcurrent; + ProcessPending(); + } + + // Check if partial frame writer + if (mPartialFrame == aStream) { + LOG(("Stream had active partial write frame - need to abort session")); + abortCode = aResult; + if (NS_SUCCEEDED(abortCode)) + abortCode = NS_ERROR_ABORT; + + mPartialFrame = nsnull; + } + + // Check if partial frame reader + if (aStream == mFrameDataStream) { + LOG(("Stream had active partial read frame on close")); + ChangeDownstreamState(DISCARD_DATA_FRAME); + mFrameDataStream = nsnull; + } + + // check the streams blocked on write, this is linear but the list + // should be pretty short. + PRUint32 size = mReadyForWrite.GetSize(); + for (PRUint32 count = 0; count < size; ++count) { + SpdyStream *stream = static_cast(mReadyForWrite.PopFront()); + if (stream != aStream) + mReadyForWrite.Push(stream); + } + + // Check the streams blocked on urgent (i.e. window update) writing. + // This should also be short. + size = mUrgentForWrite.GetSize(); + for (PRUint32 count = 0; count < size; ++count) { + SpdyStream *stream = static_cast(mUrgentForWrite.PopFront()); + if (stream != aStream) + mUrgentForWrite.Push(stream); + } + + // Remove the stream from the ID hash table. (this one isn't short, which is + // why it is hashed.) + mStreamIDHash.Remove(aStream->StreamID()); + + // Send the stream the close() indication + aStream->Close(aResult); + + // removing from the stream transaction hash will + // delete the SpdyStream and drop the reference to + // its transaction + mStreamTransactionHash.Remove(aStream->Transaction()); + + if (NS_FAILED(abortCode)) + Close(abortCode); + else if (mShouldGoAway && !mStreamTransactionHash.Count()) + Close(NS_OK); +} + +nsresult +SpdySession::HandleSynStream(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_SYN_STREAM, + "wrong control type"); + + if (self->mFrameDataSize < 12) { + LOG(("SpdySession::HandleSynStream %p SYN_STREAM too short data=%d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 streamID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + + LOG(("SpdySession::HandleSynStream %p recv SYN_STREAM (push) for ID 0x%X.", + self, streamID)); + + if (streamID & 0x01) { // test for odd stream ID + LOG(("SpdySession::HandleSynStream %p recvd SYN_STREAM id must be even.", + self)); + return NS_ERROR_ILLEGAL_VALUE; + } + + ++(self->mServerPushedResources); + + // Anytime we start using the high bit of stream ID (either client or server) + // begin to migrate to a new session. + if (streamID >= kMaxStreamID) + self->mShouldGoAway = true; + + // todo populate cache. For now, just reject server push p3 + self->GenerateRstStream(RST_REFUSED_STREAM, streamID); + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandleSynReply(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_SYN_REPLY, + "wrong control type"); + + if (self->mFrameDataSize < 8) { + LOG(("SpdySession::HandleSynReply %p SYN REPLY too short data=%d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 streamID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + self->mFrameDataStream = self->mStreamIDHash.Get(streamID); + if (!self->mFrameDataStream) { + LOG(("SpdySession::HandleSynReply %p lookup streamID in syn_reply " + "0x%X failed. NextStreamID = 0x%x", self, streamID, + self->mNextStreamID)); + if (streamID >= self->mNextStreamID) + self->GenerateRstStream(RST_INVALID_STREAM, streamID); + + // It is likely that this is a reply to a stream ID that has been canceled. + // For the most part we would like to ignore it, but the header needs to be + // be parsed to keep the compression context synchronized + self->DownstreamUncompress(self->mFrameBuffer + 14, + self->mFrameDataSize - 6); + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; + } + + if (!self->mFrameDataStream->SetFullyOpen()) { + // "If an endpoint receives multiple SYN_REPLY frames for the same active + // stream ID, it must drop the stream, and send a RST_STREAM for the + // stream with the error PROTOCOL_ERROR." + // + // In addition to that we abort the session - this is a serious protocol + // violation. + + self->GenerateRstStream(RST_PROTOCOL_ERROR, streamID); + return NS_ERROR_ILLEGAL_VALUE; + } + + self->mFrameDataLast = self->mFrameBuffer[4] & kFlag_Data_FIN; + + if (self->mFrameBuffer[4] & kFlag_Data_UNI) { + LOG(("SynReply had unidirectional flag set on it - nonsensical")); + return NS_ERROR_ILLEGAL_VALUE; + } + + LOG(("SpdySession::HandleSynReply %p SYN_REPLY for 0x%X fin=%d", + self, streamID, self->mFrameDataLast)); + + // The spdystream needs to see flattened http headers + // The Frame Buffer currently holds the complete SYN_REPLY + // frame. The interesting data is at offset 14, where the + // compressed name/value header block lives. + // We unpack that into the mDecompressBuffer - we can't do + // it streamed because the version and status information + // is not guaranteed to be first. This is then finally + // converted to HTTP format in mFlatHTTPResponseHeaders + + nsresult rv = self->DownstreamUncompress(self->mFrameBuffer + 14, + self->mFrameDataSize - 6); + if (NS_FAILED(rv)) + return rv; + + Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_SIZE, + self->mFrameDataSize - 6); + PRUint32 ratio = + (self->mFrameDataSize - 6) * 100 / self->mDecompressBufferUsed; + Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_RATIO, ratio); + + // status and version are required. + nsDependentCSubstring status, version; + rv = self->FindHeader(NS_LITERAL_CSTRING("status"), status); + if (NS_FAILED(rv)) + return rv; + + rv = self->FindHeader(NS_LITERAL_CSTRING("version"), version); + if (NS_FAILED(rv)) + return rv; + + rv = self->ConvertHeaders(status, version); + if (NS_FAILED(rv)) + return rv; + + self->ChangeDownstreamState(PROCESSING_CONTROL_SYN_REPLY); + return NS_OK; +} + +nsresult +SpdySession::HandleRstStream(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_RST_STREAM, + "wrong control type"); + + if (self->mFrameDataSize != 8) { + LOG(("SpdySession::HandleRstStream %p RST_STREAM wrong length data=%d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 streamID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + + self->mDownstreamRstReason = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[3]); + + LOG(("SpdySession::HandleRstStream %p RST_STREAM Reason Code %u ID %x", + self, self->mDownstreamRstReason, streamID)); + + if (self->mDownstreamRstReason == RST_INVALID_STREAM || + self->mDownstreamRstReason == RST_FLOW_CONTROL_ERROR) { + // basically just ignore this + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; + } + + self->mFrameDataStream = self->mStreamIDHash.Get(streamID); + if (!self->mFrameDataStream) { + LOG(("SpdySession::HandleRstStream %p lookup streamID for RST Frame " + "0x%X failed", self, streamID)); + return NS_ERROR_ILLEGAL_VALUE; + } + + self->ChangeDownstreamState(PROCESSING_CONTROL_RST_STREAM); + return NS_OK; +} + +nsresult +SpdySession::HandleSettings(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_SETTINGS, + "wrong control type"); + + if (self->mFrameDataSize < 4) { + LOG(("SpdySession::HandleSettings %p SETTINGS wrong length data=%d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 numEntries = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + + LOG(("SpdySession::HandleSettings %p SETTINGS Control Frame with %d entries", + self, numEntries)); + + for (PRUint32 index = 0; index < numEntries; ++index) { + // To clarify the v2 spec: + // Each entry is a 24 bits of a little endian id + // followed by 8 bits of flags + // followed by a 32 bit big endian value + + unsigned char *setting = reinterpret_cast + (self->mFrameBuffer.get()) + 12 + index * 8; + + PRUint32 id = (setting[2] << 16) + (setting[1] << 8) + setting[0]; + PRUint32 flags = setting[3]; + PRUint32 value = PR_ntohl(reinterpret_cast(setting)[1]); + + LOG(("Settings ID %d, Flags %X, Value %d",id, flags, value)); + + switch (id) + { + case SETTINGS_TYPE_UPLOAD_BW: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_UL_BW, value); + break; + + case SETTINGS_TYPE_DOWNLOAD_BW: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_DL_BW, value); + break; + + case SETTINGS_TYPE_RTT: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_RTT, value); + break; + + case SETTINGS_TYPE_MAX_CONCURRENT: + self->mMaxConcurrent = value; + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_MAX_STREAMS, value); + break; + + case SETTINGS_TYPE_CWND: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_CWND, value); + break; + + case SETTINGS_TYPE_DOWNLOAD_RETRANS_RATE: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_RETRANS, value); + break; + + case SETTINGS_TYPE_INITIAL_WINDOW: + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_IW, value >> 10); + break; + + default: + break; + } + + } + + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandleNoop(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_NOOP, + "wrong control type"); + + if (self->mFrameDataSize != 0) { + LOG(("SpdySession::HandleNoop %p NOP had data %d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + LOG(("SpdySession::HandleNoop %p NOP.", self)); + + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandlePing(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_PING, + "wrong control type"); + + if (self->mFrameDataSize != 4) { + LOG(("SpdySession::HandlePing %p PING had wrong amount of data %d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 pingID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + + LOG(("SpdySession::HandlePing %p PING ID 0x%X.", self, pingID)); + + if (pingID & 0x01) { + // We never expect to see an odd PING beacuse we never generate PING. + // The spec mandates ignoring this + LOG(("SpdySession::HandlePing %p PING ID from server was odd.", + self)); + } + else { + self->GeneratePing(pingID); + } + + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandleGoAway(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_GOAWAY, + "wrong control type"); + + if (self->mFrameDataSize != 4) { + LOG(("SpdySession::HandleGoAway %p GOAWAY had wrong amount of data %d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + self->mShouldGoAway = true; + self->mGoAwayID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + self->mCleanShutdown = true; + + LOG(("SpdySession::HandleGoAway %p GOAWAY Last-Good-ID 0x%X.", + self, self->mGoAwayID)); + self->ResumeRecv(self); + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandleHeaders(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_HEADERS, + "wrong control type"); + + if (self->mFrameDataSize < 10) { + LOG(("SpdySession::HandleHeaders %p HEADERS had wrong amount of data %d", + self, self->mFrameDataSize)); + return NS_ERROR_ILLEGAL_VALUE; + } + + PRUint32 streamID = + PR_ntohl(reinterpret_cast(self->mFrameBuffer.get())[2]); + + // this is actually not legal in the HTTP mapping of SPDY. All + // headers are in the syn or syn reply. Log and ignore it. + + LOG(("SpdySession::HandleHeaders %p HEADERS for Stream 0x%X. " + "They are ignored in the HTTP/SPDY mapping.", + self, streamID)); + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +nsresult +SpdySession::HandleWindowUpdate(SpdySession *self) +{ + NS_ABORT_IF_FALSE(self->mFrameControlType == CONTROL_TYPE_WINDOW_UPDATE, + "wrong control type"); + LOG(("SpdySession::HandleWindowUpdate %p WINDOW UPDATE was " + "received. WINDOW UPDATE is no longer defined in v2. Ignoring.", + self)); + + self->ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; +} + +// Used for the hashtable enumeration to propogate OnTransportStatus events +struct transportStatus +{ + nsITransport *transport; + nsresult status; + PRUint64 progress; +}; + +static PLDHashOperator +StreamTransportStatus(nsAHttpTransaction *key, + nsAutoPtr &stream, + void *closure) +{ + struct transportStatus *status = + static_cast(closure); + + stream->Transaction()->OnTransportStatus(status->transport, + status->status, + status->progress); + return PL_DHASH_NEXT; +} + + +//----------------------------------------------------------------------------- +// nsAHttpTransaction. It is expected that nsHttpConnection is the caller +// of these methods +//----------------------------------------------------------------------------- + +void +SpdySession::OnTransportStatus(nsITransport* aTransport, + nsresult aStatus, + PRUint64 aProgress) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + // nsHttpChannel synthesizes progress events in OnDataAvailable + if (aStatus == nsISocketTransport::STATUS_RECEIVING_FROM) + return; + + // STATUS_SENDING_TO is handled by SpdyStream + if (aStatus == nsISocketTransport::STATUS_SENDING_TO) + return; + + struct transportStatus status; + + status.transport = aTransport; + status.status = aStatus; + status.progress = aProgress; + + mStreamTransactionHash.Enumerate(StreamTransportStatus, &status); +} + +// ReadSegments() is used to write data to the network. Generally, HTTP +// request data is pulled from the approriate transaction and +// converted to SPDY data. Sometimes control data like window-update are +// generated instead. + +nsresult +SpdySession::ReadSegments(nsAHttpSegmentReader *reader, + PRUint32 count, + PRUint32 *countRead) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + nsresult rv; + *countRead = 0; + + // First priority goes to frames that were writing to the network but were + // blocked part way through. Then to frames that have no streams (e.g ping + // reply) and then third to streams marked urgent (generally they have + // window updates), and finally to streams generally + // ready to send data frames (http requests). + + LOG(("SpdySession::ReadSegments %p parial frame stream=%p", + this, mPartialFrame)); + + SpdyStream *stream = mPartialFrame; + mPartialFrame = nsnull; + + if (!stream) + stream = static_cast(mUrgentForWrite.PopFront()); + if (!stream) + stream = static_cast(mReadyForWrite.PopFront()); + if (!stream) { + LOG(("SpdySession %p could not identify a stream to write; suspending.", + this)); + FlushOutputQueue(); + SetWriteCallbacks(nsnull); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + LOG(("SpdySession %p will write from SpdyStream %p", this, stream)); + + NS_ABORT_IF_FALSE(!mSegmentReader || !reader || (mSegmentReader == reader), + "Inconsistent Write Function Callback"); + + if (reader) + mSegmentReader = reader; + rv = stream->ReadSegments(this, count, countRead); + + FlushOutputQueue(); + + if (stream->BlockedOnWrite()) { + + // We are writing a frame out, but it is blocked on the output stream. + // Make sure to service that stream next write because we can only + // multiplex between complete frames. + + LOG(("SpdySession::ReadSegments %p dealing with block on write", this)); + + NS_ABORT_IF_FALSE(!mPartialFrame, "partial frame should be empty"); + + mPartialFrame = stream; + SetWriteCallbacks(stream->Transaction()); + return rv; + } + + if (stream->RequestBlockedOnRead()) { + + // We are blocked waiting for input - either more http headers or + // any request body data. When more data from the request stream + // becomes available the httptransaction will call conn->ResumeSend(). + + LOG(("SpdySession::ReadSegments %p dealing with block on read", this)); + + // call readsegments again if there are other streams ready + // to run in this session + if (WriteQueueSize()) + rv = NS_OK; + else + rv = NS_BASE_STREAM_WOULD_BLOCK; + SetWriteCallbacks(stream->Transaction()); + return rv; + } + + NS_ABORT_IF_FALSE(rv != NS_BASE_STREAM_WOULD_BLOCK, + "Stream Would Block inconsistency"); + + if (NS_FAILED(rv)) { + LOG(("SpdySession::ReadSegments %p returning FAIL code %X", + this, rv)); + return rv; + } + + if (*countRead > 0) { + LOG(("SpdySession::ReadSegments %p stream=%p generated end of frame %d", + this, *countRead)); + mReadyForWrite.Push(stream); + SetWriteCallbacks(stream->Transaction()); + return rv; + } + + LOG(("SpdySession::ReadSegments %p stream=%p stream send complete", this)); + + // in normal http this is done by nshttpconnection, but that class does not + // know which http transaction has made this state transition. + stream->Transaction()-> + OnTransportStatus(mSocketTransport, nsISocketTransport::STATUS_WAITING_FOR, + LL_ZERO); + /* we now want to recv data */ + mConnection->ResumeRecv(stream->Transaction()); + + // call readsegments again if there are other streams ready + // to go in this session + SetWriteCallbacks(stream->Transaction()); + + return rv; +} + +// WriteSegments() is used to read data off the socket. Generally this is +// just the SPDY frame header and from there the appropriate SPDYStream +// is identified from the Stream-ID. The http transaction associated with +// that read then pulls in the data directly, which it will feed to +// OnWriteSegment(). That function will gateway it into http and feed +// it to the appropriate transaction. + +// we call writer->OnWriteSegment to get a spdy header.. and decide if it is +// data or control.. if it is control, just deal with it. +// if it is data, identify the spdy stream +// call stream->WriteSegemnts which can call this::OnWriteSegment to get the +// data. It always gets full frames if they are part of the stream + +nsresult +SpdySession::WriteSegments(nsAHttpSegmentWriter *writer, + PRUint32 count, + PRUint32 *countWritten) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + nsresult rv; + *countWritten = 0; + + if (mClosed) + return NS_ERROR_FAILURE; + + SetWriteCallbacks(nsnull); + + // We buffer all control frames and act on them in this layer. + // We buffer the first 8 bytes of data frames (the header) but + // the actual data is passed through unprocessed. + + if (mDownstreamState == BUFFERING_FRAME_HEADER) { + // The first 8 bytes of every frame is header information that + // we are going to want to strip before passing to http. That is + // true of both control and data packets. + + NS_ABORT_IF_FALSE(mFrameBufferUsed < 8, + "Frame Buffer Used Too Large for State"); + + rv = writer->OnWriteSegment(mFrameBuffer + mFrameBufferUsed, + 8 - mFrameBufferUsed, + countWritten); + if (NS_FAILED(rv)) { + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + ResumeRecv(nsnull); + } + return rv; + } + + mFrameBufferUsed += *countWritten; + + if (mFrameBufferUsed < 8) + { + LOG(("SpdySession::WriteSegments %p " + "BUFFERING FRAME HEADER incomplete size=%d", + this, mFrameBufferUsed)); + return rv; + } + + // For both control and data frames the second 32 bit word of the header + // is 8-flags, 24-length. (network byte order) + mFrameDataSize = + PR_ntohl(reinterpret_cast(mFrameBuffer.get())[1]); + mFrameDataSize &= 0x00ffffff; + mFrameDataRead = 0; + + if (mFrameBuffer[0] & kFlag_Control) { + EnsureBuffer(mFrameBuffer, mFrameDataSize + 8, 8, mFrameBufferSize); + ChangeDownstreamState(BUFFERING_CONTROL_FRAME); + + // The first 32 bit word of the header is + // 1 ctrl - 15 version - 16 type + PRUint16 version = + PR_ntohs(reinterpret_cast(mFrameBuffer.get())[0]); + version &= 0x7fff; + + mFrameControlType = + PR_ntohs(reinterpret_cast(mFrameBuffer.get())[1]); + + LOG(("SpdySession::WriteSegments %p - Control Frame Identified " + "type %d version %d data len %d", + this, mFrameControlType, version, mFrameDataSize)); + + if (mFrameControlType >= CONTROL_TYPE_LAST || + mFrameControlType <= CONTROL_TYPE_FIRST) + return NS_ERROR_ILLEGAL_VALUE; + + // The protocol document says this value must be 1 even though this + // is known as version 2.. Testing interop indicates that is a typo + // in the protocol document + if (version != 2) { + return NS_ERROR_ILLEGAL_VALUE; + } + } + else { + ChangeDownstreamState(PROCESSING_DATA_FRAME); + + PRUint32 streamID = + PR_ntohl(reinterpret_cast(mFrameBuffer.get())[0]); + mFrameDataStream = mStreamIDHash.Get(streamID); + if (!mFrameDataStream) { + LOG(("SpdySession::WriteSegments %p lookup streamID 0x%X failed. " + "Next = 0x%x", this, streamID, mNextStreamID)); + if (streamID >= mNextStreamID) + GenerateRstStream(RST_INVALID_STREAM, streamID); + ChangeDownstreamState(DISCARD_DATA_FRAME); + } + mFrameDataLast = (mFrameBuffer[4] & kFlag_Data_FIN); + Telemetry::Accumulate(Telemetry::SPDY_CHUNK_RECVD, mFrameDataSize >> 10); + LOG(("Start Processing Data Frame. Session=%p Stream 0x%x Fin=%d Len=%d", + this, streamID, mFrameDataLast, mFrameDataSize)); + + if (mFrameBuffer[4] & kFlag_Data_ZLIB) { + LOG(("Data flag has ZLIB flag set which is not valid >=2 spdy")); + return NS_ERROR_ILLEGAL_VALUE; + } + } + } + + if (mDownstreamState == PROCESSING_CONTROL_RST_STREAM) { + if (mDownstreamRstReason == RST_REFUSED_STREAM) + rv = NS_ERROR_NET_RESET; //we can retry this 100% safely + else if (mDownstreamRstReason == RST_CANCEL || + mDownstreamRstReason == RST_PROTOCOL_ERROR || + mDownstreamRstReason == RST_INTERNAL_ERROR || + mDownstreamRstReason == RST_UNSUPPORTED_VERSION) + rv = NS_ERROR_NET_INTERRUPT; + else + rv = NS_ERROR_ILLEGAL_VALUE; + + if (mDownstreamRstReason != RST_REFUSED_STREAM && + mDownstreamRstReason != RST_CANCEL) + mShouldGoAway = true; + + // mFrameDataStream is reset by ChangeDownstreamState + SpdyStream *stream = mFrameDataStream; + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + CleanupStream(stream, rv); + return NS_OK; + } + + if (mDownstreamState == PROCESSING_DATA_FRAME || + mDownstreamState == PROCESSING_CONTROL_SYN_REPLY) { + + mSegmentWriter = writer; + rv = mFrameDataStream->WriteSegments(this, count, countWritten); + mSegmentWriter = nsnull; + + if (rv == NS_BASE_STREAM_CLOSED) { + // This will happen when the transaction figures out it is EOF, generally + // due to a content-length match being made + SpdyStream *stream = mFrameDataStream; + if (mFrameDataRead == mFrameDataSize) + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + CleanupStream(stream, NS_OK); + NS_ABORT_IF_FALSE(!mNeedsCleanup, "double cleanup out of data frame"); + return NS_OK; + } + + if (mNeedsCleanup) { + CleanupStream(mNeedsCleanup, NS_OK); + mNeedsCleanup = nsnull; + } + + // In v3 this is where we would generate a window update + + return rv; + } + + if (mDownstreamState == DISCARD_DATA_FRAME) { + char trash[4096]; + PRUint32 count = NS_MIN(4096U, mFrameDataSize - mFrameDataRead); + + if (!count) { + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + *countWritten = 1; + return NS_OK; + } + + rv = writer->OnWriteSegment(trash, count, countWritten); + + if (NS_FAILED(rv)) { + // maybe just blocked reading from network + ResumeRecv(nsnull); + return rv; + } + + mFrameDataRead += *countWritten; + + if (mFrameDataRead == mFrameDataSize) + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return rv; + } + + NS_ABORT_IF_FALSE(mDownstreamState == BUFFERING_CONTROL_FRAME, + "Not in Bufering Control Frame State"); + NS_ABORT_IF_FALSE(mFrameBufferUsed == 8, + "Frame Buffer Header Not Present"); + + rv = writer->OnWriteSegment(mFrameBuffer + 8 + mFrameDataRead, + mFrameDataSize - mFrameDataRead, + countWritten); + if (NS_FAILED(rv)) { + // maybe just blocked reading from network + ResumeRecv(nsnull); + return rv; + } + + mFrameDataRead += *countWritten; + + if (mFrameDataRead != mFrameDataSize) + return NS_OK; + + rv = sControlFunctions[mFrameControlType](this); + + NS_ABORT_IF_FALSE(NS_FAILED(rv) || + mDownstreamState != BUFFERING_CONTROL_FRAME, + "Control Handler returned OK but did not change state"); + + if (mShouldGoAway && !mStreamTransactionHash.Count()) + Close(NS_OK); + return rv; +} + +void +SpdySession::Close(nsresult aReason) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + if (mClosed) + return; + + LOG(("SpdySession::Close %p %X", this, aReason)); + + mClosed = true; + mStreamTransactionHash.Enumerate(Shutdown, this); + GenerateGoAway(); + mConnection = nsnull; +} + +void +SpdySession::CloseTransaction(nsAHttpTransaction *aTransaction, + nsresult aResult) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("spdysession::CloseTransaction %p %p %x", this, aTransaction, aResult)); + + // Generally this arrives as a cancel event from the connection manager. + + // need to find the stream and call CleanupStream() on it. + SpdyStream *stream = mStreamTransactionHash.Get(aTransaction); + if (!stream) { + LOG(("spdysession::CloseTransaction %p %p %x - not found.", + this, aTransaction, aResult)); + return; + } + LOG(("SpdySession::CloseTranscation probably a cancel. " + "this=%p, trans=%p, result=%x, streamID=0x%X stream=%p", + this, aTransaction, aResult, stream->StreamID(), stream)); + CleanupStream(stream, aResult); + ResumeRecv(this); +} + + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +nsresult +SpdySession::OnReadSegment(const char *buf, + PRUint32 count, + PRUint32 *countRead) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + nsresult rv; + + if (!mOutputQueueUsed && mSegmentReader) { + + // try and write directly without output queue + rv = mSegmentReader->OnReadSegment(buf, count, countRead); + if (NS_SUCCEEDED(rv) || (rv != NS_BASE_STREAM_WOULD_BLOCK)) + return rv; + } + + if (mOutputQueueUsed + count > mOutputQueueSize) + FlushOutputQueue(); + + if (mOutputQueueUsed + count > mOutputQueueSize) + count = mOutputQueueSize - mOutputQueueUsed; + + if (!count) + return NS_BASE_STREAM_WOULD_BLOCK; + + memcpy(mOutputQueueBuffer.get() + mOutputQueueUsed, buf, count); + mOutputQueueUsed += count; + *countRead = count; + + FlushOutputQueue(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +nsresult +SpdySession::OnWriteSegment(char *buf, + PRUint32 count, + PRUint32 *countWritten) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(mSegmentWriter, "OnWriteSegment with null mSegmentWriter"); + nsresult rv; + + if (mDownstreamState == PROCESSING_DATA_FRAME) { + + if (mFrameDataLast && + mFrameDataRead == mFrameDataSize) { + // This will result in Close() being called + mNeedsCleanup = mFrameDataStream; + + LOG(("SpdySession::OnWriteSegment %p - recorded downstream fin of " + "stream %p 0x%X", this, mFrameDataStream, + mFrameDataStream->StreamID())); + *countWritten = 0; + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_BASE_STREAM_CLOSED; + } + + count = NS_MIN(count, mFrameDataSize - mFrameDataRead); + rv = mSegmentWriter->OnWriteSegment(buf, count, countWritten); + if (NS_FAILED(rv)) + return rv; + + mFrameDataRead += *countWritten; + + if ((mFrameDataRead == mFrameDataSize) && !mFrameDataLast) + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + + return rv; + } + + if (mDownstreamState == PROCESSING_CONTROL_SYN_REPLY) { + + if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut && + mFrameDataLast) { + *countWritten = 0; + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_BASE_STREAM_CLOSED; + } + + count = NS_MIN(count, + mFlatHTTPResponseHeaders.Length() - + mFlatHTTPResponseHeadersOut); + memcpy(buf, + mFlatHTTPResponseHeaders.get() + mFlatHTTPResponseHeadersOut, + count); + mFlatHTTPResponseHeadersOut += count; + *countWritten = count; + + if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut && + !mFrameDataLast) + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + return NS_OK; + } + + return NS_ERROR_UNEXPECTED; +} + +//----------------------------------------------------------------------------- +// Modified methods of nsAHttpConnection +//----------------------------------------------------------------------------- + +nsresult +SpdySession::ResumeSend(nsAHttpTransaction *caller) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdySession::ResumeSend %p caller=%p", this, caller)); + + // a trapped signal from the http transaction to the connection that + // it is no longer blocked on read. + + if (!mConnection) + return NS_ERROR_FAILURE; + + SpdyStream *stream = mStreamTransactionHash.Get(caller); + if (stream) + mReadyForWrite.Push(stream); + else + LOG(("SpdySession::ResumeSend %p caller %p not found", this, caller)); + + return mConnection->ResumeSend(caller); +} + +nsresult +SpdySession::ResumeRecv(nsAHttpTransaction *caller) +{ + if (!mConnection) + return NS_ERROR_FAILURE; + + return mConnection->ResumeRecv(caller); +} + +bool +SpdySession::IsPersistent() +{ + return PR_TRUE; +} + +nsresult +SpdySession::TakeTransport(nsISocketTransport **, + nsIAsyncInputStream **, + nsIAsyncOutputStream **) +{ + NS_ABORT_IF_FALSE(false, "TakeTransport of SpdySession"); + return NS_ERROR_UNEXPECTED; +} + +nsHttpConnection * +SpdySession::TakeHttpConnection() +{ + NS_ABORT_IF_FALSE(false, "TakeHttpConnection of SpdySession"); + return nsnull; +} + +//----------------------------------------------------------------------------- +// unused methods of nsAHttpTransaction +// We can be sure of this because SpdySession is only constructed in +// nsHttpConnection and is never passed out of that object +//----------------------------------------------------------------------------- + +void +SpdySession::SetConnection(nsAHttpConnection *) +{ + // This is unexpected + NS_ABORT_IF_FALSE(false, "SpdySession::SetConnection()"); +} + +void +SpdySession::GetSecurityCallbacks(nsIInterfaceRequestor **, + nsIEventTarget **) +{ + // This is unexpected + NS_ABORT_IF_FALSE(false, "SpdySession::GetSecurityCallbacks()"); +} + +void +SpdySession::SetSSLConnectFailed() +{ + NS_ABORT_IF_FALSE(false, "SpdySession::SetSSLConnectFailed()"); +} + +bool +SpdySession::IsDone() +{ + NS_ABORT_IF_FALSE(false, "SpdySession::IsDone()"); + return PR_FALSE; +} + +nsresult +SpdySession::Status() +{ + NS_ABORT_IF_FALSE(false, "SpdySession::Status()"); + return NS_ERROR_UNEXPECTED; +} + +PRUint32 +SpdySession::Available() +{ + NS_ABORT_IF_FALSE(false, "SpdySession::Available()"); + return 0; +} + +nsHttpRequestHead * +SpdySession::RequestHead() +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(false, + "SpdySession::RequestHead() " + "should not be called after SPDY is setup"); + return NULL; +} + +//----------------------------------------------------------------------------- +// Pass through methods of nsAHttpConnection +//----------------------------------------------------------------------------- + +nsAHttpConnection * +SpdySession::Connection() +{ + NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + return mConnection; +} + +nsresult +SpdySession::OnHeadersAvailable(nsAHttpTransaction *transaction, + nsHttpRequestHead *requestHead, + nsHttpResponseHead *responseHead, + bool *reset) +{ + return mConnection->OnHeadersAvailable(transaction, + requestHead, + responseHead, + reset); +} + +void +SpdySession::GetConnectionInfo(nsHttpConnectionInfo **connInfo) +{ + mConnection->GetConnectionInfo(connInfo); +} + +void +SpdySession::GetSecurityInfo(nsISupports **supports) +{ + mConnection->GetSecurityInfo(supports); +} + +bool +SpdySession::IsReused() +{ + return mConnection->IsReused(); +} + +nsresult +SpdySession::PushBack(const char *buf, PRUint32 len) +{ + return mConnection->PushBack(buf, len); +} + +bool +SpdySession::LastTransactionExpectedNoContent() +{ + return mConnection->LastTransactionExpectedNoContent(); +} + +void +SpdySession::SetLastTransactionExpectedNoContent(bool val) +{ + mConnection->SetLastTransactionExpectedNoContent(val); +} + +} // namespace mozilla::net +} // namespace mozilla + diff --git a/netwerk/protocol/http/SpdySession.h b/netwerk/protocol/http/SpdySession.h new file mode 100644 --- /dev/null +++ b/netwerk/protocol/http/SpdySession.h @@ -0,0 +1,317 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* ***** 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 Mozilla. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick McManus + * + * Alternatively, the contents of this file may be used under the terms of + * either of 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 ***** */ + +#ifndef mozilla_net_SpdySession_h +#define mozilla_net_SpdySession_h + +// SPDY as defined by +// http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft2 + +#include "nsAHttpTransaction.h" +#include "nsAHttpConnection.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "nsDeque.h" +#include "nsHashKeys.h" +#include "zlib.h" + +class nsHttpConnection; +class nsISocketTransport; + +namespace mozilla { namespace net { + +class SpdyStream; + +class SpdySession : public nsAHttpTransaction + , public nsAHttpConnection + , public nsAHttpSegmentReader + , public nsAHttpSegmentWriter +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_NSAHTTPCONNECTION + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + + SpdySession(nsAHttpTransaction *, nsISocketTransport *, PRInt32); + ~SpdySession(); + + bool AddStream(nsAHttpTransaction *, PRInt32); + bool CanReuse() { return !mShouldGoAway && !mClosed; } + void DontReuse(); + bool RoomForMoreStreams(); + PRUint32 RegisterStreamID(SpdyStream *); + + const static PRUint8 kFlag_Control = 0x80; + + const static PRUint8 kFlag_Data_FIN = 0x01; + const static PRUint8 kFlag_Data_UNI = 0x02; + const static PRUint8 kFlag_Data_ZLIB = 0x02; + + const static PRUint8 kPri00 = 0x00; + const static PRUint8 kPri01 = 0x40; + const static PRUint8 kPri02 = 0x80; + const static PRUint8 kPri03 = 0xC0; + + enum + { + CONTROL_TYPE_FIRST = 0, + CONTROL_TYPE_SYN_STREAM = 1, + CONTROL_TYPE_SYN_REPLY = 2, + CONTROL_TYPE_RST_STREAM = 3, + CONTROL_TYPE_SETTINGS = 4, + CONTROL_TYPE_NOOP = 5, + CONTROL_TYPE_PING = 6, + CONTROL_TYPE_GOAWAY = 7, + CONTROL_TYPE_HEADERS = 8, + CONTROL_TYPE_WINDOW_UPDATE = 9, /* no longer in v2 */ + CONTROL_TYPE_LAST = 10 + }; + + enum + { + RST_PROTOCOL_ERROR = 1, + RST_INVALID_STREAM = 2, + RST_REFUSED_STREAM = 3, + RST_UNSUPPORTED_VERSION = 4, + RST_CANCEL = 5, + RST_INTERNAL_ERROR = 6, + RST_FLOW_CONTROL_ERROR = 7, + RST_BAD_ASSOC_STREAM = 8 + }; + + enum + { + SETTINGS_TYPE_UPLOAD_BW = 1, // kb/s + SETTINGS_TYPE_DOWNLOAD_BW = 2, // kb/s + SETTINGS_TYPE_RTT = 3, // ms + SETTINGS_TYPE_MAX_CONCURRENT = 4, // streams + SETTINGS_TYPE_CWND = 5, // packets + SETTINGS_TYPE_DOWNLOAD_RETRANS_RATE = 6, // percentage + SETTINGS_TYPE_INITIAL_WINDOW = 7 // bytes. Not used in v2. + }; + + // This should be big enough to hold all of your control packets, + // but if it needs to grow for huge headers it can do so dynamically. + // About 1% of requests to SPDY google services seem to be > 1000 + // with all less than 2000. + const static PRUint32 kDefaultBufferSize = 2000; + + const static PRUint32 kDefaultQueueSize = 16000; + const static PRUint32 kQueueTailRoom = 4000; + const static PRUint32 kSendingChunkSize = 4000; + const static PRUint32 kDefaultMaxConcurrent = 100; + const static PRUint32 kMaxStreamID = 0x7800000; + + static nsresult HandleSynStream(SpdySession *); + static nsresult HandleSynReply(SpdySession *); + static nsresult HandleRstStream(SpdySession *); + static nsresult HandleSettings(SpdySession *); + static nsresult HandleNoop(SpdySession *); + static nsresult HandlePing(SpdySession *); + static nsresult HandleGoAway(SpdySession *); + static nsresult HandleHeaders(SpdySession *); + static nsresult HandleWindowUpdate(SpdySession *); + + static void EnsureBuffer(nsAutoArrayPtr &, + PRUint32, PRUint32, PRUint32 &); +private: + + enum stateType { + BUFFERING_FRAME_HEADER, + BUFFERING_CONTROL_FRAME, + PROCESSING_DATA_FRAME, + DISCARD_DATA_FRAME, + PROCESSING_CONTROL_SYN_REPLY, + PROCESSING_CONTROL_RST_STREAM + }; + + PRUint32 WriteQueueSize(); + void ChangeDownstreamState(enum stateType); + nsresult DownstreamUncompress(char *, PRUint32); + void zlibInit(); + nsresult FindHeader(nsCString, nsDependentCSubstring &); + nsresult ConvertHeaders(nsDependentCSubstring &, + nsDependentCSubstring &); + void GeneratePing(PRUint32); + void GenerateRstStream(PRUint32, PRUint32); + void GenerateGoAway(); + void CleanupStream(SpdyStream *, nsresult); + + void SetWriteCallbacks(nsAHttpTransaction *); + void FlushOutputQueue(); + + bool RoomForMoreConcurrent(); + void ActivateStream(SpdyStream *); + void ProcessPending(); + + static PLDHashOperator Shutdown(nsAHttpTransaction *, + nsAutoPtr &, + void *); + + // This is intended to be nsHttpConnectionMgr:nsHttpConnectionHandle taken + // from the first transcation on this session. That object contains the + // pointer to the real network-level nsHttpConnection object. + nsRefPtr mConnection; + + // The underlying socket transport object is needed to propogate some events + nsISocketTransport *mSocketTransport; + + // These are temporary state variables to hold the argument to + // Read/WriteSegments so it can be accessed by On(read/write)segment + // further up the stack. + nsAHttpSegmentReader *mSegmentReader; + nsAHttpSegmentWriter *mSegmentWriter; + + PRUint32 mSendingChunkSize; /* the transmisison chunk size */ + PRUint32 mNextStreamID; /* 24 bits */ + PRUint32 mConcurrentHighWater; /* max parallelism on session */ + + stateType mDownstreamState; /* in frame, between frames, etc.. */ + + // Maintain 5 indexes - one by stream ID, one by transaction ptr, + // one list of streams ready to write, one list of streams that are queued + // due to max parallelism settings, and one list of streams + // that must be given priority to write for window updates. The objects + // are not ref counted - they get destryoed + // by the nsClassHashtable implementation when they are removed from + // there. + nsDataHashtable mStreamIDHash; + nsClassHashtable, + SpdyStream> mStreamTransactionHash; + nsDeque mReadyForWrite; + nsDeque mQueuedStreams; + + // UrgentForWrite is meant to carry window updates. They were defined in + // the v2 spec but apparently never implemented so are now scheduled to + // be removed. But they will be reintroduced for v3, so we will leave + // this queue in place to ease that transition. + nsDeque mUrgentForWrite; + + // If we block while wrting out a frame then this points to the stream + // that was blocked. When writing again that stream must be the first + // one to write. It is null if there is not a partial frame. + SpdyStream *mPartialFrame; + + // Compression contexts for header transport using deflate. + // SPDY compresses only HTTP headers and does not reset zlib in between + // frames. + z_stream mDownstreamZlib; + z_stream mUpstreamZlib; + + // mFrameBuffer is used to store received control packets and the 8 bytes + // of header on data packets + PRUint32 mFrameBufferSize; + PRUint32 mFrameBufferUsed; + nsAutoArrayPtr mFrameBuffer; + + // mFrameDataSize/Read are used for tracking the amount of data consumed + // in a data frame. the data itself is not buffered in spdy + // The frame size is mFrameDataSize + the constant 8 byte header + PRUint32 mFrameDataSize; + PRUint32 mFrameDataRead; + bool mFrameDataLast; // This frame was marked FIN + + // When a frame has been received that is addressed to a particular stream + // (e.g. a data frame after the stream-id has been decoded), this points + // to the stream. + SpdyStream *mFrameDataStream; + + // A state variable to cleanup a closed stream after the stack has unwound. + SpdyStream *mNeedsCleanup; + + // The CONTROL_TYPE value for a control frame + PRUint32 mFrameControlType; + + // This reason code in the last processed RESET frame + PRUint32 mDownstreamRstReason; + + // These are used for decompressing downstream spdy response headers + // This is done at the session level because sometimes the stream + // has already been canceled but the decompression still must happen + // to keep the zlib state correct for the next state of headers. + PRUint32 mDecompressBufferSize; + PRUint32 mDecompressBufferUsed; + nsAutoArrayPtr mDecompressBuffer; + + // for the conversion of downstream http headers into spdy formatted headers + nsCString mFlatHTTPResponseHeaders; + PRUint32 mFlatHTTPResponseHeadersOut; + + // when set, the session will go away when it reaches 0 streams + bool mShouldGoAway; + + // the session has received a nsAHttpTransaction::Close() call + bool mClosed; + + // the session received a GoAway frame with a valid GoAwayID + bool mCleanShutdown; + + // If a GoAway message was received this is the ID of the last valid + // stream. 0 otherwise. (0 is never a valid stream id.) + PRUint32 mGoAwayID; + + // The limit on number of concurrent streams for this session. Normally it + // is basically unlimited, but the SETTINGS control message from the + // server might bring it down. + PRUint32 mMaxConcurrent; + + // The actual number of concurrent streams at this moment. Generally below + // mMaxConcurrent, but the max can be lowered in real time to a value + // below the current value + PRUint32 mConcurrent; + + // The number of server initiated SYN-STREAMS, tracked for telemetry + PRUint32 mServerPushedResources; + + // This is a output queue of bytes ready to be written to the SSL stream. + // When that streams returns WOULD_BLOCK on direct write the bytes get + // coalesced together here. This results in larger writes to the SSL layer. + // The buffer is not dynamically grown to accomodate stream writes, but + // does expand to accept infallible session wide frames like GoAway and RST. + PRUint32 mOutputQueueSize; + PRUint32 mOutputQueueUsed; + PRUint32 mOutputQueueSent; + nsAutoArrayPtr mOutputQueueBuffer; +}; + +}} // namespace mozilla::net + +#endif // mozilla_net_SpdySession_h diff --git a/netwerk/protocol/http/SpdyStream.cpp b/netwerk/protocol/http/SpdyStream.cpp new file mode 100644 --- /dev/null +++ b/netwerk/protocol/http/SpdyStream.cpp @@ -0,0 +1,843 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* ***** 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 Mozilla. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick McManus + * + * Alternatively, the contents of this file may be used under the terms of + * either of 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 ***** */ + +#include "nsHttp.h" +#include "SpdySession.h" +#include "SpdyStream.h" +#include "nsAlgorithm.h" +#include "prnetdb.h" +#include "nsHttpRequestHead.h" +#include "mozilla/Telemetry.h" +#include "nsISocketTransport.h" +#include "nsISupportsPriority.h" + +#ifdef DEBUG +// defined by the socket transport service while active +extern PRThread *gSocketThread; +#endif + +namespace mozilla { +namespace net { + +SpdyStream::SpdyStream(nsAHttpTransaction *httpTransaction, + SpdySession *spdySession, + nsISocketTransport *socketTransport, + PRUint32 chunkSize, + z_stream *compressionContext, + PRInt32 priority) + : mUpstreamState(GENERATING_SYN_STREAM), + mTransaction(httpTransaction), + mSession(spdySession), + mSocketTransport(socketTransport), + mSegmentReader(nsnull), + mSegmentWriter(nsnull), + mStreamID(0), + mChunkSize(chunkSize), + mSynFrameComplete(0), + mBlockedOnWrite(0), + mRequestBlockedOnRead(0), + mSentFinOnData(0), + mRecvdFin(0), + mFullyOpen(0), + mTxInlineFrameAllocation(SpdySession::kDefaultBufferSize), + mTxInlineFrameSize(0), + mTxInlineFrameSent(0), + mTxStreamFrameSize(0), + mTxStreamFrameSent(0), + mZlib(compressionContext), + mRequestBodyLen(0), + mPriority(priority) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + LOG(("SpdyStream::SpdyStream %p", this)); + + mTxInlineFrame = new char[mTxInlineFrameAllocation]; +} + +SpdyStream::~SpdyStream() +{ +} + +// ReadSegments() is used to write data down the socket. Generally, HTTP +// request data is pulled from the approriate transaction and +// converted to SPDY data. Sometimes control data like a window-update is +// generated instead. + +nsresult +SpdyStream::ReadSegments(nsAHttpSegmentReader *reader, + PRUint32 count, + PRUint32 *countRead) +{ + LOG(("SpdyStream::ReadSegments %p count=%d state=%x", + this, count, mUpstreamState)); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + LOG(("SpdyStream %p ReadSegments reader=%p count=%d", + this, reader, count)); + + nsresult rv = NS_ERROR_UNEXPECTED; + mBlockedOnWrite = 0; + mRequestBlockedOnRead = 0; + + switch (mUpstreamState) { + case GENERATING_SYN_STREAM: + case GENERATING_REQUEST_BODY: + case SENDING_REQUEST_BODY: + // Call into the HTTP Transaction to generate the HTTP request + // stream. That stream will show up in OnReadSegment(). + mSegmentReader = reader; + rv = mTransaction->ReadSegments(this, count, countRead); + mSegmentReader = nsnull; + + if (NS_SUCCEEDED(rv) && + mUpstreamState == GENERATING_SYN_STREAM && + !mSynFrameComplete) + mBlockedOnWrite = 1; + + // Mark that we are blocked on read if we the http transaction + // is going to get us going again. + if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mBlockedOnWrite) + mRequestBlockedOnRead = 1; + + if (!mBlockedOnWrite && NS_SUCCEEDED(rv) && (!*countRead)) { + LOG(("ReadSegments %p Send Request data complete from %x", + this, mUpstreamState)); + if (mSentFinOnData) { + ChangeState(UPSTREAM_COMPLETE); + } + else { + GenerateDataFrameHeader(0, true); + ChangeState(SENDING_FIN_STREAM); + mBlockedOnWrite = 1; + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + } + + break; + + case SENDING_SYN_STREAM: + // We were trying to send the SYN-STREAM but only got part of it out + // before being blocked. Try and send more. + mSegmentReader = reader; + rv = TransmitFrame(nsnull, nsnull); + mSegmentReader = nsnull; + *countRead = 0; + if (NS_SUCCEEDED(rv)) + rv = NS_BASE_STREAM_WOULD_BLOCK; + + if (!mTxInlineFrameSize) { + if (mSentFinOnData) { + ChangeState(UPSTREAM_COMPLETE); + rv = NS_OK; + } + else { + ChangeState(GENERATING_REQUEST_BODY); + mBlockedOnWrite = 1; + } + } + break; + + case SENDING_FIN_STREAM: + // We were trying to send the SYN-STREAM but only got part of it out + // before being blocked. Try and send more. + if (!mSentFinOnData) { + mSegmentReader = reader; + rv = TransmitFrame(nsnull, nsnull); + mSegmentReader = nsnull; + if (!mTxInlineFrameSize) + ChangeState(UPSTREAM_COMPLETE); + } + else { + rv = NS_OK; + mTxInlineFrameSize = 0; // cancel fin data packet + ChangeState(UPSTREAM_COMPLETE); + } + + *countRead = 0; + + // don't change OK to WOULD BLOCK. we are really done sending if OK + break; + + default: + NS_ABORT_IF_FALSE(false, "SpdyStream::ReadSegments unknown state"); + break; + } + + return rv; +} + +// WriteSegments() is used to read data off the socket. Generally this is +// just the SPDY frame header and from there the appropriate SPDYStream +// is identified from the Stream-ID. The http transaction associated with +// that read then pulls in the data directly. + +nsresult +SpdyStream::WriteSegments(nsAHttpSegmentWriter *writer, + PRUint32 count, + PRUint32 *countWritten) +{ + LOG(("SpdyStream::WriteSegments %p count=%d state=%x", + this, count, mUpstreamState)); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(!mSegmentWriter, "segment writer in progress"); + + mSegmentWriter = writer; + nsresult rv = mTransaction->WriteSegments(writer, count, countWritten); + mSegmentWriter = nsnull; + return rv; +} + +PLDHashOperator +SpdyStream::hdrHashEnumerate(const nsACString &key, + nsAutoPtr &value, + void *closure) +{ + SpdyStream *self = static_cast(closure); + + self->CompressToFrame(key); + self->CompressToFrame(value.get()); + return PL_DHASH_NEXT; +} + +nsresult +SpdyStream::ParseHttpRequestHeaders(const char *buf, + PRUint32 avail, + PRUint32 *countUsed) +{ + // Returns NS_OK even if the headers are incomplete + // set mSynFrameComplete flag if they are complete + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(mUpstreamState == GENERATING_SYN_STREAM, "wrong state"); + + LOG(("SpdyStream::ParseHttpRequestHeaders %p avail=%d state=%x", + this, avail, mUpstreamState)); + + mFlatHttpRequestHeaders.Append(buf, avail); + + // We can use the simple double crlf because firefox is the + // only client we are parsing + PRInt32 endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n"); + + if (endHeader == -1) { + // We don't have all the headers yet + LOG(("SpdyStream::ParseHttpRequestHeaders %p " + "Need more header bytes. Len = %d", + this, mFlatHttpRequestHeaders.Length())); + *countUsed = avail; + return NS_OK; + } + + // We have recvd all the headers, trim the local + // buffer of the final empty line, and set countUsed to reflect + // the whole header has been consumed. + PRUint32 oldLen = mFlatHttpRequestHeaders.Length(); + mFlatHttpRequestHeaders.SetLength(endHeader + 2); + *countUsed = avail - (oldLen - endHeader) + 4; + mSynFrameComplete = 1; + + // It is now OK to assign a streamID that we are assured will + // be monotonically increasing amongst syn-streams on this + // session + mStreamID = mSession->RegisterStreamID(this); + NS_ABORT_IF_FALSE(mStreamID & 1, + "Spdy Stream Channel ID must be odd"); + + if (mStreamID >= 0x80000000) { + // streamID must fit in 31 bits. This is theoretically possible + // because stream ID assignment is asynchronous to stream creation + // because of the protocol requirement that the ID in syn-stream + // be monotonically increasing. In reality this is really not possible + // because new streams stop being added to a session with 0x10000000 / 2 + // IDs still available and no race condition is going to bridge that gap, + // so we can be comfortable on just erroring out for correctness in that + // case. + LOG(("Stream assigned out of range ID: 0x%X", mStreamID)); + return NS_ERROR_UNEXPECTED; + } + + // Now we need to convert the flat http headers into a set + // of SPDY headers.. writing to mTxInlineFrame{sz} + + mTxInlineFrame[0] = SpdySession::kFlag_Control; + mTxInlineFrame[1] = 2; /* version */ + mTxInlineFrame[2] = 0; + mTxInlineFrame[3] = SpdySession::CONTROL_TYPE_SYN_STREAM; + // 4 to 7 are length and flags, we'll fill that in later + + PRUint32 networkOrderID = PR_htonl(mStreamID); + memcpy(mTxInlineFrame + 8, &networkOrderID, 4); + + // this is the associated-to field, which is not used sending + // from the client in the http binding + memset (mTxInlineFrame + 12, 0, 4); + + // Priority flags are the C0 mask of byte 16. + // From low to high: 00 40 80 C0 + // higher raw priority values are actually less important + // + // The other 6 bits of 16 are unused. Spdy/3 will expand + // priority to 4 bits. + // + // When Spdy/3 implements WINDOW_UPDATE the lowest priority + // streams over a threshold (32?) should be given tiny + // receive windows, separate from their spdy priority + // + if (mPriority >= nsISupportsPriority::PRIORITY_LOW) + mTxInlineFrame[16] = SpdySession::kPri00; + else if (mPriority >= nsISupportsPriority::PRIORITY_NORMAL) + mTxInlineFrame[16] = SpdySession::kPri01; + else if (mPriority >= nsISupportsPriority::PRIORITY_HIGH) + mTxInlineFrame[16] = SpdySession::kPri02; + else + mTxInlineFrame[16] = SpdySession::kPri03; + + mTxInlineFrame[17] = 0; /* unused */ + +// nsCString methodHeader; +// mTransaction->RequestHead()->Method()->ToUTF8String(methodHeader); + const char *methodHeader = mTransaction->RequestHead()->Method().get(); + + nsCString hostHeader; + mTransaction->RequestHead()->GetHeader(nsHttp::Host, hostHeader); + + nsCString versionHeader; + if (mTransaction->RequestHead()->Version() == NS_HTTP_VERSION_1_1) + versionHeader = NS_LITERAL_CSTRING("HTTP/1.1"); + else + versionHeader = NS_LITERAL_CSTRING("HTTP/1.0"); + + nsClassHashtable hdrHash; + + // use mRequestHead() to get a sense of how big to make the hash, + // even though we are parsing the actual text stream because + // it is legit to append headers. + hdrHash.Init(1 + (mTransaction->RequestHead()->Headers().Count() * 2)); + + const char *beginBuffer = mFlatHttpRequestHeaders.BeginReading(); + + // need to hash all the headers together to remove duplicates, special + // headers, etc.. + + PRInt32 crlfIndex = mFlatHttpRequestHeaders.Find("\r\n"); + while (true) { + PRInt32 startIndex = crlfIndex + 2; + + crlfIndex = mFlatHttpRequestHeaders.Find("\r\n", false, startIndex); + if (crlfIndex == -1) + break; + + PRInt32 colonIndex = mFlatHttpRequestHeaders.Find(":", false, startIndex, + crlfIndex - startIndex); + if (colonIndex == -1) + break; + + nsDependentCSubstring name = Substring(beginBuffer + startIndex, + beginBuffer + colonIndex); + // all header names are lower case in spdy + ToLowerCase(name); + + if (name.Equals("method") || + name.Equals("version") || + name.Equals("scheme") || + name.Equals("keep-alive") || + name.Equals("accept-encoding") || + name.Equals("TE") || + name.Equals("connection") || + name.Equals("proxy-connection") || + name.Equals("url")) + continue; + + nsCString *val = hdrHash.Get(name); + if (!val) { + val = new nsCString(); + hdrHash.Put(name, val); + } + + PRInt32 valueIndex = colonIndex + 1; + while (valueIndex < crlfIndex && beginBuffer[valueIndex] == ' ') + ++valueIndex; + + nsDependentCSubstring v = Substring(beginBuffer + valueIndex, + beginBuffer + crlfIndex); + if (!val->IsEmpty()) + val->Append(static_cast(0)); + val->Append(v); + + if (name.Equals("content-length")) { + PRInt64 len; + if (nsHttp::ParseInt64(val->get(), nsnull, &len)) + mRequestBodyLen = len; + } + } + + mTxInlineFrameSize = 18; + + LOG(("http request headers to encode are: \n%s", + mFlatHttpRequestHeaders.get())); + + // The header block length + PRUint16 count = hdrHash.Count() + 4; /* method, scheme, url, version */ + CompressToFrame(count); + + // method, scheme, url, and version headers for request line + + CompressToFrame(NS_LITERAL_CSTRING("method")); + CompressToFrame(methodHeader, strlen(methodHeader)); + CompressToFrame(NS_LITERAL_CSTRING("scheme")); + CompressToFrame(NS_LITERAL_CSTRING("https")); + CompressToFrame(NS_LITERAL_CSTRING("url")); + CompressToFrame(mTransaction->RequestHead()->RequestURI()); + CompressToFrame(NS_LITERAL_CSTRING("version")); + CompressToFrame(versionHeader); + + hdrHash.Enumerate(hdrHashEnumerate, this); + CompressFlushFrame(); + + // 4 to 7 are length and flags, which we can now fill in + (reinterpret_cast(mTxInlineFrame.get()))[1] = + PR_htonl(mTxInlineFrameSize - 8); + + NS_ABORT_IF_FALSE(!mTxInlineFrame[4], + "Size greater than 24 bits"); + + // For methods other than POST and PUT, we will set the fin bit + // right on the syn stream packet. + + if (mTransaction->RequestHead()->Method() != nsHttp::Post && + mTransaction->RequestHead()->Method() != nsHttp::Put) { + mSentFinOnData = 1; + mTxInlineFrame[4] = SpdySession::kFlag_Data_FIN; + } + + Telemetry::Accumulate(Telemetry::SPDY_SYN_SIZE, mTxInlineFrameSize - 18); + + // The size of the input headers is approximate + PRUint32 ratio = + (mTxInlineFrameSize - 18) * 100 / + (11 + mTransaction->RequestHead()->RequestURI().Length() + + mFlatHttpRequestHeaders.Length()); + + Telemetry::Accumulate(Telemetry::SPDY_SYN_RATIO, ratio); + return NS_OK; +} + +nsresult +SpdyStream::TransmitFrame(const char *buf, + PRUint32 *countUsed) +{ + NS_ABORT_IF_FALSE(mTxInlineFrameSize, "empty stream frame in transmit"); + NS_ABORT_IF_FALSE(mSegmentReader, "TransmitFrame with null mSegmentReader"); + + PRUint32 transmittedCount; + nsresult rv; + + LOG(("SpdyStream::TransmitFrame %p inline=%d of %d stream=%d of %d", + this, mTxInlineFrameSent, mTxInlineFrameSize, + mTxStreamFrameSent, mTxStreamFrameSize)); + if (countUsed) + *countUsed = 0; + mBlockedOnWrite = 0; + + // In the (relatively common) event that we have a small amount of data + // split between the inlineframe and the streamframe, then move the stream + // data into the inlineframe via copy in order to coalesce into one write. + // Given the interaction with ssl this is worth the small copy cost. + if (mTxStreamFrameSize && mTxInlineFrameSize && + !mTxInlineFrameSent && !mTxStreamFrameSent && + mTxStreamFrameSize < SpdySession::kDefaultBufferSize && + mTxInlineFrameSize + mTxStreamFrameSize < mTxInlineFrameAllocation) { + LOG(("Coalesce Transmit")); + memcpy (mTxInlineFrame + mTxInlineFrameSize, + buf, mTxStreamFrameSize); + mTxInlineFrameSize += mTxStreamFrameSize; + mTxStreamFrameSent = 0; + mTxStreamFrameSize = 0; + } + + // This function calls mSegmentReader->OnReadSegment to report the actual SPDY + // bytes through to the SpdySession and then the HttpConnection which calls + // the socket write function. + + while (mTxInlineFrameSent < mTxInlineFrameSize) { + rv = mSegmentReader->OnReadSegment(mTxInlineFrame + mTxInlineFrameSent, + mTxInlineFrameSize - mTxInlineFrameSent, + &transmittedCount); + LOG(("SpdyStream::TransmitFrame for inline %p result %x len=%d", + this, rv, transmittedCount)); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) + mBlockedOnWrite = 1; + + if (NS_FAILED(rv)) // this will include WOULD_BLOCK + return rv; + + mTxInlineFrameSent += transmittedCount; + } + + PRUint32 offset = 0; + NS_ABORT_IF_FALSE(mTxStreamFrameSize >= mTxStreamFrameSent, + "negative unsent"); + PRUint32 avail = mTxStreamFrameSize - mTxStreamFrameSent; + + while (avail) { + NS_ABORT_IF_FALSE(countUsed, "null countused pointer in a stream context"); + rv = mSegmentReader->OnReadSegment(buf + offset, avail, &transmittedCount); + + LOG(("SpdyStream::TransmitFrame for stream %p result %x len=%d", + this, rv, transmittedCount)); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) + mBlockedOnWrite = 1; + + if (NS_FAILED(rv)) // this will include WOULD_BLOCK + return rv; + + if (mUpstreamState == SENDING_REQUEST_BODY) { + mTransaction->OnTransportStatus(mSocketTransport, + nsISocketTransport::STATUS_SENDING_TO, + transmittedCount); + } + + *countUsed += transmittedCount; + avail -= transmittedCount; + offset += transmittedCount; + mTxStreamFrameSent += transmittedCount; + } + + if (!avail) { + mTxInlineFrameSent = 0; + mTxInlineFrameSize = 0; + mTxStreamFrameSent = 0; + mTxStreamFrameSize = 0; + } + + return NS_OK; +} + +void +SpdyStream::ChangeState(enum stateType newState) +{ + LOG(("SpdyStream::ChangeState() %p from %X to %X", mUpstreamState, newState)); + mUpstreamState = newState; + return; +} + +void +SpdyStream::GenerateDataFrameHeader(PRUint32 dataLength, bool lastFrame) +{ + LOG(("SpdyStream::GenerateDataFrameHeader %p len=%d last=%d", + this, dataLength, lastFrame)); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(!mTxInlineFrameSize, "inline frame not empty"); + NS_ABORT_IF_FALSE(!mTxInlineFrameSent, "inline partial send not 0"); + NS_ABORT_IF_FALSE(!mTxStreamFrameSize, "stream frame not empty"); + NS_ABORT_IF_FALSE(!mTxStreamFrameSent, "stream partial send not 0"); + NS_ABORT_IF_FALSE(!(dataLength & 0xff000000), "datalength > 24 bits"); + + (reinterpret_cast(mTxInlineFrame.get()))[0] = PR_htonl(mStreamID); + (reinterpret_cast(mTxInlineFrame.get()))[1] = + PR_htonl(dataLength); + + NS_ABORT_IF_FALSE(!(mTxInlineFrame[0] & 0x80), + "control bit set unexpectedly"); + NS_ABORT_IF_FALSE(!mTxInlineFrame[4], "flag bits set unexpectedly"); + + mTxInlineFrameSize = 8; + mTxStreamFrameSize = dataLength; + + if (lastFrame) { + mTxInlineFrame[4] |= SpdySession::kFlag_Data_FIN; + if (dataLength) + mSentFinOnData = 1; + } +} + +void +SpdyStream::CompressToFrame(const nsACString &str) +{ + CompressToFrame(str.BeginReading(), str.Length()); +} + +void +SpdyStream::CompressToFrame(const nsACString *str) +{ + CompressToFrame(str->BeginReading(), str->Length()); +} + +// Dictionary taken from +// http://dev.chromium.org/spdy/spdy-protocol/spdy-protocol-draft2 +// Name/Value Header Block Format +// spec indicates that the compression dictionary is not null terminated +// but in reality it is. see: +// https://groups.google.com/forum/#!topic/spdy-dev/2pWxxOZEIcs + +const char *SpdyStream::kDictionary = + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + ".1statusversionurl"; + +// use for zlib data types +void * +SpdyStream::zlib_allocator(void *opaque, uInt items, uInt size) +{ + return moz_xmalloc(items * size); +} + +// use for zlib data types +void +SpdyStream::zlib_destructor(void *opaque, void *addr) +{ + moz_free(addr); +} + +void +SpdyStream::ExecuteCompress(PRUint32 flushMode) +{ + // Expect mZlib->avail_in and mZlib->next_in to be set. + // Append the compressed version of next_in to mTxInlineFrame + + do + { + PRUint32 avail = mTxInlineFrameAllocation - mTxInlineFrameSize; + if (avail < 1) { + SpdySession::EnsureBuffer(mTxInlineFrame, + mTxInlineFrameAllocation + 2000, + mTxInlineFrameSize, + mTxInlineFrameAllocation); + avail = mTxInlineFrameAllocation - mTxInlineFrameSize; + } + + mZlib->next_out = reinterpret_cast (mTxInlineFrame.get()) + + mTxInlineFrameSize; + mZlib->avail_out = avail; + deflate(mZlib, flushMode); + mTxInlineFrameSize += avail - mZlib->avail_out; + } while (mZlib->avail_in > 0 || !mZlib->avail_out); +} + +void +SpdyStream::CompressToFrame(PRUint16 data) +{ + // convert the data to network byte order and write that + // to the compressed stream + + data = PR_htons(data); + + mZlib->next_in = reinterpret_cast (&data); + mZlib->avail_in = 2; + ExecuteCompress(Z_NO_FLUSH); +} + + +void +SpdyStream::CompressToFrame(const char *data, PRUint32 len) +{ + // Format calls for a network ordered 16 bit length + // followed by the utf8 string + + // for now, silently truncate headers greater than 64KB. Spdy/3 will + // fix this by making the len a 32 bit quantity + if (len > 0xffff) + len = 0xffff; + + PRUint16 networkLen = len; + networkLen = PR_htons(len); + + // write out the length + mZlib->next_in = reinterpret_cast (&networkLen); + mZlib->avail_in = 2; + ExecuteCompress(Z_NO_FLUSH); + + // write out the data + mZlib->next_in = (unsigned char *)data; + mZlib->avail_in = len; + ExecuteCompress(Z_NO_FLUSH); +} + +void +SpdyStream::CompressFlushFrame() +{ + mZlib->next_in = (unsigned char *) ""; + mZlib->avail_in = 0; + ExecuteCompress(Z_SYNC_FLUSH); +} + +void +SpdyStream::Close(nsresult reason) +{ + mTransaction->Close(reason); +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +nsresult +SpdyStream::OnReadSegment(const char *buf, + PRUint32 count, + PRUint32 *countRead) +{ + LOG(("SpdyStream::OnReadSegment %p count=%d state=%x", + this, count, mUpstreamState)); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(mSegmentReader, "OnReadSegment with null mSegmentReader"); + + nsresult rv = NS_ERROR_UNEXPECTED; + PRUint32 dataLength; + + switch (mUpstreamState) { + case GENERATING_SYN_STREAM: + // The buffer is the HTTP request stream, including at least part of the + // HTTP request header. This state's job is to build a SYN_STREAM frame + // from the header information. count is the number of http bytes available + // (which may include more than the header), and in countRead we return + // the number of those bytes that we consume (i.e. the portion that are + // header bytes) + + rv = ParseHttpRequestHeaders(buf, count, countRead); + if (NS_FAILED(rv)) + return rv; + LOG(("ParseHttpRequestHeaders %p used %d of %d.", + this, *countRead, count)); + if (mSynFrameComplete) { + NS_ABORT_IF_FALSE(mTxInlineFrameSize, + "OnReadSegment SynFrameComplete 0b"); + rv = TransmitFrame(nsnull, nsnull); + if (rv == NS_BASE_STREAM_WOULD_BLOCK && *countRead) + rv = NS_OK; + if (mTxInlineFrameSize) + ChangeState(SENDING_SYN_STREAM); + else + ChangeState(GENERATING_REQUEST_BODY); + break; + } + NS_ABORT_IF_FALSE(*countRead == count, + "Header parsing not complete but unused data"); + break; + + case GENERATING_REQUEST_BODY: + NS_ABORT_IF_FALSE(!mTxInlineFrameSent, + "OnReadSegment in generating_request_body with " + "frame in progress"); + if (count < mChunkSize && count < mRequestBodyLen) { + LOG(("SpdyStream %p id %x has %d to write out of a bodylen %d" + " with a chunk size of %d. Waiting for more.", + this, mStreamID, count, mChunkSize, mRequestBodyLen)); + rv = NS_BASE_STREAM_WOULD_BLOCK; + break; + } + + dataLength = NS_MIN(count, mChunkSize); + if (dataLength > mRequestBodyLen) + return NS_ERROR_UNEXPECTED; + mRequestBodyLen -= dataLength; + GenerateDataFrameHeader(dataLength, !mRequestBodyLen); + ChangeState(SENDING_REQUEST_BODY); + // NO BREAK + + case SENDING_REQUEST_BODY: + NS_ABORT_IF_FALSE(mTxInlineFrameSize, "OnReadSegment Send Data Header 0b"); + rv = TransmitFrame(buf, countRead); + LOG(("TransmitFrame() rv=%x returning %d data bytes. " + "Header is %d/%d Body is %d/%d.", + rv, *countRead, + mTxInlineFrameSent, mTxInlineFrameSize, + mTxStreamFrameSent, mTxStreamFrameSize)); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK && *countRead) + rv = NS_OK; + + // If that frame was all sent, look for another one + if (!mTxInlineFrameSize) + ChangeState(GENERATING_REQUEST_BODY); + break; + + case SENDING_SYN_STREAM: + rv = NS_BASE_STREAM_WOULD_BLOCK; + break; + + case SENDING_FIN_STREAM: + NS_ABORT_IF_FALSE(false, + "resuming partial fin stream out of OnReadSegment"); + break; + + default: + NS_ABORT_IF_FALSE(false, "SpdyStream::OnReadSegment non-write state"); + break; + } + + return rv; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +nsresult +SpdyStream::OnWriteSegment(char *buf, + PRUint32 count, + PRUint32 *countWritten) +{ + LOG(("SpdyStream::OnWriteSegment %p count=%d state=%x", + this, count, mUpstreamState)); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(mSegmentWriter, "OnWriteSegment with null mSegmentWriter"); + + return mSegmentWriter->OnWriteSegment(buf, count, countWritten); +} + +} // namespace mozilla::net +} // namespace mozilla + diff --git a/netwerk/protocol/http/SpdyStream.h b/netwerk/protocol/http/SpdyStream.h new file mode 100644 --- /dev/null +++ b/netwerk/protocol/http/SpdyStream.h @@ -0,0 +1,207 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* ***** 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 Mozilla. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick McManus + * + * Alternatively, the contents of this file may be used under the terms of + * either of 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 ***** */ + +#ifndef mozilla_net_SpdyStream_h +#define mozilla_net_SpdyStream_h + +#include "nsAHttpTransaction.h" + +namespace mozilla { namespace net { + +class SpdyStream : public nsAHttpSegmentReader + , public nsAHttpSegmentWriter +{ +public: + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + + SpdyStream(nsAHttpTransaction *, + SpdySession *, nsISocketTransport *, + PRUint32, z_stream *, PRInt32); + ~SpdyStream(); + + PRUint32 StreamID() { return mStreamID; } + + nsresult ReadSegments(nsAHttpSegmentReader *, PRUint32, PRUint32 *); + nsresult WriteSegments(nsAHttpSegmentWriter *, PRUint32, PRUint32 *); + + bool BlockedOnWrite() + { + return static_cast(mBlockedOnWrite); + } + + bool RequestBlockedOnRead() + { + return static_cast(mRequestBlockedOnRead); + } + + // returns false if called more than once + bool SetFullyOpen() + { + bool result = !mFullyOpen; + mFullyOpen = 1; + return result; + } + + nsAHttpTransaction *Transaction() + { + return mTransaction; + } + + void Close(nsresult reason); + + void SetRecvdFin(bool aStatus) { mRecvdFin = aStatus ? 1 : 0; } + bool RecvdFin() { return mRecvdFin; } + + // The zlib header compression dictionary defined by SPDY, + // and hooks to the mozilla allocator for zlib to use. + static const char *kDictionary; + static void *zlib_allocator(void *, uInt, uInt); + static void zlib_destructor(void *, void *); + +private: + + enum stateType { + GENERATING_SYN_STREAM, + SENDING_SYN_STREAM, + GENERATING_REQUEST_BODY, + SENDING_REQUEST_BODY, + SENDING_FIN_STREAM, + UPSTREAM_COMPLETE + }; + + static PLDHashOperator hdrHashEnumerate(const nsACString &, + nsAutoPtr &, + void *); + + void ChangeState(enum stateType ); + nsresult ParseHttpRequestHeaders(const char *, PRUint32, PRUint32 *); + nsresult TransmitFrame(const char *, PRUint32 *); + void GenerateDataFrameHeader(PRUint32, bool); + + void CompressToFrame(const nsACString &); + void CompressToFrame(const nsACString *); + void CompressToFrame(const char *, PRUint32); + void CompressToFrame(PRUint16); + void CompressFlushFrame(); + void ExecuteCompress(PRUint32); + + // Each stream goes from syn_stream to upstream_complete, perhaps + // looping on multiple instances of generating_request_body and + // sending_request_body for each SPDY chunk in the upload. + enum stateType mUpstreamState; + + // The underlying HTTP transaction + nsRefPtr mTransaction; + + // The session that this stream is a subset of + SpdySession *mSession; + + // The underlying socket transport object is needed to propogate some events + nsISocketTransport *mSocketTransport; + + // These are temporary state variables to hold the argument to + // Read/WriteSegments so it can be accessed by On(read/write)segment + // further up the stack. + nsAHttpSegmentReader *mSegmentReader; + nsAHttpSegmentWriter *mSegmentWriter; + + // The 24 bit SPDY stream ID + PRUint32 mStreamID; + + // The quanta upstream data frames are chopped into + PRUint32 mChunkSize; + + // Flag is set when all http request headers have been read + PRUint32 mSynFrameComplete : 1; + + // Flag is set when there is more request data to send and the + // stream needs to be rescheduled for writing. Sometimes this + // is done as a matter of fairness, not really due to blocking + PRUint32 mBlockedOnWrite : 1; + + // Flag is set when the HTTP processor has more data to send + // but has blocked in doing so. + PRUint32 mRequestBlockedOnRead : 1; + + // Flag is set when a FIN has been placed on a data or syn packet + // (i.e after the client has closed) + PRUint32 mSentFinOnData : 1; + + // Flag is set after the response frame bearing the fin bit has + // been processed. (i.e. after the server has closed). + PRUint32 mRecvdFin : 1; + + // Flag is set after syn reply received + PRUint32 mFullyOpen : 1; + + // The InlineFrame and associated data is used for composing control + // frames and data frame headers. + nsAutoArrayPtr mTxInlineFrame; + PRUint32 mTxInlineFrameAllocation; + PRUint32 mTxInlineFrameSize; + PRUint32 mTxInlineFrameSent; + + // mTxStreamFrameSize and mTxStreamFrameSent track the progress of + // transmitting a request body data frame. The data frame itself + // is never copied into the spdy layer. + PRUint32 mTxStreamFrameSize; + PRUint32 mTxStreamFrameSent; + + // Compression context and buffer for request header compression. + // This is a copy of SpdySession::mUpstreamZlib because it needs + // to remain the same in all streams of a session. + z_stream *mZlib; + nsCString mFlatHttpRequestHeaders; + + // Track the content-length of a request body so that we can + // place the fin flag on the last data packet instead of waiting + // for a stream closed indication. Relying on stream close results + // in an extra 0-length runt packet and seems to have some interop + // problems with the google servers. + PRInt64 mRequestBodyLen; + + // based on nsISupportsPriority definitions + PRInt32 mPriority; + +}; + +}} // namespace mozilla::net + +#endif // mozilla_net_SpdyStream_h diff --git a/netwerk/protocol/http/nsAHttpConnection.h b/netwerk/protocol/http/nsAHttpConnection.h --- a/netwerk/protocol/http/nsAHttpConnection.h +++ b/netwerk/protocol/http/nsAHttpConnection.h @@ -72,18 +72,18 @@ public: nsHttpResponseHead *, bool *reset) = 0; // // called by a transaction to resume either sending or receiving data // after a transaction returned NS_BASE_STREAM_WOULD_BLOCK from its // ReadSegments/WriteSegments methods. // - virtual nsresult ResumeSend() = 0; - virtual nsresult ResumeRecv() = 0; + virtual nsresult ResumeSend(nsAHttpTransaction *caller) = 0; + virtual nsresult ResumeRecv(nsAHttpTransaction *caller) = 0; // // called by the connection manager to close a transaction being processed // by this connection. // // @param transaction // the transaction being closed. // @param reason @@ -123,18 +123,18 @@ public: // Transfer the base http connection object along with a // reference to it to the caller. virtual nsHttpConnection *TakeHttpConnection() = 0; }; #define NS_DECL_NSAHTTPCONNECTION \ nsresult OnHeadersAvailable(nsAHttpTransaction *, nsHttpRequestHead *, nsHttpResponseHead *, bool *reset); \ - nsresult ResumeSend(); \ - nsresult ResumeRecv(); \ + nsresult ResumeSend(nsAHttpTransaction *); \ + nsresult ResumeRecv(nsAHttpTransaction *); \ void CloseTransaction(nsAHttpTransaction *, nsresult); \ void GetConnectionInfo(nsHttpConnectionInfo **); \ nsresult TakeTransport(nsISocketTransport **, \ nsIAsyncInputStream **, \ nsIAsyncOutputStream **); \ void GetSecurityInfo(nsISupports **); \ bool IsPersistent(); \ bool IsReused(); \ diff --git a/netwerk/protocol/http/nsAHttpTransaction.h b/netwerk/protocol/http/nsAHttpTransaction.h --- a/netwerk/protocol/http/nsAHttpTransaction.h +++ b/netwerk/protocol/http/nsAHttpTransaction.h @@ -57,16 +57,17 @@ class nsHttpRequestHead; // write function returns NS_BASE_STREAM_WOULD_BLOCK in this case). //---------------------------------------------------------------------------- class nsAHttpTransaction : public nsISupports { public: // called by the connection when it takes ownership of the transaction. virtual void SetConnection(nsAHttpConnection *) = 0; + virtual nsAHttpConnection *Connection() = 0; // called by the connection to get security callbacks to set on the // socket transport. virtual void GetSecurityCallbacks(nsIInterfaceRequestor **, nsIEventTarget **) = 0; // called to report socket status (see nsITransportEventSink) virtual void OnTransportStatus(nsITransport* transport, @@ -94,16 +95,17 @@ public: virtual void SetSSLConnectFailed() = 0; // called to retrieve the request headers of the transaction virtual nsHttpRequestHead *RequestHead() = 0; }; #define NS_DECL_NSAHTTPTRANSACTION \ void SetConnection(nsAHttpConnection *); \ + nsAHttpConnection *Connection(); \ void GetSecurityCallbacks(nsIInterfaceRequestor **, \ nsIEventTarget **); \ void OnTransportStatus(nsITransport* transport, \ nsresult status, PRUint64 progress); \ bool IsDone(); \ nsresult Status(); \ PRUint32 Available(); \ nsresult ReadSegments(nsAHttpSegmentReader *, PRUint32, PRUint32 *); \ diff --git a/netwerk/protocol/http/nsHttp.h b/netwerk/protocol/http/nsHttp.h --- a/netwerk/protocol/http/nsHttp.h +++ b/netwerk/protocol/http/nsHttp.h @@ -126,16 +126,21 @@ typedef PRUint8 nsHttpVersion; // a transaction with this caps flag keeps timing information #define NS_HTTP_TIMING_ENABLED (1<<5) // a transaction with this caps flag will not only not use an existing // persistent connection but it will close outstanding ones to the same // host. Used by a forced reload to reset the connection states. #define NS_HTTP_CLEAR_KEEPALIVES (1<<6) +// Disallow the use of the SPDY protocol. This is meant for the contexts +// such as HTTP upgrade which are nonsensical for SPDY, it is not the +// SPDY configuration variable. +#define NS_HTTP_DISALLOW_SPDY (1<<7) + //----------------------------------------------------------------------------- // some default values //----------------------------------------------------------------------------- // hard upper limit on the number of requests that can be pipelined #define NS_HTTP_MAX_PIPELINED_REQUESTS 8 #define NS_HTTP_DEFAULT_PORT 80 diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -629,16 +629,17 @@ nsHttpChannel::SetupTransaction() if (mUpgradeProtocolCallback) { mRequestHead.SetHeader(nsHttp::Upgrade, mUpgradeProtocol, false); mRequestHead.SetHeader(nsHttp::Connection, nsDependentCString(nsHttp::Upgrade.get()), true); mCaps |= NS_HTTP_STICKY_CONNECTION; mCaps &= ~NS_HTTP_ALLOW_PIPELINING; mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; + mCaps |= NS_HTTP_DISALLOW_SPDY; } nsCOMPtr responseStream; rv = mTransaction->Init(mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mUploadStreamHasHeaders, NS_GetCurrentThread(), callbacks, this, getter_AddRefs(responseStream)); if (NS_FAILED(rv)) { diff --git a/netwerk/protocol/http/nsHttpConnection.cpp b/netwerk/protocol/http/nsHttpConnection.cpp --- a/netwerk/protocol/http/nsHttpConnection.cpp +++ b/netwerk/protocol/http/nsHttpConnection.cpp @@ -48,16 +48,19 @@ #include "nsIServiceManager.h" #include "nsISSLSocketControl.h" #include "nsStringStream.h" #include "netCore.h" #include "nsNetCID.h" #include "nsProxyRelease.h" #include "prmem.h" #include "nsPreloadedStream.h" +#include "SpdySession.h" +#include "mozilla/Telemetry.h" +#include "nsISupportsPriority.h" #ifdef DEBUG // defined by the socket transport service while active extern PRThread *gSocketThread; #endif static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); @@ -77,16 +80,20 @@ nsHttpConnection::nsHttpConnection() , mMaxBytesRead(0) , mKeepAlive(true) // assume to keep-alive by default , mKeepAliveMask(true) , mSupportsPipelining(false) // assume low-grade server , mIsReused(false) , mCompletedProxyConnect(false) , mLastTransactionExpectedNoContent(false) , mIdleMonitoring(false) + , mNPNComplete(false) + , mUsingSpdy(false) + , mPriority(nsISupportsPriority::PRIORITY_NORMAL) + , mReportedSpdy(false) { LOG(("Creating nsHttpConnection @%x\n", this)); // grab a reference to the handler to ensure that it doesn't go away. nsHttpHandler *handler = gHttpHandler; NS_ADDREF(handler); } @@ -136,41 +143,139 @@ nsHttpConnection::Init(nsHttpConnectionI mCallbacks = callbacks; mCallbackTarget = callbackTarget; rv = mSocketTransport->SetSecurityCallbacks(this); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } +bool +nsHttpConnection::EnsureNPNComplete() +{ + // NPN is only used by SPDY right now. + // + // If for some reason the components to check on NPN aren't available, + // this function will just return true to continue on and disable SPDY + + NS_ABORT_IF_FALSE(mSocketTransport, "EnsureNPNComplete " + "socket transport precondition"); + + if (mNPNComplete) + return true; + + nsresult rv; + + nsCOMPtr securityInfo; + nsCOMPtr ssl; + nsCAutoString negotiatedNPN; + + rv = mSocketTransport->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (NS_FAILED(rv)) + goto npnComplete; + + ssl = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv)) + goto npnComplete; + + rv = ssl->GetNegotiatedNPN(negotiatedNPN); + if (rv == NS_ERROR_NOT_CONNECTED) { + + rv = ssl->ForceHandshake(); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) + goto npnComplete; + return false; + } + + if (NS_FAILED(rv)) + goto npnComplete; + + LOG(("nsHttpConnection::EnsureNPNComplete %p negotiated to '%s'", + this, negotiatedNPN.get())); + + if (negotiatedNPN.Equals(NS_LITERAL_CSTRING("spdy/2"))) { + + mUsingSpdy = true; + mIsReused = true; /* all spdy streams are reused */ + + // Wrap the old http transaction into the new spdy session + // as the first stream + mSpdySession = new SpdySession(mTransaction, + mSocketTransport, + mPriority); + mTransaction = mSpdySession; + mIdleTimeout = gHttpHandler->SpdyTimeout(); + } + + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SPDY_NPN_CONNECT, + mUsingSpdy); + +npnComplete: + LOG(("nsHttpConnection::EnsureNPNComplete setting complete to true")); + mNPNComplete = true; + return true; +} + // called on the socket thread nsresult -nsHttpConnection::Activate(nsAHttpTransaction *trans, PRUint8 caps) +nsHttpConnection::Activate(nsAHttpTransaction *trans, PRUint8 caps, PRInt32 pri) { nsresult rv; NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); LOG(("nsHttpConnection::Activate [this=%x trans=%x caps=%x]\n", this, trans, caps)); + mPriority = pri; + if (mTransaction && mUsingSpdy) + return AddTransaction(trans, pri); + NS_ENSURE_ARG_POINTER(trans); NS_ENSURE_TRUE(!mTransaction, NS_ERROR_IN_PROGRESS); // Update security callbacks nsCOMPtr callbacks; nsCOMPtr callbackTarget; trans->GetSecurityCallbacks(getter_AddRefs(callbacks), getter_AddRefs(callbackTarget)); if (callbacks != mCallbacks) { mCallbacks.swap(callbacks); if (callbacks) NS_ProxyRelease(mCallbackTarget, callbacks); mCallbackTarget = callbackTarget; } + // Setup NPN Negotiation if necessary (only for SPDY) + if (!mNPNComplete) { + + mNPNComplete = true; + + if (mConnInfo->UsingSSL() && + !(caps & NS_HTTP_DISALLOW_SPDY) && + gHttpHandler->IsSpdyEnabled()) { + LOG(("nsHttpConnection::Init Setting up SPDY Negotiation")); + nsCOMPtr securityInfo; + nsresult rv = + mSocketTransport->GetSecurityInfo(getter_AddRefs(securityInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr ssl = + do_QueryInterface(securityInfo, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray protocolArray; + protocolArray.AppendElement(NS_LITERAL_CSTRING("spdy/2")); + protocolArray.AppendElement(NS_LITERAL_CSTRING("http/1.1")); + if (NS_SUCCEEDED(ssl->SetNPNList(protocolArray))) { + LOG(("nsHttpConnection::Init Setting up SPDY Negotiation OK")); + mNPNComplete = false; + } + } + } + // take ownership of the transaction mTransaction = trans; NS_ABORT_IF_FALSE(!mIdleMonitoring, "Activating a connection with an Idle Monitor"); mIdleMonitoring = false; // set mKeepAlive according to what will be requested @@ -193,16 +298,39 @@ nsHttpConnection::Activate(nsAHttpTransa failed_activation: if (NS_FAILED(rv)) { mTransaction = nsnull; } return rv; } +nsresult +nsHttpConnection::AddTransaction(nsAHttpTransaction *httpTransaction, + PRInt32 priority) +{ + LOG(("nsHttpConnection::AddTransaction for SPDY")); + + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(mSpdySession && mUsingSpdy, + "AddTransaction to live http connection without spdy"); + NS_ABORT_IF_FALSE(mTransaction, + "AddTransaction to idle http connection"); + + if (!mSpdySession->AddStream(httpTransaction, priority)) { + NS_ABORT_IF_FALSE(0, "AddStream should never fail due to" + "RoomForMore() admission check"); + return NS_ERROR_FAILURE; + } + + ResumeSend(httpTransaction); + + return NS_OK; +} + void nsHttpConnection::Close(nsresult reason) { LOG(("nsHttpConnection::Close [this=%x reason=%x]\n", this, reason)); NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); if (NS_FAILED(reason)) { @@ -232,40 +360,67 @@ nsHttpConnection::ProxyStartSSL() if (NS_FAILED(rv)) return rv; nsCOMPtr ssl = do_QueryInterface(securityInfo, &rv); if (NS_FAILED(rv)) return rv; return ssl->ProxyStartSSL(); } +void +nsHttpConnection::DontReuse() +{ + mKeepAliveMask = false; + mKeepAlive = false; + mIdleTimeout = 0; + if (mUsingSpdy) + mSpdySession->DontReuse(); +} + bool nsHttpConnection::CanReuse() { - bool canReuse = IsKeepAlive() && + bool canReuse; + + if (mUsingSpdy) + canReuse = mSpdySession->CanReuse(); + else + canReuse = IsKeepAlive(); + + canReuse = canReuse && (NowInSeconds() - mLastReadTime < mIdleTimeout) && IsAlive(); - + // An idle persistent connection should not have data waiting to be read // before a request is sent. Data here is likely a 408 timeout response // which we would deal with later on through the restart logic, but that // path is more expensive than just closing the socket now. SSL check can // be removed with fixing of 631801 PRUint32 dataSize; - if (canReuse && mSocketIn && !mConnInfo->UsingSSL() && + if (canReuse && mSocketIn && !mConnInfo->UsingSSL() && !mUsingSpdy && NS_SUCCEEDED(mSocketIn->Available(&dataSize)) && dataSize) { LOG(("nsHttpConnection::CanReuse %p %s" "Socket not reusable because read data pending (%d) on it.\n", this, mConnInfo->Host(), dataSize)); canReuse = false; } return canReuse; } +bool +nsHttpConnection::CanDirectlyActivate() +{ + // return true if a new transaction can be addded to ths connection at any + // time through Activate(). In practice this means this is a healthy SPDY + // connection with room for more concurrent streams. + + return UsingSpdy() && CanReuse() && mSpdySession->RoomForMoreStreams(); +} + PRUint32 nsHttpConnection::TimeToLive() { PRInt32 tmp = mIdleTimeout - (NowInSeconds() - mLastReadTime); if (0 > tmp) tmp = 0; return tmp; } @@ -290,16 +445,20 @@ nsHttpConnection::IsAlive() #endif return alive; } bool nsHttpConnection::SupportsPipelining(nsHttpResponseHead *responseHead) { + // SPDY supports infinite parallelism, so no need to pipeline. + if (mUsingSpdy) + return false; + // XXX there should be a strict mode available that disables this // blacklisting. // assuming connection is HTTP/1.1 with keep-alive enabled if (mConnInfo->UsingHttpProxy() && !mConnInfo->UsingSSL()) { // XXX check for bad proxy servers... return true; } @@ -414,30 +573,37 @@ nsHttpConnection::OnHeadersAvailable(nsA // reused as well as the maximum amount of time the connection can be idle // before the server will close it. we ignore the max reuse count, because // a "keep-alive" connection is by definition capable of being reused, and // we only care about being able to reuse it once. if a timeout is not // specified then we use our advertized timeout value. if (mKeepAlive) { val = responseHead->PeekHeader(nsHttp::Keep_Alive); - const char *cp = PL_strcasestr(val, "timeout="); - if (cp) - mIdleTimeout = (PRUint32) atoi(cp + 8); - else - mIdleTimeout = gHttpHandler->IdleTimeout(); + if (!mUsingSpdy) { + const char *cp = PL_strcasestr(val, "timeout="); + if (cp) + mIdleTimeout = (PRUint32) atoi(cp + 8); + else + mIdleTimeout = gHttpHandler->IdleTimeout(); + } + else { + mIdleTimeout = gHttpHandler->SpdyTimeout(); + } LOG(("Connection can be reused [this=%x idle-timeout=%u]\n", this, mIdleTimeout)); } // if we're doing an SSL proxy connect, then we need to check whether or not // the connect was successful. if so, then we have to reset the transaction // and step-up the socket connection to SSL. finally, we have to wake up the // socket write request. if (mProxyConnectStream) { + NS_ABORT_IF_FALSE(!mUsingSpdy, + "SPDY NPN Complete while using proxy connect stream"); mProxyConnectStream = 0; if (responseHead->Status() == 200) { LOG(("proxy CONNECT succeeded! ssl=%s\n", mConnInfo->UsingSSL() ? "true" :"false")); *reset = true; nsresult rv; if (mConnInfo->UsingSSL()) { rv = ProxyStartSSL(); @@ -500,16 +666,18 @@ nsHttpConnection::SetIsReusedAfter(PRUin mConsiderReusedAfterInterval = PR_MillisecondsToInterval(afterMilliseconds); } nsresult nsHttpConnection::TakeTransport(nsISocketTransport **aTransport, nsIAsyncInputStream **aInputStream, nsIAsyncOutputStream **aOutputStream) { + if (mUsingSpdy) + return NS_ERROR_FAILURE; if (mTransaction && !mTransaction->IsDone()) return NS_ERROR_IN_PROGRESS; if (!(mSocketTransport && mSocketIn && mSocketOut)) return NS_ERROR_NOT_INITIALIZED; if (mInputOverflow) mSocketIn = mInputOverflow.forget(); @@ -547,31 +715,31 @@ nsHttpConnection::PushBack(const char *d return NS_ERROR_UNEXPECTED; } mInputOverflow = new nsPreloadedStream(mSocketIn, data, length); return NS_OK; } nsresult -nsHttpConnection::ResumeSend() +nsHttpConnection::ResumeSend(nsAHttpTransaction *) { LOG(("nsHttpConnection::ResumeSend [this=%p]\n", this)); NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); if (mSocketOut) return mSocketOut->AsyncWait(this, 0, 0, nsnull); NS_NOTREACHED("no socket output stream"); return NS_ERROR_UNEXPECTED; } nsresult -nsHttpConnection::ResumeRecv() +nsHttpConnection::ResumeRecv(nsAHttpTransaction *) { LOG(("nsHttpConnection::ResumeRecv [this=%p]\n", this)); NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); if (mSocketIn) return mSocketIn->AsyncWait(this, 0, 0, nsnull); @@ -580,17 +748,18 @@ nsHttpConnection::ResumeRecv() } void nsHttpConnection::BeginIdleMonitoring() { LOG(("nsHttpConnection::BeginIdleMonitoring [this=%p]\n", this)); NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); NS_ABORT_IF_FALSE(!mTransaction, "BeginIdleMonitoring() while active"); - + NS_ABORT_IF_FALSE(!mUsingSpdy, "Idle monitoring of spdy not allowed"); + LOG(("Entering Idle Monitoring Mode [this=%p]", this)); mIdleMonitoring = true; if (mSocketIn) mSocketIn->AsyncWait(this, 0, 0, nsnull); } void nsHttpConnection::EndIdleMonitoring() @@ -622,16 +791,22 @@ nsHttpConnection::CloseTransaction(nsAHt if (mCurrentBytesRead > mMaxBytesRead) mMaxBytesRead = mCurrentBytesRead; // mask this error code because its not a real error. if (reason == NS_BASE_STREAM_CLOSED) reason = NS_OK; + if (mUsingSpdy) { + DontReuse(); + mUsingSpdy = false; + mSpdySession = nsnull; + } + mTransaction->Close(reason); mTransaction = nsnull; if (mCallbacks) { nsIInterfaceRequestor *cbs = nsnull; mCallbacks.swap(cbs); NS_ProxyRelease(mCallbackTarget, cbs); } @@ -686,30 +861,52 @@ nsHttpConnection::OnSocketWritable() { LOG(("nsHttpConnection::OnSocketWritable [this=%x]\n", this)); nsresult rv; PRUint32 n; bool again = true; do { + mSocketOutCondition = NS_OK; + // if we're doing an SSL proxy connect, then we need to bypass calling // into the transaction. // // NOTE: this code path can't be shared since the transaction doesn't // implement nsIInputStream. doing so is not worth the added cost of // extra indirections during normal reading. // if (mProxyConnectStream) { LOG((" writing CONNECT request stream\n")); rv = mProxyConnectStream->ReadSegments(ReadFromStream, this, nsIOService::gDefaultSegmentSize, &n); } + else if (!EnsureNPNComplete()) { + // When SPDY is disabled this branch is not executed because Activate() + // sets mNPNComplete to true in that case. + + // We are ready to proceed with SSL but the handshake is not done. + // When using NPN to negotiate between HTTPS and SPDY, we need to + // see the results of the handshake to know what bytes to send, so + // we cannot proceed with the request headers. + + mSocketIn->AsyncWait(this, 0, 0, nsnull); + rv = NS_OK; + mSocketOutCondition = NS_BASE_STREAM_WOULD_BLOCK; + n = 0; + } else { + if (gHttpHandler->IsSpdyEnabled() && !mReportedSpdy) { + mReportedSpdy = true; + gHttpHandler->ConnMgr()-> + ReportSpdyConnection(this, mUsingSpdy); + } + LOG((" writing transaction request stream\n")); rv = mTransaction->ReadSegments(this, nsIOService::gDefaultSegmentSize, &n); } LOG((" ReadSegments returned [rv=%x read=%u sock-cond=%x]\n", rv, n, mSocketOutCondition)); // XXX some streams return NS_BASE_STREAM_CLOSED to indicate EOF. @@ -776,16 +973,27 @@ nsHttpConnection::OnWriteSegment(char *b return mSocketInCondition; } nsresult nsHttpConnection::OnSocketReadable() { LOG(("nsHttpConnection::OnSocketReadable [this=%x]\n", this)); + if (!mNPNComplete) { + // When SPDY is disabled this branch is not executed because Activate() + // sets mNPNComplete to true in that case. + + if (!EnsureNPNComplete()) + mSocketIn->AsyncWait(this, 0, 0, nsnull); + else + return OnSocketWritable(); + return NS_OK; + } + PRUint32 now = NowInSeconds(); if (mKeepAliveMask && (now - mLastReadTime >= PRUint32(mMaxHangTime))) { LOG(("max hang time exceeded!\n")); // give the handler a chance to create a new persistent connection to // this host if we've been busy for too long. mKeepAliveMask = false; gHttpHandler->ProcessPendingQ(mConnInfo); @@ -825,16 +1033,18 @@ nsHttpConnection::OnSocketReadable() nsresult nsHttpConnection::SetupProxyConnect() { const char *val; LOG(("nsHttpConnection::SetupProxyConnect [this=%x]\n", this)); NS_ENSURE_TRUE(!mProxyConnectStream, NS_ERROR_ALREADY_INITIALIZED); + NS_ABORT_IF_FALSE(!mUsingSpdy, + "SPDY NPN Complete while using proxy connect stream"); nsCAutoString buf; nsresult rv = nsHttpHandler::GenerateHostPort( nsDependentCString(mConnInfo->Host()), mConnInfo->Port(), buf); if (NS_FAILED(rv)) return rv; // CONNECT host:port HTTP/1.1 diff --git a/netwerk/protocol/http/nsHttpConnection.h b/netwerk/protocol/http/nsHttpConnection.h --- a/netwerk/protocol/http/nsHttpConnection.h +++ b/netwerk/protocol/http/nsHttpConnection.h @@ -42,16 +42,17 @@ #include "nsHttp.h" #include "nsHttpConnectionInfo.h" #include "nsAHttpConnection.h" #include "nsAHttpTransaction.h" #include "nsXPIDLString.h" #include "nsCOMPtr.h" #include "nsAutoPtr.h" #include "prinrval.h" +#include "SpdySession.h" #include "nsIStreamListener.h" #include "nsISocketTransport.h" #include "nsIAsyncInputStream.h" #include "nsIAsyncOutputStream.h" #include "nsIInterfaceRequestor.h" #include "nsIEventTarget.h" @@ -87,35 +88,36 @@ public: // single transaction before it should no longer be kept // alive. a value of 0xffff indicates no limit. nsresult Init(nsHttpConnectionInfo *info, PRUint16 maxHangTime, nsISocketTransport *, nsIAsyncInputStream *, nsIAsyncOutputStream *, nsIInterfaceRequestor *, nsIEventTarget *); // Activate causes the given transaction to be processed on this - // connection. It fails if there is already an existing transaction. - nsresult Activate(nsAHttpTransaction *, PRUint8 caps); + // connection. It fails if there is already an existing transaction unless + // a multiplexing protocol such as SPDY is being used + nsresult Activate(nsAHttpTransaction *, PRUint8 caps, PRInt32 pri); // Close the underlying socket transport. void Close(nsresult reason); //------------------------------------------------------------------------- // XXX document when these are ok to call bool SupportsPipelining() { return mSupportsPipelining; } - bool IsKeepAlive() { return mKeepAliveMask && mKeepAlive; } + bool IsKeepAlive() { return mUsingSpdy || + (mKeepAliveMask && mKeepAlive); } bool CanReuse(); // can this connection be reused? + bool CanDirectlyActivate(); // Returns time in seconds for how long connection can be reused. PRUint32 TimeToLive(); - void DontReuse() { mKeepAliveMask = false; - mKeepAlive = false; - mIdleTimeout = 0; } + void DontReuse(); void DropTransport() { DontReuse(); mSocketTransport = 0; } bool LastTransactionExpectedNoContent() { return mLastTransactionExpectedNoContent; } void SetLastTransactionExpectedNoContent(bool val) @@ -134,43 +136,52 @@ public: nsIAsyncInputStream **, nsIAsyncOutputStream **); void GetSecurityInfo(nsISupports **); bool IsPersistent() { return IsKeepAlive(); } bool IsReused(); void SetIsReusedAfter(PRUint32 afterMilliseconds); void SetIdleTimeout(PRUint16 val) {mIdleTimeout = val;} nsresult PushBack(const char *data, PRUint32 length); - nsresult ResumeSend(); - nsresult ResumeRecv(); + nsresult ResumeSend(nsAHttpTransaction *caller); + nsresult ResumeRecv(nsAHttpTransaction *caller); PRInt64 MaxBytesRead() {return mMaxBytesRead;} static NS_METHOD ReadFromStream(nsIInputStream *, void *, const char *, PRUint32, PRUint32, PRUint32 *); // When a persistent connection is in the connection manager idle // connection pool, the nsHttpConnection still reads errors and hangups // on the socket so that it can be proactively released if the server // initiates a termination. Only call on socket thread. void BeginIdleMonitoring(); void EndIdleMonitoring(); + bool UsingSpdy() { return mUsingSpdy; } + private: // called to cause the underlying socket to start speaking SSL nsresult ProxyStartSSL(); nsresult OnTransactionDone(nsresult reason); nsresult OnSocketWritable(); nsresult OnSocketReadable(); nsresult SetupProxyConnect(); bool IsAlive(); bool SupportsPipelining(nsHttpResponseHead *); + // Makes certain the SSL handshake is complete and NPN negotiation + // has had a chance to happen + bool EnsureNPNComplete(); + + // Directly Add a transaction to an active connection for SPDY + nsresult AddTransaction(nsAHttpTransaction *, PRInt32); + private: nsCOMPtr mSocketTransport; nsCOMPtr mSocketIn; nsCOMPtr mSocketOut; nsresult mSocketInCondition; nsresult mSocketOutCondition; @@ -198,11 +209,18 @@ private: bool mKeepAlive; bool mKeepAliveMask; bool mSupportsPipelining; bool mIsReused; bool mCompletedProxyConnect; bool mLastTransactionExpectedNoContent; bool mIdleMonitoring; + + // SPDY related + bool mNPNComplete; + bool mUsingSpdy; + nsRefPtr mSpdySession; + PRInt32 mPriority; + bool mReportedSpdy; }; #endif // nsHttpConnection_h__ diff --git a/netwerk/protocol/http/nsHttpConnectionInfo.h b/netwerk/protocol/http/nsHttpConnectionInfo.h --- a/netwerk/protocol/http/nsHttpConnectionInfo.h +++ b/netwerk/protocol/http/nsHttpConnectionInfo.h @@ -120,16 +120,17 @@ public: PRInt32 Port() const { return mPort; } nsProxyInfo *ProxyInfo() { return mProxyInfo; } bool UsingHttpProxy() const { return mUsingHttpProxy; } bool UsingSSL() const { return mUsingSSL; } PRInt32 DefaultPort() const { return mUsingSSL ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT; } void SetAnonymous(bool anon) { mHashKey.SetCharAt(anon ? 'A' : '.', 2); } bool ShouldForceConnectMethod(); + const nsCString &GetHost() { return mHost; } private: nsrefcnt mRef; nsCString mHashKey; nsCString mHost; PRInt32 mPort; nsCOMPtr mProxyInfo; bool mUsingHttpProxy; diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.cpp b/netwerk/protocol/http/nsHttpConnectionMgr.cpp --- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp +++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp @@ -42,16 +42,19 @@ #include "nsHttpHandler.h" #include "nsNetCID.h" #include "nsCOMPtr.h" #include "nsNetUtil.h" #include "nsIServiceManager.h" #include "nsIObserverService.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsICancelable.h" using namespace mozilla; // defined by the socket transport service while active extern PRThread *gSocketThread; static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID); @@ -135,16 +138,17 @@ nsHttpConnectionMgr::Init(PRUint16 maxCo PRUint16 maxPersistConnsPerProxy, PRUint16 maxRequestDelay, PRUint16 maxPipelinedRequests) { LOG(("nsHttpConnectionMgr::Init\n")); { ReentrantMonitorAutoEnter mon(mReentrantMonitor); + mSpdyPreferredHash.Init(); mMaxConns = maxConns; mMaxConnsPerHost = maxConnsPerHost; mMaxConnsPerProxy = maxConnsPerProxy; mMaxPersistConnsPerHost = maxPersistConnsPerHost; mMaxPersistConnsPerProxy = maxPersistConnsPerProxy; mMaxRequestDelay = maxRequestDelay; mMaxPipelinedRequests = maxPipelinedRequests; @@ -224,18 +228,23 @@ nsHttpConnectionMgr::PruneDeadConnection mTimeOfNextWakeUp = timeInSeconds + NowInSeconds(); mTimer->Init(this, timeInSeconds*1000, nsITimer::TYPE_ONE_SHOT); } else { NS_WARNING("failed to create: timer for pruning the dead connections!"); } } void -nsHttpConnectionMgr::StopPruneDeadConnectionsTimer() +nsHttpConnectionMgr::ConditionallyStopPruneDeadConnectionsTimer() { + // Leave the timer in place if there are connections that potentially + // need management + if (mNumIdleConns || (mNumActiveConns && gHttpHandler->IsSpdyEnabled())) + return; + LOG(("nsHttpConnectionMgr::StopPruneDeadConnectionsTimer\n")); // Reset mTimeOfNextWakeUp so that we can find a new shortest value. mTimeOfNextWakeUp = LL_MAXUINT; if (mTimer) { mTimer->Cancel(); mTimer = NULL; } @@ -403,21 +412,101 @@ nsHttpConnectionMgr::CloseIdleConnection nsConnectionEntry *ent = mCT.Get(ci->HashKey()); if (!ent || !ent->mIdleConns.RemoveElement(conn)) return NS_ERROR_UNEXPECTED; conn->Close(NS_ERROR_ABORT); NS_RELEASE(conn); mNumIdleConns--; - if (0 == mNumIdleConns) - StopPruneDeadConnectionsTimer(); + ConditionallyStopPruneDeadConnectionsTimer(); return NS_OK; } +void +nsHttpConnectionMgr::ReportSpdyConnection(nsHttpConnection *conn, + bool usingSpdy) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + nsConnectionEntry *ent = mCT.Get(conn->ConnectionInfo()->HashKey()); + NS_ABORT_IF_FALSE(ent, "no connection entry"); + if (!ent) + return; + + ent->mTestedSpdy = true; + + if (!usingSpdy) { + if (ent->mUsingSpdy) + conn->DontReuse(); + return; + } + + ent->mUsingSpdy = true; + + PRUint32 ttl = conn->TimeToLive(); + PRUint64 timeOfExpire = NowInSeconds() + ttl; + if (!mTimer || timeOfExpire < mTimeOfNextWakeUp) + PruneDeadConnectionsAfter(ttl); + + nsConnectionEntry *preferred = GetSpdyPreferred(ent->mDottedDecimalAddress); + LOG(("ReportSpdyConnection %s %s ent=%p ispreferred=%d\n", + ent->mConnInfo->Host(), ent->mDottedDecimalAddress.get(), + ent, preferred)); + + if (!preferred) { + ent->mSpdyPreferred = true; + SetSpdyPreferred(ent->mDottedDecimalAddress, ent); + ent->mSpdyRedir = false; + } + else if (preferred != ent) { + // A different hostname is the preferred spdy host for this + // IP address. + ent->mSpdyRedir = true; + conn->DontReuse(); + } + ProcessSpdyPendingQ(); +} + +nsHttpConnectionMgr::nsConnectionEntry * +nsHttpConnectionMgr::GetSpdyPreferred(nsACString &aDottedDecimal) +{ + if (!gHttpHandler->IsSpdyEnabled() || + !gHttpHandler->CoalesceSpdy() || + aDottedDecimal.IsEmpty()) + return nsnull; + + return mSpdyPreferredHash.Get(aDottedDecimal); +} + +void +nsHttpConnectionMgr::SetSpdyPreferred(nsACString &aDottedDecimal, + nsConnectionEntry *ent) +{ + if (!gHttpHandler->CoalesceSpdy()) + return; + + if (aDottedDecimal.IsEmpty()) + return; + + mSpdyPreferredHash.Put(aDottedDecimal, ent); +} + +void +nsHttpConnectionMgr::RemoveSpdyPreferred(nsACString &aDottedDecimal) +{ + if (!gHttpHandler->CoalesceSpdy()) + return; + + if (aDottedDecimal.IsEmpty()) + return; + + mSpdyPreferredHash.Remove(aDottedDecimal); +} + //----------------------------------------------------------------------------- // enumeration callbacks PLDHashOperator nsHttpConnectionMgr::ProcessOneTransactionCB(const nsACString &key, nsAutoPtr &ent, void *closure) { @@ -444,79 +533,101 @@ nsHttpConnectionMgr::PurgeExcessIdleConn // There are no idle conns left in this connection entry return PL_DHASH_NEXT; } nsHttpConnection *conn = ent->mIdleConns[0]; ent->mIdleConns.RemoveElementAt(0); conn->Close(NS_ERROR_ABORT); NS_RELEASE(conn); self->mNumIdleConns--; - if (0 == self->mNumIdleConns) - self->StopPruneDeadConnectionsTimer(); + self->ConditionallyStopPruneDeadConnectionsTimer(); } return PL_DHASH_STOP; } PLDHashOperator nsHttpConnectionMgr::PruneDeadConnectionsCB(const nsACString &key, nsAutoPtr &ent, void *closure) { nsHttpConnectionMgr *self = (nsHttpConnectionMgr *) closure; LOG((" pruning [ci=%s]\n", ent->mConnInfo->HashKey().get())); // Find out how long it will take for next idle connection to not be reusable // anymore. + bool liveConnections = false; PRUint32 timeToNextExpire = PR_UINT32_MAX; PRInt32 count = ent->mIdleConns.Length(); if (count > 0) { for (PRInt32 i=count-1; i>=0; --i) { nsHttpConnection *conn = ent->mIdleConns[i]; if (!conn->CanReuse()) { ent->mIdleConns.RemoveElementAt(i); conn->Close(NS_ERROR_ABORT); NS_RELEASE(conn); self->mNumIdleConns--; } else { timeToNextExpire = NS_MIN(timeToNextExpire, conn->TimeToLive()); + liveConnections = true; } } } + if (ent->mUsingSpdy) { + for (PRUint32 index = 0; index < ent->mActiveConns.Length(); ++index) { + nsHttpConnection *conn = ent->mActiveConns[index]; + if (conn->UsingSpdy()) { + if (!conn->CanReuse()) { + // marking it dont reuse will create an active tear down if + // the spdy session is idle. + conn->DontReuse(); + } + else { + timeToNextExpire = NS_MIN(timeToNextExpire, + conn->TimeToLive()); + liveConnections = true; + } + } + } + } + // If time to next expire found is shorter than time to next wake-up, we need to // change the time for next wake-up. - PRUint32 now = NowInSeconds(); - if (0 < ent->mIdleConns.Length()) { + if (liveConnections) { + PRUint32 now = NowInSeconds(); PRUint64 timeOfNextExpire = now + timeToNextExpire; // If pruning of dead connections is not already scheduled to happen // or time found for next connection to expire is is before // mTimeOfNextWakeUp, we need to schedule the pruning to happen // after timeToNextExpire. if (!self->mTimer || timeOfNextExpire < self->mTimeOfNextWakeUp) { self->PruneDeadConnectionsAfter(timeToNextExpire); } - } else if (0 == self->mNumIdleConns) { - self->StopPruneDeadConnectionsTimer(); + } else { + self->ConditionallyStopPruneDeadConnectionsTimer(); } #ifdef DEBUG count = ent->mActiveConns.Length(); if (count > 0) { for (PRInt32 i=count-1; i>=0; --i) { nsHttpConnection *conn = ent->mActiveConns[i]; LOG((" active conn [%x] with trans [%x]\n", conn, conn->Transaction())); } } #endif // if this entry is empty, then we can remove it. if (ent->mIdleConns.Length() == 0 && ent->mActiveConns.Length() == 0 && ent->mHalfOpens.Length() == 0 && - ent->mPendingQ.Length() == 0) { + ent->mPendingQ.Length() == 0 && + ((!ent->mTestedSpdy && !ent->mUsingSpdy) || + !gHttpHandler->IsSpdyEnabled() || + self->mCT.Count() > 300)) { LOG((" removing empty connection entry\n")); return PL_DHASH_REMOVE; } // else, use this opportunity to compact our arrays... ent->mIdleConns.Compact(); ent->mActiveConns.Compact(); ent->mPendingQ.Compact(); @@ -552,18 +663,17 @@ nsHttpConnectionMgr::ShutdownPassCB(cons ent->mIdleConns.RemoveElementAt(0); self->mNumIdleConns--; conn->Close(NS_ERROR_ABORT); NS_RELEASE(conn); } // If all idle connections are removed, // we can stop pruning dead connections. - if (0 == self->mNumIdleConns) - self->StopPruneDeadConnectionsTimer(); + self->ConditionallyStopPruneDeadConnectionsTimer(); // close all pending transactions while (ent->mPendingQ.Length()) { trans = ent->mPendingQ[0]; ent->mPendingQ.RemoveElementAt(0); trans->Close(NS_ERROR_ABORT); @@ -577,25 +687,29 @@ nsHttpConnectionMgr::ShutdownPassCB(cons return PL_DHASH_REMOVE; } //----------------------------------------------------------------------------- bool nsHttpConnectionMgr::ProcessPendingQForEntry(nsConnectionEntry *ent) { + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); LOG(("nsHttpConnectionMgr::ProcessPendingQForEntry [ci=%s]\n", ent->mConnInfo->HashKey().get())); - PRInt32 i, count = ent->mPendingQ.Length(); + if (gHttpHandler->IsSpdyEnabled()) + ProcessSpdyPendingQ(ent); + + PRUint32 i, count = ent->mPendingQ.Length(); if (count > 0) { LOG((" pending-count=%u\n", count)); nsHttpTransaction *trans = nsnull; nsHttpConnection *conn = nsnull; - for (i=0; imPendingQ[i]; // When this transaction has already established a half-open // connection, we want to prevent any duplicate half-open // connections from being established and bound to this // transaction. Allow only use of an idle persistent connection // (if found) for transactions referred by a half-open connection. bool alreadyHalfOpen = false; @@ -604,16 +718,23 @@ nsHttpConnectionMgr::ProcessPendingQForE alreadyHalfOpen = true; break; } } GetConnection(ent, trans, alreadyHalfOpen, &conn); if (conn) break; + + // Check to see if a pending transaction was dispatched with the + // coalesce logic + if (count != ent->mPendingQ.Length()) { + count = ent->mPendingQ.Length(); + i = 0; + } } if (conn) { LOG((" dispatching pending transaction...\n")); // remove pending transaction ent->mPendingQ.RemoveElementAt(i); nsresult rv = DispatchTransaction(ent, trans, trans->Caps(), conn); @@ -731,26 +852,33 @@ void nsHttpConnectionMgr::GetConnection(nsConnectionEntry *ent, nsHttpTransaction *trans, bool onlyReusedConnection, nsHttpConnection **result) { LOG(("nsHttpConnectionMgr::GetConnection [ci=%s caps=%x]\n", ent->mConnInfo->HashKey().get(), PRUint32(trans->Caps()))); - // First, see if an idle persistent connection may be reused instead of + // First, see if an existing connection can be used - either an idle + // persistent connection or an active spdy session may be reused instead of // establishing a new socket. We do not need to check the connection limits // yet as they govern the maximum number of open connections and reusing // an old connection never increases that. *result = nsnull; nsHttpConnection *conn = nsnull; + bool addConnToActiveList = true; if (trans->Caps() & NS_HTTP_ALLOW_KEEPALIVE) { + + conn = GetSpdyPreferredConn(ent); + if (conn) + addConnToActiveList = false; + // search the idle connection list. Each element in the list // has a reference, so if we remove it from the list into a local // ptr, that ptr now owns the reference while (!conn && (ent->mIdleConns.Length() > 0)) { conn = ent->mIdleConns[0]; // we check if the connection can be reused before even checking if // it is a "matching" connection. if (!conn->CanReuse()) { @@ -763,28 +891,46 @@ nsHttpConnectionMgr::GetConnection(nsCon conn->EndIdleMonitoring(); } ent->mIdleConns.RemoveElementAt(0); mNumIdleConns--; // If there are no idle connections left at all, we need to make // sure that we are not pruning dead connections anymore. - if (0 == mNumIdleConns) - StopPruneDeadConnectionsTimer(); + ConditionallyStopPruneDeadConnectionsTimer(); } } if (!conn) { // If the onlyReusedConnection parameter is TRUE, then GetConnection() // does not create new transports under any circumstances. if (onlyReusedConnection) return; + // If this is a possible Spdy connection we need to limit the number of + // connections outstanding to 1 while we wait for the spdy/https + // ReportSpdyConnection() + + if (gHttpHandler->IsSpdyEnabled() && + ent->mConnInfo->UsingSSL() && + !ent->mConnInfo->UsingHttpProxy()) + { + nsConnectionEntry *preferred = + GetSpdyPreferred(ent->mDottedDecimalAddress); + if (preferred) + ent = preferred; + + if ((!ent->mTestedSpdy || ent->mUsingSpdy) && + (ent->mSpdyRedir || ent->mHalfOpens.Length() || + ent->mActiveConns.Length())) + return; + } + // Check if we need to purge an idle connection. Note that we may have // removed one above; if so, this will be a no-op. We do this before // checking the active connection limit to catch the case where we do // have an idle connection, but the purge timer hasn't fired yet. // XXX this just purges a random idle connection. we should instead // enumerate the entire hash table to find the eldest idle connection. if (mNumIdleConns && mNumIdleConns + mNumActiveConns + 1 >= mMaxConns) mCT.Enumerate(PurgeExcessIdleConnectionsCB, this); @@ -794,27 +940,34 @@ nsHttpConnectionMgr::GetConnection(nsCon // host or proxy. If we have, we're done. if (AtActiveConnectionLimit(ent, trans->Caps())) { LOG(("nsHttpConnectionMgr::GetConnection [ci = %s]" "at active connection limit - will queue\n", ent->mConnInfo->HashKey().get())); return; } + LOG(("nsHttpConnectionMgr::GetConnection Open Connection " + "%s %s ent=%p spdy=%d", + ent->mConnInfo->Host(), ent->mDottedDecimalAddress.get(), + ent, ent->mUsingSpdy)); + nsresult rv = CreateTransport(ent, trans); if (NS_FAILED(rv)) trans->Close(rv); return; } - // hold an owning ref to this connection - ent->mActiveConns.AppendElement(conn); - mNumActiveConns++; + if (addConnToActiveList) { + // hold an owning ref to this connection + ent->mActiveConns.AppendElement(conn); + mNumActiveConns++; + } + NS_ADDREF(conn); - *result = conn; } void nsHttpConnectionMgr::AddActiveConn(nsHttpConnection *conn, nsConnectionEntry *ent) { NS_ADDREF(conn); @@ -846,39 +999,50 @@ nsHttpConnectionMgr::CreateTransport(nsC sock->SetupBackupTimer(); ent->mHalfOpens.AppendElement(sock); return NS_OK; } nsresult nsHttpConnectionMgr::DispatchTransaction(nsConnectionEntry *ent, - nsAHttpTransaction *trans, + nsHttpTransaction *aTrans, PRUint8 caps, nsHttpConnection *conn) { LOG(("nsHttpConnectionMgr::DispatchTransaction [ci=%s trans=%x caps=%x conn=%x]\n", - ent->mConnInfo->HashKey().get(), trans, caps, conn)); + ent->mConnInfo->HashKey().get(), aTrans, caps, conn)); + nsresult rv; + + PRInt32 priority = aTrans->Priority(); + + if (conn->UsingSpdy()) { + rv = conn->Activate(aTrans, caps, priority); + NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv), "SPDY Cannot Fail Dispatch"); + return rv; + } nsConnectionHandle *handle = new nsConnectionHandle(conn); if (!handle) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(handle); nsHttpPipeline *pipeline = nsnull; + nsAHttpTransaction *trans = aTrans; + if (conn->SupportsPipelining() && (caps & NS_HTTP_ALLOW_PIPELINING)) { LOG((" looking to build pipeline...\n")); if (BuildPipeline(ent, trans, &pipeline)) trans = pipeline; } // give the transaction the indirect reference to the connection. trans->SetConnection(handle); - nsresult rv = conn->Activate(trans, caps); + rv = conn->Activate(trans, caps, priority); if (NS_FAILED(rv)) { LOG((" conn->Activate failed [rv=%x]\n", rv)); ent->mActiveConns.RemoveElement(conn); mNumActiveConns--; // sever back references to connection, and do so without triggering // a call to ReclaimConnection ;-) trans->SetConnection(nsnull); @@ -963,16 +1127,27 @@ nsHttpConnectionMgr::ProcessNewTransacti if (!clone) return NS_ERROR_OUT_OF_MEMORY; ent = new nsConnectionEntry(clone); if (!ent) return NS_ERROR_OUT_OF_MEMORY; mCT.Put(ci->HashKey(), ent); } + // SPDY coalescing of hostnames means we might redirect from this + // connection entry onto the preferred one. + nsConnectionEntry *preferredEntry = + GetSpdyPreferred(ent->mDottedDecimalAddress); + if (preferredEntry) { + LOG(("nsHttpConnectionMgr::ProcessNewTransaction trans=%p " + "redirected via coalescing from %s to %s\n", trans, + ent->mConnInfo->Host(), preferredEntry->mConnInfo->Host())); + ent = preferredEntry; + } + // If we are doing a force reload then close out any existing conns // to this host so that changes in DNS, LBs, etc.. are reflected if (caps & NS_HTTP_CLEAR_KEEPALIVES) ClosePersistentConnections(ent); // Check if the transaction already has a sticky reference to a connection. // If so, then we can just use it directly by transferring its reference // to the new connection var instead of calling GetConnection() to search @@ -1002,16 +1177,94 @@ nsHttpConnectionMgr::ProcessNewTransacti else { rv = DispatchTransaction(ent, trans, caps, conn); NS_RELEASE(conn); } return rv; } +void +nsHttpConnectionMgr::ProcessSpdyPendingQ(nsConnectionEntry *ent) +{ + nsHttpConnection *conn = GetSpdyPreferredConn(ent); + if (!conn) + return; + + for (PRInt32 index = ent->mPendingQ.Length() - 1; + index >= 0 && conn->CanDirectlyActivate(); + --index) { + nsHttpTransaction *trans = ent->mPendingQ[index]; + + if (!(trans->Caps() & NS_HTTP_ALLOW_KEEPALIVE) || + trans->Caps() & NS_HTTP_DISALLOW_SPDY) + continue; + + ent->mPendingQ.RemoveElementAt(index); + + nsresult rv2 = DispatchTransaction(ent, trans, trans->Caps(), conn); + NS_ABORT_IF_FALSE(NS_SUCCEEDED(rv2), "Dispatch SPDY Transaction"); + NS_RELEASE(trans); + } +} + +PLDHashOperator +nsHttpConnectionMgr::ProcessSpdyPendingQCB(const nsACString &key, + nsAutoPtr &ent, + void *closure) +{ + nsHttpConnectionMgr *self = (nsHttpConnectionMgr *) closure; + self->ProcessSpdyPendingQ(ent); + return PL_DHASH_NEXT; +} + +void +nsHttpConnectionMgr::ProcessSpdyPendingQ() +{ + mCT.Enumerate(ProcessSpdyPendingQCB, this); +} + +nsHttpConnection * +nsHttpConnectionMgr::GetSpdyPreferredConn(nsConnectionEntry *ent) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + NS_ABORT_IF_FALSE(ent, "no connection entry"); + + nsConnectionEntry *preferred = GetSpdyPreferred(ent->mDottedDecimalAddress); + + // this entry is spdy-enabled if it is a redirect to another spdy host + if (preferred && preferred != ent) { + ent->mUsingSpdy = true; + ent->mSpdyRedir = true; + } + else { + ent->mSpdyRedir = false; + // don't clear usingSpdy, that will be reset in ReportSpdyConnection + // if it no longer applies + } + + if (!preferred) + preferred = ent; + + nsHttpConnection *conn = nsnull; + + if (preferred->mUsingSpdy) { + for (PRUint32 index = 0; + index < preferred->mActiveConns.Length(); + ++index) { + if (preferred->mActiveConns[index]->CanDirectlyActivate()) { + conn = preferred->mActiveConns[index]; + break; + } + } + } + + return conn; +} + //----------------------------------------------------------------------------- void nsHttpConnectionMgr::OnMsgShutdown(PRInt32, void *) { LOG(("nsHttpConnectionMgr::OnMsgShutdown\n")); mCT.Enumerate(ShutdownPassCB, this); @@ -1105,17 +1358,20 @@ nsHttpConnectionMgr::OnMsgProcessPending void nsHttpConnectionMgr::OnMsgPruneDeadConnections(PRInt32, void *) { LOG(("nsHttpConnectionMgr::OnMsgPruneDeadConnections\n")); // Reset mTimeOfNextWakeUp so that we can find a new shortest value. mTimeOfNextWakeUp = LL_MAXUINT; - if (mNumIdleConns > 0) + + // check canreuse() for all idle connections plus any active connections on + // connection entries that are using spdy. + if (mNumIdleConns || (mNumActiveConns && gHttpHandler->IsSpdyEnabled())) mCT.Enumerate(PruneDeadConnectionsCB, this); } void nsHttpConnectionMgr::OnMsgClosePersistentConnections(PRInt32, void *) { LOG(("nsHttpConnectionMgr::OnMsgClosePersistentConnections\n")); @@ -1141,16 +1397,20 @@ nsHttpConnectionMgr::OnMsgReclaimConnect nsConnectionEntry *ent = mCT.Get(ci->HashKey()); NS_ASSERTION(ent, "no connection entry"); if (ent) { // If the connection is in the active list, remove that entry // and the reference held by the mActiveConns list. // This is never the final reference on conn as the event context // is also holding one that is released at the end of this function. + + if (ent->mUsingSpdy) + conn->DontReuse(); + if (ent->mActiveConns.RemoveElement(conn)) { nsHttpConnection *temp = conn; NS_RELEASE(temp); mNumActiveConns--; } if (conn->CanReuse()) { LOG((" adding connection to idle list\n")); @@ -1169,17 +1429,17 @@ nsHttpConnectionMgr::OnMsgReclaimConnect } NS_ADDREF(conn); ent->mIdleConns.InsertElementAt(idx, conn); mNumIdleConns++; conn->BeginIdleMonitoring(); // If the added connection was first idle connection or has shortest - // time to live among the idle connections, pruning dead + // time to live among the watched connections, pruning dead // connections needs to be done when it can't be reused anymore. PRUint32 timeToLive = conn->TimeToLive(); if(!mTimer || NowInSeconds() + timeToLive < mTimeOfNextWakeUp) PruneDeadConnectionsAfter(timeToLive); } else { LOG((" connection cannot be reused; closing connection\n")); // make sure the connection is closed and release our reference. @@ -1219,16 +1479,25 @@ nsHttpConnectionMgr::OnMsgUpdateParam(PR case MAX_PIPELINED_REQUESTS: mMaxPipelinedRequests = value; break; default: NS_NOTREACHED("unexpected parameter name"); } } +// nsHttpConnectionMgr::nsConnectionEntry +nsHttpConnectionMgr::nsConnectionEntry::~nsConnectionEntry() +{ + if (mSpdyPreferred) + gHttpHandler->ConnMgr()->RemoveSpdyPreferred(mDottedDecimalAddress); + + NS_RELEASE(mConnInfo); +} + //----------------------------------------------------------------------------- // nsHttpConnectionMgr::nsConnectionHandle nsHttpConnectionMgr::nsConnectionHandle::~nsConnectionHandle() { if (mConn) { gHttpHandler->ReclaimConnection(mConn); NS_RELEASE(mConn); @@ -1242,25 +1511,25 @@ nsHttpConnectionMgr::nsConnectionHandle: nsHttpRequestHead *req, nsHttpResponseHead *resp, bool *reset) { return mConn->OnHeadersAvailable(trans, req, resp, reset); } nsresult -nsHttpConnectionMgr::nsConnectionHandle::ResumeSend() +nsHttpConnectionMgr::nsConnectionHandle::ResumeSend(nsAHttpTransaction *caller) { - return mConn->ResumeSend(); + return mConn->ResumeSend(caller); } nsresult -nsHttpConnectionMgr::nsConnectionHandle::ResumeRecv() +nsHttpConnectionMgr::nsConnectionHandle::ResumeRecv(nsAHttpTransaction *caller) { - return mConn->ResumeRecv(); + return mConn->ResumeRecv(caller); } void nsHttpConnectionMgr::nsConnectionHandle::CloseTransaction(nsAHttpTransaction *trans, nsresult reason) { mConn->CloseTransaction(trans, reason); } @@ -1412,20 +1681,45 @@ nsHalfOpenSocket::SetupStreams(nsISocket gHttpHandler->ConnMgr()->StartedConnect(); return rv; } nsresult nsHttpConnectionMgr::nsHalfOpenSocket::SetupPrimaryStreams() { - nsresult rv = SetupStreams(getter_AddRefs(mSocketTransport), - getter_AddRefs(mStreamIn), - getter_AddRefs(mStreamOut), - false); + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + nsresult rv; + if (gHttpHandler->IsSpdyEnabled() && + gHttpHandler->CoalesceSpdy() && + mEnt->mConnInfo->UsingSSL() && + !mEnt->mConnInfo->UsingHttpProxy() && + !mEnt->mDidDNS) { + + // Grab the DNS resolution for this host for un-sharding purposes + // This lookup will no doubt be coalesced by the DNS sub system as it + // overlaps with the SocketTransport + + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID, + &rv); + mEnt->mDidDNS = true; + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr cancelable; + dns->AsyncResolve(mEnt->mConnInfo->GetHost(), 0, this, + NS_GetCurrentThread(), + getter_AddRefs(cancelable)); + } + } + + rv = SetupStreams(getter_AddRefs(mSocketTransport), + getter_AddRefs(mStreamIn), + getter_AddRefs(mStreamOut), + false); LOG(("nsHalfOpenSocket::SetupPrimaryStream [this=%p ent=%s rv=%x]", this, mEnt->mConnInfo->Host(), rv)); if (NS_FAILED(rv)) { if (mStreamOut) mStreamOut->AsyncWait(nsnull, 0, 0, nsnull); mStreamOut = nsnull; mStreamIn = nsnull; mSocketTransport = nsnull; @@ -1447,21 +1741,38 @@ nsHttpConnectionMgr::nsHalfOpenSocket::S mBackupStreamOut->AsyncWait(nsnull, 0, 0, nsnull); mBackupStreamOut = nsnull; mBackupStreamIn = nsnull; mBackupTransport = nsnull; } return rv; } + +NS_IMETHODIMP // nsIDNSListener +nsHttpConnectionMgr:: +nsHalfOpenSocket::OnLookupComplete(nsICancelable *aRequest, + nsIDNSRecord *aRecord, + nsresult aStatus) +{ + NS_ABORT_IF_FALSE(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + + if (NS_SUCCEEDED(aStatus) && mEnt) { + aRecord->GetNextAddrAsString(mEnt->mDottedDecimalAddress); + gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(mEnt); + } + return NS_OK; +} + void nsHttpConnectionMgr::nsHalfOpenSocket::SetupBackupTimer() { PRUint16 timeout = gHttpHandler->GetIdleSynTimeout(); NS_ABORT_IF_FALSE(!mSynTimer, "timer already initd"); + if (timeout) { // Setup the timer that will establish a backup socket // if we do not get a writable event on the main one. // We do this because a lost SYN takes a very long time // to repair at the TCP level. // // Failure to setup the timer is something we can live with, // so don't return an error in that case. diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.h b/netwerk/protocol/http/nsHttpConnectionMgr.h --- a/netwerk/protocol/http/nsHttpConnectionMgr.h +++ b/netwerk/protocol/http/nsHttpConnectionMgr.h @@ -40,19 +40,21 @@ #define nsHttpConnectionMgr_h__ #include "nsHttpConnectionInfo.h" #include "nsHttpConnection.h" #include "nsHttpTransaction.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "nsClassHashtable.h" +#include "nsDataHashtable.h" #include "nsAutoPtr.h" #include "mozilla/ReentrantMonitor.h" #include "nsISocketTransportService.h" +#include "nsIDNSListener.h" #include "nsIObserver.h" #include "nsITimer.h" class nsHttpPipeline; //----------------------------------------------------------------------------- @@ -91,18 +93,19 @@ public: //------------------------------------------------------------------------- // NOTE: functions below may be called on any thread. //------------------------------------------------------------------------- // Schedules next pruning of dead connection to happen after // given time. void PruneDeadConnectionsAfter(PRUint32 time); - // Stops timer scheduled for next pruning of dead connections. - void StopPruneDeadConnectionsTimer(); + // Stops timer scheduled for next pruning of dead connections if + // there are no more idle connections or active spdy ones + void ConditionallyStopPruneDeadConnectionsTimer(); // adds a transaction to the list of managed transactions. nsresult AddTransaction(nsHttpTransaction *, PRInt32 priority); // called to reschedule the given transaction. it must already have been // added to the connection manager via AddTransaction. nsresult RescheduleTransaction(nsHttpTransaction *, PRInt32 priority); @@ -142,40 +145,65 @@ public: // preference to the specified connection. nsresult ProcessPendingQ(nsHttpConnectionInfo *); // This is used to force an idle connection to be closed and removed from // the idle connection list. It is called when the idle connection detects // that the network peer has closed the transport. nsresult CloseIdleConnection(nsHttpConnection *); + // The connection manager needs to know when a normal HTTP connection has been + // upgraded to SPDY because the dispatch and idle semantics are a little + // bit different. + void ReportSpdyConnection(nsHttpConnection *, bool usingSpdy); + private: virtual ~nsHttpConnectionMgr(); class nsHalfOpenSocket; // nsConnectionEntry // // mCT maps connection info hash key to nsConnectionEntry object, which // contains list of active and idle connections as well as the list of // pending transactions. // struct nsConnectionEntry { nsConnectionEntry(nsHttpConnectionInfo *ci) - : mConnInfo(ci) + : mConnInfo(ci), + mUsingSpdy(false), + mTestedSpdy(false), + mSpdyRedir(false), + mDidDNS(false), + mSpdyPreferred(false) { NS_ADDREF(mConnInfo); } - ~nsConnectionEntry() { NS_RELEASE(mConnInfo); } + ~nsConnectionEntry(); nsHttpConnectionInfo *mConnInfo; nsTArray mPendingQ; // pending transaction queue nsTArray mActiveConns; // active connections nsTArray mIdleConns; // idle persistent connections nsTArray mHalfOpens; + + // Spdy sometimes resolves the address in the socket manager in order + // to re-coalesce sharded HTTP hosts. + // + // When a set of hosts are coalesced together one of them is marked + // mSpdyPreferred, and the others are marked mSpdyRedir. The mapping is + // maintained in the conection manager mSpdyPreferred hash. + // + nsCString mDottedDecimalAddress; + + bool mUsingSpdy; + bool mTestedSpdy; + bool mSpdyRedir; + bool mDidDNS; + bool mSpdyPreferred; }; // nsConnectionHandle // // thin wrapper around a real connection, used to keep track of references // to the connection to determine when the connection may be reused. the // transaction (or pipeline) owns a reference to this handle. this extra // layer of indirection greatly simplifies consumer code, avoiding the @@ -195,24 +223,26 @@ private: }; // nsHalfOpenSocket is used to hold the state of an opening TCP socket // while we wait for it to establish and bind it to a connection class nsHalfOpenSocket : public nsIOutputStreamCallback, public nsITransportEventSink, public nsIInterfaceRequestor, - public nsITimerCallback + public nsITimerCallback, + public nsIDNSListener { public: NS_DECL_ISUPPORTS NS_DECL_NSIOUTPUTSTREAMCALLBACK NS_DECL_NSITRANSPORTEVENTSINK NS_DECL_NSIINTERFACEREQUESTOR NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIDNSLISTENER nsHalfOpenSocket(nsConnectionEntry *ent, nsHttpTransaction *trans); ~nsHalfOpenSocket(); nsresult SetupStreams(nsISocketTransport **, nsIAsyncInputStream **, nsIAsyncOutputStream **, @@ -267,27 +297,41 @@ private: static PLDHashOperator PruneDeadConnectionsCB(const nsACString &, nsAutoPtr &, void *); static PLDHashOperator ShutdownPassCB(const nsACString &, nsAutoPtr &, void *); static PLDHashOperator PurgeExcessIdleConnectionsCB(const nsACString &, nsAutoPtr &, void *); static PLDHashOperator ClosePersistentConnectionsCB(const nsACString &, nsAutoPtr &, void *); bool ProcessPendingQForEntry(nsConnectionEntry *); bool AtActiveConnectionLimit(nsConnectionEntry *, PRUint8 caps); void GetConnection(nsConnectionEntry *, nsHttpTransaction *, bool, nsHttpConnection **); - nsresult DispatchTransaction(nsConnectionEntry *, nsAHttpTransaction *, + nsresult DispatchTransaction(nsConnectionEntry *, nsHttpTransaction *, PRUint8 caps, nsHttpConnection *); bool BuildPipeline(nsConnectionEntry *, nsAHttpTransaction *, nsHttpPipeline **); nsresult ProcessNewTransaction(nsHttpTransaction *); nsresult EnsureSocketThreadTargetIfOnline(); void ClosePersistentConnections(nsConnectionEntry *ent); nsresult CreateTransport(nsConnectionEntry *, nsHttpTransaction *); void AddActiveConn(nsHttpConnection *, nsConnectionEntry *); void StartedConnect(); void RecvdConnect(); - + + // Manage the preferred spdy connection entry for this address + nsConnectionEntry *GetSpdyPreferred(nsACString &aDottedDecimal); + void SetSpdyPreferred(nsACString &aDottedDecimal, + nsConnectionEntry *ent); + void RemoveSpdyPreferred(nsACString &aDottedDecimal); + nsHttpConnection *GetSpdyPreferredConn(nsConnectionEntry *ent); + nsDataHashtable mSpdyPreferredHash; + + void ProcessSpdyPendingQ(nsConnectionEntry *ent); + void ProcessSpdyPendingQ(); + static PLDHashOperator ProcessSpdyPendingQCB( + const nsACString &key, nsAutoPtr &ent, + void *closure); + // message handlers have this signature typedef void (nsHttpConnectionMgr:: *nsConnEventHandler)(PRInt32, void *); // nsConnEvent // // subclass of nsRunnable used to marshall events to the socket transport // thread. this class is used to implement PostEvent. // diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp --- a/netwerk/protocol/http/nsHttpHandler.cpp +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -169,16 +169,17 @@ nsHttpHandler::nsHttpHandler() : mConnMgr(nsnull) , mHttpVersion(NS_HTTP_VERSION_1_1) , mProxyHttpVersion(NS_HTTP_VERSION_1_1) , mCapabilities(NS_HTTP_ALLOW_KEEPALIVE) , mProxyCapabilities(NS_HTTP_ALLOW_KEEPALIVE) , mReferrerLevel(0xff) // by default we always send a referrer , mFastFallbackToIPv4(false) , mIdleTimeout(10) + , mSpdyTimeout(180) , mMaxRequestAttempts(10) , mMaxRequestDelay(10) , mIdleSynTimeout(250) , mMaxConnections(24) , mMaxConnectionsPerServer(8) , mMaxPersistentConnectionsPerServer(2) , mMaxPersistentConnectionsPerProxy(4) , mMaxPipelinedRequests(2) @@ -193,16 +194,18 @@ nsHttpHandler::nsHttpHandler() , mLegacyAppVersion("5.0") , mProduct("Gecko") , mUserAgentIsDirty(true) , mUseCache(true) , mPromptTempRedirect(true) , mSendSecureXSiteReferrer(true) , mEnablePersistentHttpsCaching(false) , mDoNotTrackEnabled(false) + , mEnableSpdy(false) + , mCoalesceSpdy(true) { #if defined(PR_LOGGING) gHttpLog = PR_NewLogModule("nsHttp"); #endif LOG(("Creating nsHttpHandler [this=%x].\n", this)); NS_ASSERTION(!gHttpHandler, "HTTP handler already created!"); @@ -1079,16 +1082,34 @@ nsHttpHandler::PrefsChanged(nsIPrefBranc } if (PREF_CHANGED(HTTP_PREF("phishy-userpass-length"))) { rv = prefs->GetIntPref(HTTP_PREF("phishy-userpass-length"), &val); if (NS_SUCCEEDED(rv)) mPhishyUserPassLength = (PRUint8) clamped(val, 0, 0xff); } + if (PREF_CHANGED(HTTP_PREF("spdy.enabled"))) { + rv = prefs->GetBoolPref(HTTP_PREF("spdy.enabled"), &cVar); + if (NS_SUCCEEDED(rv)) + mEnableSpdy = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.coalesce-hostnames"))) { + rv = prefs->GetBoolPref(HTTP_PREF("spdy.coalesce-hostnames"), &cVar); + if (NS_SUCCEEDED(rv)) + mCoalesceSpdy = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.timeout"))) { + rv = prefs->GetIntPref(HTTP_PREF("spdy.timeout"), &val); + if (NS_SUCCEEDED(rv)) + mSpdyTimeout = (PRUint16) clamped(val, 1, 0xffff); + } + // // INTL options // if (PREF_CHANGED(INTL_ACCEPT_LANGUAGES)) { nsCOMPtr pls; prefs->GetComplexValue(INTL_ACCEPT_LANGUAGES, NS_GET_IID(nsIPrefLocalizedString), diff --git a/netwerk/protocol/http/nsHttpHandler.h b/netwerk/protocol/http/nsHttpHandler.h --- a/netwerk/protocol/http/nsHttpHandler.h +++ b/netwerk/protocol/http/nsHttpHandler.h @@ -96,27 +96,31 @@ public: const nsAFlatCString &UserAgent(); nsHttpVersion HttpVersion() { return mHttpVersion; } nsHttpVersion ProxyHttpVersion() { return mProxyHttpVersion; } PRUint8 ReferrerLevel() { return mReferrerLevel; } bool SendSecureXSiteReferrer() { return mSendSecureXSiteReferrer; } PRUint8 RedirectionLimit() { return mRedirectionLimit; } PRUint16 IdleTimeout() { return mIdleTimeout; } + PRUint16 SpdyTimeout() { return mSpdyTimeout; } PRUint16 MaxRequestAttempts() { return mMaxRequestAttempts; } const char *DefaultSocketType() { return mDefaultSocketType.get(); /* ok to return null */ } nsIIDNService *IDNConverter() { return mIDNConverter; } PRUint32 PhishyUserPassLength() { return mPhishyUserPassLength; } PRUint8 GetQoSBits() { return mQoSBits; } PRUint16 GetIdleSynTimeout() { return mIdleSynTimeout; } bool FastFallbackToIPv4() { return mFastFallbackToIPv4; } PRUint32 MaxSocketCount(); bool IsPersistentHttpsCachingEnabled() { return mEnablePersistentHttpsCaching; } + bool IsSpdyEnabled() { return mEnableSpdy; } + bool CoalesceSpdy() { return mCoalesceSpdy; } + bool PromptTempRedirect() { return mPromptTempRedirect; } nsHttpAuthCache *AuthCache() { return &mAuthCache; } nsHttpConnectionMgr *ConnMgr() { return mConnMgr; } // cache support nsresult GetCacheSession(nsCacheStoragePolicy, nsICacheSession **); PRUint32 GenerateUniqueID() { return ++mLastUniqueID; } @@ -259,16 +263,17 @@ private: PRUint8 mProxyHttpVersion; PRUint8 mCapabilities; PRUint8 mProxyCapabilities; PRUint8 mReferrerLevel; bool mFastFallbackToIPv4; PRUint16 mIdleTimeout; + PRUint16 mSpdyTimeout; PRUint16 mMaxRequestAttempts; PRUint16 mMaxRequestDelay; PRUint16 mIdleSynTimeout; PRUint16 mMaxConnections; PRUint8 mMaxConnectionsPerServer; PRUint8 mMaxPersistentConnectionsPerServer; PRUint8 mMaxPersistentConnectionsPerProxy; @@ -326,16 +331,20 @@ private: // if true allow referrer headers between secure non-matching hosts bool mSendSecureXSiteReferrer; // Persistent HTTPS caching flag bool mEnablePersistentHttpsCaching; // For broadcasting the preference to not be tracked bool mDoNotTrackEnabled; + + // Try to use SPDY instead of HTTP/1.1 over SSL + bool mEnableSpdy; + bool mCoalesceSpdy; }; //----------------------------------------------------------------------------- extern nsHttpHandler *gHttpHandler; //----------------------------------------------------------------------------- // nsHttpsHandler - thin wrapper to distinguish the HTTP handler from the diff --git a/netwerk/protocol/http/nsHttpPipeline.cpp b/netwerk/protocol/http/nsHttpPipeline.cpp --- a/netwerk/protocol/http/nsHttpPipeline.cpp +++ b/netwerk/protocol/http/nsHttpPipeline.cpp @@ -120,17 +120,17 @@ nsHttpPipeline::AddTransaction(nsAHttpTr NS_ADDREF(trans); mRequestQ.AppendElement(trans); if (mConnection) { trans->SetConnection(this); if (mRequestQ.Length() == 1) - mConnection->ResumeSend(); + mConnection->ResumeSend(trans); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpPipeline::nsISupports //----------------------------------------------------------------------------- @@ -159,28 +159,28 @@ nsHttpPipeline::OnHeadersAvailable(nsAHt NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); NS_ASSERTION(mConnection, "no connection"); // trans has now received its response headers; forward to the real connection return mConnection->OnHeadersAvailable(trans, requestHead, responseHead, reset); } nsresult -nsHttpPipeline::ResumeSend() +nsHttpPipeline::ResumeSend(nsAHttpTransaction *) { NS_NOTREACHED("nsHttpPipeline::ResumeSend"); return NS_ERROR_UNEXPECTED; } nsresult -nsHttpPipeline::ResumeRecv() +nsHttpPipeline::ResumeRecv(nsAHttpTransaction *trans) { NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); NS_ASSERTION(mConnection, "no connection"); - return mConnection->ResumeRecv(); + return mConnection->ResumeRecv(trans); } void nsHttpPipeline::CloseTransaction(nsAHttpTransaction *trans, nsresult reason) { LOG(("nsHttpPipeline::CloseTransaction [this=%x trans=%x reason=%x]\n", this, trans, reason)); @@ -357,16 +357,25 @@ nsHttpPipeline::SetConnection(nsAHttpCon NS_IF_ADDREF(mConnection = conn); PRInt32 i, count = mRequestQ.Length(); for (i=0; iSetConnection(this); } +nsAHttpConnection * +nsHttpPipeline::Connection() +{ + LOG(("nsHttpPipeline::Connection [this=%x conn=%x]\n", this, mConnection)); + + NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); + return mConnection; +} + void nsHttpPipeline::GetSecurityCallbacks(nsIInterfaceRequestor **result, nsIEventTarget **target) { NS_ASSERTION(PR_GetCurrentThread() == gSocketThread, "wrong thread"); // return security callbacks from first request nsAHttpTransaction *trans = Request(0); diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp --- a/netwerk/protocol/http/nsHttpTransaction.cpp +++ b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -300,16 +300,22 @@ nsHttpTransaction::Init(PRUint8 caps, nsIOService::gDefaultSegmentCount, nsIOService::gBufferCache); if (NS_FAILED(rv)) return rv; NS_ADDREF(*responseBody = mPipeIn); return NS_OK; } +nsAHttpConnection * +nsHttpTransaction::Connection() +{ + return mConnection; +} + nsHttpResponseHead * nsHttpTransaction::TakeResponseHead() { if (!mHaveAllHeaders) { NS_WARNING("response headers not available or incomplete"); return nsnull; } @@ -1282,30 +1288,30 @@ NS_IMPL_THREADSAFE_QUERY_INTERFACE2(nsHt // nsHttpTransaction::nsIInputStreamCallback //----------------------------------------------------------------------------- // called on the socket thread NS_IMETHODIMP nsHttpTransaction::OnInputStreamReady(nsIAsyncInputStream *out) { if (mConnection) { - nsresult rv = mConnection->ResumeSend(); + nsresult rv = mConnection->ResumeSend(this); if (NS_FAILED(rv)) NS_ERROR("ResumeSend failed"); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpTransaction::nsIOutputStreamCallback //----------------------------------------------------------------------------- // called on the socket thread NS_IMETHODIMP nsHttpTransaction::OnOutputStreamReady(nsIAsyncOutputStream *out) { if (mConnection) { - nsresult rv = mConnection->ResumeRecv(); + nsresult rv = mConnection->ResumeRecv(this); if (NS_FAILED(rv)) NS_ERROR("ResumeRecv failed"); } return NS_OK; } diff --git a/netwerk/protocol/http/nsHttpTransaction.h b/netwerk/protocol/http/nsHttpTransaction.h --- a/netwerk/protocol/http/nsHttpTransaction.h +++ b/netwerk/protocol/http/nsHttpTransaction.h @@ -115,28 +115,27 @@ public: // attributes PRUint8 Caps() { return mCaps; } nsHttpConnectionInfo *ConnectionInfo() { return mConnInfo; } nsHttpResponseHead *ResponseHead() { return mHaveAllHeaders ? mResponseHead : nsnull; } nsISupports *SecurityInfo() { return mSecurityInfo; } nsIInterfaceRequestor *Callbacks() { return mCallbacks; } nsIEventTarget *ConsumerTarget() { return mConsumerTarget; } - nsAHttpConnection *Connection() { return mConnection; } // Called to take ownership of the response headers; the transaction // will drop any reference to the response headers after this call. nsHttpResponseHead *TakeResponseHead(); // Called to find out if the transaction generated a complete response. bool ResponseIsComplete() { return mResponseIsComplete; } bool SSLConnectFailed() { return mSSLConnectFailed; } - // These methods may only be used by the connection manager. + // SetPriority() may only be used by the connection manager. void SetPriority(PRInt32 priority) { mPriority = priority; } PRInt32 Priority() { return mPriority; } const TimingStruct& Timings() const { return mTimings; } private: nsresult Restart(); char *LocateHttpStart(char *buf, PRUint32 len, diff --git a/netwerk/socket/nsISSLSocketControl.idl b/netwerk/socket/nsISSLSocketControl.idl --- a/netwerk/socket/nsISSLSocketControl.idl +++ b/netwerk/socket/nsISSLSocketControl.idl @@ -37,16 +37,52 @@ * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsISupports.idl" interface nsIInterfaceRequestor; -[scriptable, uuid(a092097c-8386-4f1b-97b1-90eb70008c2d)] +%{C++ +#include "nsTArray.h" +class nsCString; +%} +[ref] native nsCStringTArrayRef(nsTArray); + +[scriptable, uuid(87fe5d5a-7f72-48d7-8a13-ef992e0cad43)] interface nsISSLSocketControl : nsISupports { attribute nsIInterfaceRequestor notificationCallbacks; void proxyStartSSL(); void StartTLS(); + + /* NPN (Next Protocol Negotiation) is a mechanism for + negotiating the protocol to be spoken inside the SSL + tunnel during the SSL handshake. The NPNList is the list + of offered client side protocols. setNPNList() needs to + be called before any data is read or written (including the + handshake to be setup correctly. */ + + [noscript] void setNPNList(in nsCStringTArrayRef aNPNList); + + /* SSL sessions are normally initiated by reading or writing + data to the SSL channel. However if a completed channel is + necessary before doing that I/O (e.g. in the case of NPN + the NPN selection done at handshake time is necessary to + know which bytes to write or how to interpret any bytes + read) forceHandshake() can be called to kick off the channel + initiation. */ + + void forceHandshake(); + + /* negotiatedNPN is '' if no NPN list was provided by the client, + * or if the server did not select any protocol choice from that + * list. That also includes the case where the server does not + * implement NPN. + * + * If negotiatedNPN is read before NPN has progressed to the point + * where this information is available NS_ERROR_NOT_CONNECTED is + * raised. + */ + readonly attribute ACString negotiatedNPN; }; diff --git a/security/manager/ssl/src/nsNSSCallbacks.cpp b/security/manager/ssl/src/nsNSSCallbacks.cpp --- a/security/manager/ssl/src/nsNSSCallbacks.cpp +++ b/security/manager/ssl/src/nsNSSCallbacks.cpp @@ -948,16 +948,29 @@ void PR_CALLBACK HandshakeCallback(PRFil } } } status->mHaveKeyLengthAndCipher = true; status->mKeyLength = keyLength; status->mSecretKeyLength = encryptBits; status->mCipherName.Assign(cipherName); + + // Get the NPN value. Do this on the stack and copy it into + // a string rather than preallocating the string because right + // now we expect NPN to fail more often than it succeeds. + SSLNextProtoState state; + unsigned char npnbuf[256]; + unsigned int npnlen; + + if (SSL_GetNextProto(fd, &state, npnbuf, &npnlen, 256) == SECSuccess && + state == SSL_NEXT_PROTO_NEGOTIATED) + infoObject->SetNegotiatedNPN(reinterpret_cast(npnbuf), npnlen); + else + infoObject->SetNegotiatedNPN(nsnull, 0); } PORT_Free(cipherName); PR_FREEIF(certOrgName); PR_Free(signer); } SECStatus diff --git a/security/manager/ssl/src/nsNSSIOLayer.cpp b/security/manager/ssl/src/nsNSSIOLayer.cpp --- a/security/manager/ssl/src/nsNSSIOLayer.cpp +++ b/security/manager/ssl/src/nsNSSIOLayer.cpp @@ -213,17 +213,18 @@ nsNSSSocketInfo::nsNSSSocketInfo() mHandshakePending(true), mCanceled(false), mHasCleartextPhase(false), mHandshakeInProgress(false), mAllowTLSIntoleranceTimeout(true), mRememberClientAuthCertificate(false), mHandshakeStartTime(0), mPort(0), - mIsCertIssuerBlacklisted(false) + mIsCertIssuerBlacklisted(false), + mNPNCompleted(false) { mThreadData = new nsSSLSocketThreadData; } nsNSSSocketInfo::~nsNSSSocketInfo() { delete mThreadData; @@ -525,16 +526,36 @@ nsNSSSocketInfo::GetErrorMessage(PRUnich else { *aText = ToNewUnicode(mErrorMessage); NS_ENSURE_TRUE(*aText, NS_ERROR_OUT_OF_MEMORY); } return NS_OK; } void +nsNSSSocketInfo::SetNegotiatedNPN(const char *value, PRUint32 length) +{ + if (!value) + mNegotiatedNPN.Truncate(); + else + mNegotiatedNPN.Assign(value, length); + mNPNCompleted = true; +} + +NS_IMETHODIMP +nsNSSSocketInfo::GetNegotiatedNPN(nsACString &aNegotiatedNPN) +{ + if (!mNPNCompleted) + return NS_ERROR_NOT_CONNECTED; + + aNegotiatedNPN = mNegotiatedNPN; + return NS_OK; +} + +void nsNSSSocketInfo::SetErrorMessage(const PRUnichar* aText) { mErrorMessage.Assign(aText); } /* void getInterface (in nsIIDRef uuid, [iid_is (uuid), retval] out nsQIResult result); */ NS_IMETHODIMP nsNSSSocketInfo::GetInterface(const nsIID & uuid, void * *result) { nsresult rv; @@ -581,16 +602,36 @@ nsNSSSocketInfo::ProxyStartSSL() } NS_IMETHODIMP nsNSSSocketInfo::StartTLS() { return ActivateSSL(); } +NS_IMETHODIMP +nsNSSSocketInfo::SetNPNList(nsTArray &protocolArray) +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) + return NS_ERROR_NOT_AVAILABLE; + + return nsSSLThread::requestSetNextProtoNego(this, protocolArray); +} + +NS_IMETHODIMP +nsNSSSocketInfo::ForceHandshake() +{ + nsNSSShutDownPreventionLock locker; + if (isAlreadyShutDown()) + return NS_ERROR_NOT_AVAILABLE; + + return nsSSLThread::requestForceHandshake(this); +} + static NS_DEFINE_CID(kNSSCertificateCID, NS_X509CERT_CID); #define NSSSOCKETINFOMAGIC { 0xa9863a23, 0x26b8, 0x4a9c, \ { 0x83, 0xf1, 0xe9, 0xda, 0xdb, 0x36, 0xb8, 0x30 } } static NS_DEFINE_CID(kNSSSocketInfoMagic, NSSSOCKETINFOMAGIC); NS_IMETHODIMP nsNSSSocketInfo::Write(nsIObjectOutputStream* stream) { stream->WriteID(kNSSSocketInfoMagic); diff --git a/security/manager/ssl/src/nsNSSIOLayer.h b/security/manager/ssl/src/nsNSSIOLayer.h --- a/security/manager/ssl/src/nsNSSIOLayer.h +++ b/security/manager/ssl/src/nsNSSIOLayer.h @@ -77,17 +77,18 @@ public: bool ensure_buffer_size(PRInt32 amount); enum ssl_state { ssl_invalid, // used for initializating, should never occur ssl_idle, // not in use by SSL thread, no activity pending ssl_pending_write, // waiting for SSL thread to complete writing ssl_pending_read, // waiting for SSL thread to complete reading ssl_writing_done, // SSL write completed, results are ready - ssl_reading_done // SSL read completed, results are ready + ssl_reading_done, // SSL read completed, results are ready + ssl_force_handshake// SSL handshake required }; ssl_state mSSLState; // Used to transport I/O error codes between SSL thread // and initial caller thread. PRErrorCode mPRErrorCode; @@ -195,16 +196,19 @@ public: PRStatus CloseSocketAndDestroy(); bool IsCertIssuerBlacklisted() const { return mIsCertIssuerBlacklisted; } void SetCertIssuerBlacklisted() { mIsCertIssuerBlacklisted = true; } + + void SetNegotiatedNPN(const char *value, PRUint32 length); + protected: nsCOMPtr mCallbacks; PRFileDesc* mFd; nsCOMPtr mPreviousCert; // DocShellDependent enum { blocking_state_unknown, is_nonblocking_socket, is_blocking_socket } mBlockingState; PRUint32 mSecurityState; @@ -232,16 +236,19 @@ protected: nsRefPtr mSSLStatus; nsresult ActivateSSL(); nsSSLSocketThreadData *mThreadData; nsresult EnsureDocShellDependentStuffKnown(); + nsCString mNegotiatedNPN; + bool mNPNCompleted; + private: virtual void virtualDestroyNSSReference(); void destructorSafeDestroyNSSReference(); friend class nsSSLThread; }; class nsCStringHashSet; diff --git a/security/manager/ssl/src/nsSSLThread.cpp b/security/manager/ssl/src/nsSSLThread.cpp --- a/security/manager/ssl/src/nsSSLThread.cpp +++ b/security/manager/ssl/src/nsSSLThread.cpp @@ -179,16 +179,17 @@ PRInt32 nsSSLThread::requestRecvMsgPeek( memcpy(buf, si->mThreadData->mSSLRemainingReadResultData, return_amount); return return_amount; } case nsSSLSocketThreadData::ssl_writing_done: case nsSSLSocketThreadData::ssl_pending_write: case nsSSLSocketThreadData::ssl_pending_read: + case nsSSLSocketThreadData::ssl_force_handshake: // for safety reasons, also return would_block on any other state, // although this switch statement should be complete and list // the appropriate behaviour for each state. default: { PORT_SetError(PR_WOULD_BLOCK_ERROR); return -1; @@ -218,16 +219,133 @@ nsresult nsSSLThread::requestActivateSSL return NS_ERROR_FAILURE; if (SECSuccess != SSL_ResetHandshake(fd, false)) return NS_ERROR_FAILURE; return NS_OK; } +nsresult nsSSLThread::requestForceHandshake(nsNSSSocketInfo *si) +{ + if (!ssl_thread_singleton || !si || !ssl_thread_singleton->mThreadHandle) + { + PR_SetError(PR_UNKNOWN_ERROR, 0); + return NS_ERROR_FAILURE; + } + + bool some_socket_is_busy = false; + nsSSLSocketThreadData::ssl_state my_ssl_state = nsSSLSocketThreadData::ssl_invalid; + PRFileDesc *blockingFD = nsnull; + + { + MutexAutoLock threadLock(ssl_thread_singleton->mMutex); + + if (ssl_thread_singleton->exitRequested(threadLock)) { + PR_SetError(PR_UNKNOWN_ERROR, 0); + return NS_ERROR_FAILURE; + } + + if (getRealFDIfBlockingSocket_locked(si, blockingFD) == PR_FAILURE) { + return NS_ERROR_FAILURE; + } + + if (blockingFD) + return NS_ERROR_FAILURE; + + my_ssl_state = si->mThreadData->mSSLState; + + if (ssl_thread_singleton->mBusySocket && + ssl_thread_singleton->mBusySocket != si) + { + some_socket_is_busy = true; + } + } + + if (my_ssl_state != nsSSLSocketThreadData::ssl_idle) + { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (some_socket_is_busy) + { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (si->isPK11LoggedOut() || si->isAlreadyShutDown()) { + PR_SetError(PR_SOCKET_SHUTDOWN_ERROR, 0); + return NS_ERROR_FAILURE; + } + + if (si->GetCanceled()) { + return NS_ERROR_FAILURE; + } + + if (si->mThreadData->mPRErrorCode != PR_SUCCESS && + si->mThreadData->mPRErrorCode != PR_WOULD_BLOCK_ERROR) { + return NS_ERROR_NET_INTERRUPT; + } + + si->mThreadData->mSSLState = nsSSLSocketThreadData::ssl_force_handshake; + + { + MutexAutoLock threadLock(ssl_thread_singleton->mMutex); + + if (nsSSLIOLayerHelpers::mSharedPollableEvent) + { + NS_ASSERTION(!nsSSLIOLayerHelpers::mSocketOwningPollableEvent, + "oops, some other socket still owns our shared pollable event"); + + NS_ASSERTION(!si->mThreadData->mReplacedSSLFileDesc, "oops"); + + si->mThreadData->mReplacedSSLFileDesc = si->mFd->lower; + si->mFd->lower = nsSSLIOLayerHelpers::mSharedPollableEvent; + } + + nsSSLIOLayerHelpers::mSocketOwningPollableEvent = si; + ssl_thread_singleton->mBusySocket = si; + + // notify the thread + ssl_thread_singleton->mCond.NotifyAll(); + } + + return NS_OK; +} + +nsresult +nsSSLThread::requestSetNextProtoNego(nsNSSSocketInfo *si, + nsTArray &protocolArray) +{ + PRFileDesc *fd = getRealSSLFD(si); + if (!fd) + return NS_ERROR_FAILURE; + + nsCString npnList; + + // the nss list is a concattenated list of 8 bit byte strings. + + for (PRUint32 index = 0; index < protocolArray.Length(); ++index) { + if (protocolArray[index].IsEmpty() || + protocolArray[index].Length() > 255) + return NS_ERROR_ILLEGAL_VALUE; + + npnList.Append(protocolArray[index].Length()); + npnList.Append(protocolArray[index]); + } + + if (SSL_SetNextProtoNego( + fd, + reinterpret_cast(npnList.get()), + npnList.Length()) != SECSuccess) + return NS_ERROR_FAILURE; + + return NS_OK; + +} + PRInt16 nsSSLThread::requestPoll(nsNSSSocketInfo *si, PRInt16 in_flags, PRInt16 *out_flags) { if (!ssl_thread_singleton || !si || !ssl_thread_singleton->mThreadHandle) return 0; *out_flags = 0; // Socket is unusable - set EXCEPT-flag and return. See bug #480619. @@ -919,16 +1037,18 @@ PRInt32 nsSSLThread::requestWrite(nsNSSS void nsSSLThread::Run(void) { // Helper variable, we don't want to call destroy // while holding the mutex. nsNSSSocketInfo *socketToDestroy = nsnull; while (true) { + bool forceHandshakeDone = false; + if (socketToDestroy) { socketToDestroy->CloseSocketAndDestroy(); socketToDestroy = nsnull; } // remember whether we'll write or read nsSSLSocketThreadData::ssl_state busy_socket_ssl_state; @@ -964,16 +1084,18 @@ void nsSSLThread::Run(void) bool pending_work = false; do { if (mBusySocket && (mBusySocket->mThreadData->mSSLState == nsSSLSocketThreadData::ssl_pending_read || + mBusySocket->mThreadData->mSSLState == nsSSLSocketThreadData::ssl_force_handshake + || mBusySocket->mThreadData->mSSLState == nsSSLSocketThreadData::ssl_pending_write)) { pending_work = true; } if (!pending_work) { // no work to do ? let's wait a moment @@ -1086,31 +1208,49 @@ void nsSSLThread::Run(void) // give the error back to caller bstd.mPRErrorCode = PR_GetError(); } bstd.mSSLResultRemainingBytes = bytesRead; bstd.mSSLRemainingReadResultData = bstd.mSSLDataBuffer; busy_socket_ssl_state = nsSSLSocketThreadData::ssl_reading_done; } + else if (nsSSLSocketThreadData::ssl_force_handshake == busy_socket_ssl_state) + { + if (SSL_ForceHandshake(realFileDesc) != PR_SUCCESS) + bstd.mPRErrorCode = PR_GetError(); + + busy_socket_ssl_state = nsSSLSocketThreadData::ssl_idle; + forceHandshakeDone = true; + } } // avoid setting event repeatedly bool needToSetPollableEvent = false; { MutexAutoLock threadLock(ssl_thread_singleton->mMutex); mBusySocket->mThreadData->mSSLState = busy_socket_ssl_state; + + if (forceHandshakeDone) { + mBusySocket->mFd->lower = mBusySocket->mThreadData->mReplacedSSLFileDesc; + mBusySocket->mThreadData->mReplacedSSLFileDesc = nsnull; + nsSSLIOLayerHelpers::mSocketOwningPollableEvent = nsnull; + + nsSSLIOLayerHelpers::mPollableEventCurrentlySet = false; + mBusySocket = nsnull; + } if (!nsSSLIOLayerHelpers::mPollableEventCurrentlySet) { needToSetPollableEvent = true; nsSSLIOLayerHelpers::mPollableEventCurrentlySet = true; } + } if (needToSetPollableEvent && nsSSLIOLayerHelpers::mSharedPollableEvent) { // Wake up the file descriptor on the Necko thread, // so it can fetch the results from the SSL I/O call // that we just completed. PR_SetPollableEvent(nsSSLIOLayerHelpers::mSharedPollableEvent); diff --git a/security/manager/ssl/src/nsSSLThread.h b/security/manager/ssl/src/nsSSLThread.h --- a/security/manager/ssl/src/nsSSLThread.h +++ b/security/manager/ssl/src/nsSSLThread.h @@ -36,19 +36,21 @@ * ***** END LICENSE BLOCK ***** */ #ifndef _NSSSLTHREAD_H_ #define _NSSSLTHREAD_H_ #include "nsCOMPtr.h" #include "nsIRequest.h" #include "nsPSMBackgroundThread.h" +#include "nsTArray.h" class nsNSSSocketInfo; class nsIHttpChannel; +class nsCString; class nsSSLThread : public nsPSMBackgroundThread { private: // We use mMutex contained in our base class // to protect access to these variables: // mBusySocket, mSocketScheduledToBeDestroyed // and to nsSSLSocketThreadData::mSSLState @@ -146,13 +148,18 @@ public: static PRStatus requestSetsocketoption(nsNSSSocketInfo *si, const PRSocketOptionData *data); static PRStatus requestConnectcontinue(nsNSSSocketInfo *si, PRInt16 out_flags); static nsresult requestActivateSSL(nsNSSSocketInfo *si); + + static nsresult requestForceHandshake(nsNSSSocketInfo *si); + static nsresult requestSetNextProtoNego(nsNSSSocketInfo *si, + nsTArray &protocolArray); + static bool stoppedOrStopping(); }; #endif //_NSSSLTHREAD_H_ diff --git a/toolkit/components/telemetry/TelemetryHistograms.h b/toolkit/components/telemetry/TelemetryHistograms.h --- a/toolkit/components/telemetry/TelemetryHistograms.h +++ b/toolkit/components/telemetry/TelemetryHistograms.h @@ -150,16 +150,34 @@ HISTOGRAM(HTTP_REQUEST_PER_PAGE_FROM_CAC _HTTP_HIST(HTTP_##prefix##_REVALIDATION, labelprefix "Positive cache validation time (ms)") \ _HTTP_HIST(HTTP_##prefix##_COMPLETE_LOAD, labelprefix "Overall load time - all (ms)") \ _HTTP_HIST(HTTP_##prefix##_COMPLETE_LOAD_CACHED, labelprefix "Overall load time - cache hits (ms)") \ _HTTP_HIST(HTTP_##prefix##_COMPLETE_LOAD_NET, labelprefix "Overall load time - network (ms)") \ HTTP_HISTOGRAMS(PAGE, "page: ") HTTP_HISTOGRAMS(SUB, "subitem: ") +HISTOGRAM(SPDY_PARALLEL_STREAMS, 1, 1000, 50, EXPONENTIAL, "SPDY: Streams concurrent active per connection") +HISTOGRAM(SPDY_TOTAL_STREAMS, 1, 100000, 50, EXPONENTIAL, "SPDY: Streams created per connection") +HISTOGRAM(SPDY_SERVER_INITIATED_STREAMS, 1, 100000, 250, EXPONENTIAL, "SPDY: Streams recevied per connection") +HISTOGRAM(SPDY_CHUNK_RECVD, 1, 1000, 100, EXPONENTIAL, "SPDY: Recvd Chunk Size (rounded to KB)") +HISTOGRAM(SPDY_SYN_SIZE, 20, 20000, 50, EXPONENTIAL, "SPDY: SYN Frame Header Size") +HISTOGRAM(SPDY_SYN_RATIO, 1, 99, 20, LINEAR, "SPDY: SYN Frame Header Ratio (lower better)") +HISTOGRAM(SPDY_SYN_REPLY_SIZE, 16, 20000, 50, EXPONENTIAL, "SPDY: SYN Reply Header Size") +HISTOGRAM(SPDY_SYN_REPLY_RATIO, 1, 99, 20, LINEAR, "SPDY: SYN Reply Header Ratio (lower better)") +HISTOGRAM(SPDY_NPN_CONNECT, 0, 1, 2, BOOLEAN, "SPDY: NPN Negotiated") + +HISTOGRAM(SPDY_SETTINGS_UL_BW, 1, 10000, 100, EXPONENTIAL, "SPDY: Settings Upload Bandwidth") +HISTOGRAM(SPDY_SETTINGS_DL_BW, 1, 10000, 100, EXPONENTIAL, "SPDY: Settings Download Bandwidth") +HISTOGRAM(SPDY_SETTINGS_RTT, 1, 1000, 100, EXPONENTIAL, "SPDY: Settings RTT") +HISTOGRAM(SPDY_SETTINGS_MAX_STREAMS, 1, 5000, 100, EXPONENTIAL, "SPDY: Settings Max Streams parameter") +HISTOGRAM(SPDY_SETTINGS_CWND, 1, 500, 50, EXPONENTIAL, "SPDY: Settings CWND (packets)") +HISTOGRAM(SPDY_SETTINGS_RETRANS, 1, 100, 50, EXPONENTIAL, "SPDY: Retransmission Rate") +HISTOGRAM(SPDY_SETTINGS_IW, 1, 1000, 50, EXPONENTIAL, "SPDY: Settings IW (rounded to KB)") + #undef _HTTP_HIST #undef HTTP_HISTOGRAMS HISTOGRAM(HTTP_CACHE_DISPOSITION, 1, 5, 5, LINEAR, "HTTP Cache Hit, Reval, Failed-Reval, Miss") HISTOGRAM(HTTP_DISK_CACHE_DISPOSITION, 1, 5, 5, LINEAR, "HTTP Disk Cache Hit, Reval, Failed-Reval, Miss") HISTOGRAM(HTTP_MEMORY_CACHE_DISPOSITION, 1, 5, 5, LINEAR, "HTTP Memory Cache Hit, Reval, Failed-Reval, Miss") HISTOGRAM(HTTP_OFFLINE_CACHE_DISPOSITION, 1, 5, 5, LINEAR, "HTTP Offline Cache Hit, Reval, Failed-Reval, Miss") HISTOGRAM(CACHE_DEVICE_SEARCH, 1, 100, 100, LINEAR, "Time to search cache (ms)")