bug 378637 part 14 - https proxying for spdy31 and http2 r=hurley
authorPatrick McManus <mcmanus@ducksong.com>
Fri, 16 May 2014 11:46:13 -0400
changeset 183609 3e4ecea736c3
parent 183608 ab6d433e563b
child 183610 9cbd4cf8ce64
push id26799
push userphilringnalda@gmail.com
push date2014-05-18 00:55 +0000
treeherdermozilla-central@00ef3a7d7aa7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershurley
bugs378637
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 378637 part 14 - https proxying for spdy31 and http2 r=hurley
netwerk/protocol/http/Http2Compression.cpp
netwerk/protocol/http/Http2Compression.h
netwerk/protocol/http/Http2Session.cpp
netwerk/protocol/http/Http2Session.h
netwerk/protocol/http/Http2Stream.cpp
netwerk/protocol/http/Http2Stream.h
netwerk/protocol/http/SpdySession31.cpp
netwerk/protocol/http/SpdySession31.h
netwerk/protocol/http/SpdyStream31.cpp
netwerk/protocol/http/SpdyStream31.h
--- a/netwerk/protocol/http/Http2Compression.cpp
+++ b/netwerk/protocol/http/Http2Compression.cpp
@@ -893,17 +893,17 @@ Http2Decompressor::DoContextUpdate()
 }
 
 /////////////////////////////////////////////////////////////////
 
 nsresult
 Http2Compressor::EncodeHeaderBlock(const nsCString &nvInput,
                                    const nsACString &method, const nsACString &path,
                                    const nsACString &host, const nsACString &scheme,
-                                   nsACString &output)
+                                   bool connectForm, nsACString &output)
 {
   mAlternateReferenceSet.Clear();
   mImpliedReferenceSet.Clear();
   mOutput = &output;
   output.SetCapacity(1024);
   output.Truncate();
   mParsedContentLength = -1;
 
@@ -912,20 +912,25 @@ Http2Compressor::EncodeHeaderBlock(const
     if (mLowestBufferSizeWaiting < mMaxBufferSetting) {
       EncodeTableSizeChange(mLowestBufferSizeWaiting);
     }
     EncodeTableSizeChange(mMaxBufferSetting);
     mBufferSizeChangeWaiting = false;
   }
 
   // colon headers first
-  ProcessHeader(nvPair(NS_LITERAL_CSTRING(":method"), method), false);
-  ProcessHeader(nvPair(NS_LITERAL_CSTRING(":path"), path), false);
-  ProcessHeader(nvPair(NS_LITERAL_CSTRING(":authority"), host), false);
-  ProcessHeader(nvPair(NS_LITERAL_CSTRING(":scheme"), scheme), false);
+  if (!connectForm) {
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":method"), method), false);
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":path"), path), false);
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":authority"), host), false);
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":scheme"), scheme), false);
+  } else {
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":method"), method), false);
+    ProcessHeader(nvPair(NS_LITERAL_CSTRING(":authority"), host), false);
+  }
 
   // now the non colon headers
   const char *beginBuffer = nvInput.BeginReading();
 
   // This strips off the HTTP/1 method+path+version
   int32_t crlfIndex = nvInput.Find("\r\n");
   while (true) {
     int32_t startIndex = crlfIndex + 2;
--- a/netwerk/protocol/http/Http2Compression.h
+++ b/netwerk/protocol/http/Http2Compression.h
@@ -159,17 +159,17 @@ public:
   { };
   virtual ~Http2Compressor() { }
 
   // HTTP/1 formatted header block as input - HTTP/2 formatted
   // header block as output
   nsresult EncodeHeaderBlock(const nsCString &nvInput,
                              const nsACString &method, const nsACString &path,
                              const nsACString &host, const nsACString &scheme,
-                             nsACString &output);
+                             bool connectForm, nsACString &output);
 
   int64_t GetParsedContentLength() { return mParsedContentLength; } // -1 on not found
 
   void SetMaxBufferSize(uint32_t maxBufferSize);
   nsresult SetMaxBufferSizeInternal(uint32_t maxBufferSize);
 
 protected:
   virtual void ClearHeaderTable() MOZ_OVERRIDE;
--- a/netwerk/protocol/http/Http2Session.cpp
+++ b/netwerk/protocol/http/Http2Session.cpp
@@ -381,33 +381,33 @@ Http2Session::AddStream(nsAHttpTransacti
 
   // integrity check
   if (mStreamTransactionHash.Get(aHttpTransaction)) {
     LOG3(("   New transaction already present\n"));
     MOZ_ASSERT(false, "AddStream duplicate transaction pointer");
     return false;
   }
 
-  // assert that
-  // a] in the case we have a connection, that the new transaction connection
-  //    is either undefined or on the same connection
-  // b] in the case we don't have a connection, that the new transaction
-  //    connection is defined so we can adopt it
-  MOZ_ASSERT((mConnection && (!aHttpTransaction->Connection() ||
-                              mConnection == aHttpTransaction->Connection())) ||
-             (!mConnection && aHttpTransaction->Connection()));
-
   if (!mConnection) {
     mConnection = aHttpTransaction->Connection();
   }
+
   aHttpTransaction->SetConnection(this);
+
+  if (aUseTunnel) {
+    LOG3(("Http2Session::AddStream session=%p trans=%p OnTunnel",
+          this, aHttpTransaction));
+    DispatchOnTunnel(aHttpTransaction, aCallbacks);
+    return true;
+  }
+
   Http2Stream *stream = new Http2Stream(aHttpTransaction, this, aPriority);
 
-  LOG3(("Http2Session::AddStream session=%p stream=%p NextID=0x%X (tentative)",
-        this, stream, mNextStreamID));
+  LOG3(("Http2Session::AddStream session=%p stream=%p serial=%u "
+        "NextID=0x%X (tentative)", this, stream, mSerial, mNextStreamID));
 
   mStreamTransactionHash.Put(aHttpTransaction, stream);
 
   if (RoomForMoreConcurrent()) {
     LOG3(("Http2Session::AddStream %p stream %p activated immediately.",
           this, stream));
     ActivateStream(stream);
   } else {
@@ -986,16 +986,20 @@ Http2Session::CloseStream(Http2Stream *a
   if (aStream == mInputFrameDataStream) {
     LOG3(("Stream had active partial read frame on close"));
     ChangeDownstreamState(DISCARDING_DATA_FRAME);
     mInputFrameDataStream = nullptr;
   }
 
   RemoveStreamFromQueues(aStream);
 
+  if (aStream->IsTunnel()) {
+    UnRegisterTunnel(aStream);
+  }
+
   // Send the stream the close() indication
   aStream->Close(aResult);
 }
 
 nsresult
 Http2Session::SetInputFrameDataStream(uint32_t streamID)
 {
   mInputFrameDataStream = mStreamIDHash.Get(streamID);
@@ -1133,29 +1137,45 @@ nsresult
 Http2Session::ResponseHeadersComplete()
 {
   LOG3(("Http2Session::ResponseHeadersComplete %p for 0x%X fin=%d",
         this, mInputFrameDataStream->StreamID(), mInputFrameFinal));
 
   // only do this once, afterwards ignore trailers
   if (mInputFrameDataStream->AllHeadersReceived())
     return NS_OK;
-  mInputFrameDataStream->SetAllHeadersReceived(true);
+  nsresult rv = mInputFrameDataStream->SetAllHeadersReceived(true);
+
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
 
   // The stream needs to see flattened http headers
   // Uncompressed http/2 format headers currently live in
   // Http2Stream::mDecompressBuffer - convert that to HTTP format in
   // mFlatHTTPResponseHeaders via ConvertHeaders()
 
   mFlatHTTPResponseHeadersOut = 0;
-  nsresult rv = mInputFrameDataStream->ConvertResponseHeaders(&mDecompressor,
-                                                              mDecompressBuffer,
-                                                              mFlatHTTPResponseHeaders);
-  if (NS_FAILED(rv))
+  rv = mInputFrameDataStream->ConvertResponseHeaders(&mDecompressor,
+                                                     mDecompressBuffer,
+                                                     mFlatHTTPResponseHeaders);
+  if (rv == NS_ERROR_ABORT) {
+    LOG(("Http2Session::ResponseHeadersComplete ConvertResponseHeaders aborted\n"));
+    if (mInputFrameDataStream->IsTunnel()) {
+      gHttpHandler->ConnMgr()->CancelTransactions(
+        mInputFrameDataStream->Transaction()->ConnectionInfo(),
+        NS_ERROR_CONNECTION_REFUSED);
+    }
+    CleanupStream(mInputFrameDataStream, rv, CANCEL_ERROR);
+    ResetDownstreamState();
+    return NS_OK;
+  }
+  else if (NS_FAILED(rv)) {
     return rv;
+  }
 
   ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS);
   return NS_OK;
 }
 
 nsresult
 Http2Session::RecvPriority(Http2Session *self)
 {
@@ -2273,42 +2293,42 @@ Http2Session::WriteSegments(nsAHttpSegme
   if (mDownstreamState == PROCESSING_DATA_FRAME ||
       mDownstreamState == PROCESSING_COMPLETE_HEADERS) {
 
     // The cleanup stream should only be set while stream->WriteSegments is
     // on the stack and then cleaned up in this code block afterwards.
     MOZ_ASSERT(!mNeedsCleanup, "cleanup stream set unexpectedly");
     mNeedsCleanup = nullptr;                     /* just in case */
 
+    Http2Stream *stream = mInputFrameDataStream;
     mSegmentWriter = writer;
     rv = mInputFrameDataStream->WriteSegments(this, count, countWritten);
     mSegmentWriter = nullptr;
 
     mLastDataReadEpoch = mLastReadEpoch;
 
     if (SoftStreamError(rv)) {
       // This will happen when the transaction figures out it is EOF, generally
       // due to a content-length match being made. Return OK from this function
       // otherwise the whole session would be torn down.
-      Http2Stream *stream = mInputFrameDataStream;
 
       // if we were doing PROCESSING_COMPLETE_HEADERS need to pop the state
       // back to PROCESSING_DATA_FRAME where we came from
       mDownstreamState = PROCESSING_DATA_FRAME;
 
       if (mInputFrameDataRead == mInputFrameDataSize)
         ResetDownstreamState();
       LOG3(("Http2Session::WriteSegments session=%p stream=%p 0x%X "
             "needscleanup=%p. cleanup stream based on "
             "stream->writeSegments returning code %x\n",
             this, stream, stream ? stream->StreamID() : 0,
             mNeedsCleanup, rv));
       CleanupStream(stream, NS_OK, CANCEL_ERROR);
-      MOZ_ASSERT(!mNeedsCleanup, "double cleanup out of data frame");
-      mNeedsCleanup = nullptr;                     /* just in case */
+      MOZ_ASSERT(!mNeedsCleanup || mNeedsCleanup == stream);
+      mNeedsCleanup = nullptr;
       return NS_OK;
     }
 
     if (mNeedsCleanup) {
       LOG3(("Http2Session::WriteSegments session=%p stream=%p 0x%X "
             "cleanup stream based on mNeedsCleanup.\n",
             this, mNeedsCleanup, mNeedsCleanup ? mNeedsCleanup->StreamID() : 0));
       CleanupStream(mNeedsCleanup, NS_OK, CANCEL_ERROR);
@@ -2789,16 +2809,90 @@ Http2Session::SetNeedsCleanup()
 
 void
 Http2Session::ConnectPushedStream(Http2Stream *stream)
 {
   mReadyForRead.Push(stream);
   ForceRecv();
 }
 
+uint32_t
+Http2Session::FindTunnelCount(nsHttpConnectionInfo *aConnInfo)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  uint32_t rv = 0;
+  mTunnelHash.Get(aConnInfo->HashKey(), &rv);
+  return rv;
+}
+
+void
+Http2Session::RegisterTunnel(Http2Stream *aTunnel)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
+  uint32_t newcount = FindTunnelCount(ci) + 1;
+  mTunnelHash.Remove(ci->HashKey());
+  mTunnelHash.Put(ci->HashKey(), newcount);
+  LOG3(("Http2Stream::RegisterTunnel %p stream=%p tunnels=%d [%s]",
+        this, aTunnel, newcount, ci->HashKey().get()));
+}
+
+void
+Http2Session::UnRegisterTunnel(Http2Stream *aTunnel)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
+  MOZ_ASSERT(FindTunnelCount(ci));
+  uint32_t newcount = FindTunnelCount(ci) - 1;
+  mTunnelHash.Remove(ci->HashKey());
+  if (newcount) {
+    mTunnelHash.Put(ci->HashKey(), newcount);
+  }
+  LOG3(("Http2Session::UnRegisterTunnel %p stream=%p tunnels=%d [%s]",
+        this, aTunnel, newcount, ci->HashKey().get()));
+}
+
+void
+Http2Session::DispatchOnTunnel(nsAHttpTransaction *aHttpTransaction,
+                               nsIInterfaceRequestor *aCallbacks)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpTransaction *trans = aHttpTransaction->QueryHttpTransaction();
+  nsHttpConnectionInfo *ci = aHttpTransaction->ConnectionInfo();
+  MOZ_ASSERT(trans);
+
+  LOG3(("Http2Session::DispatchOnTunnel %p trans=%p", this, trans));
+
+  aHttpTransaction->SetConnection(nullptr);
+
+  // this transaction has done its work of setting up a tunnel, let
+  // the connection manager queue it if necessary
+  trans->SetDontRouteViaWildCard(true);
+
+  if (FindTunnelCount(ci) < gHttpHandler->MaxConnectionsPerOrigin()) {
+    LOG3(("Http2Session::DispatchOnTunnel %p create on new tunnel %s",
+          this, ci->HashKey().get()));
+    nsRefPtr<SpdyConnectTransaction> connectTrans =
+      new SpdyConnectTransaction(ci, aCallbacks,
+                                 trans->Caps(), trans, this);
+    AddStream(connectTrans, trans->Priority(),
+              false, nullptr);
+    Http2Stream *tunnel = mStreamTransactionHash.Get(connectTrans);
+    MOZ_ASSERT(tunnel);
+    RegisterTunnel(tunnel);
+  }
+
+  // requeue it. The connection manager is responsible for actually putting
+  // this on the tunnel connection with the specific ci now that it
+  // has DontRouteViaWildCard set.
+  trans->EnableKeepAlive();
+  gHttpHandler->InitiateTransaction(trans, trans->Priority());
+}
+
+
 nsresult
 Http2Session::BufferOutput(const char *buf,
                            uint32_t count,
                            uint32_t *countRead)
 {
   nsAHttpSegmentReader *old = mSegmentReader;
   mSegmentReader = nullptr;
   nsresult rv = OnReadSegment(buf, count, countRead);
@@ -2889,27 +2983,34 @@ Http2Session::TransactionHasDataToWrite(
     return;
   }
 
   LOG3(("Http2Session::TransactionHasDataToWrite %p ID is 0x%X\n",
         this, stream->StreamID()));
 
   mReadyForWrite.Push(stream);
   SetWriteCallbacks();
+
+  // NSPR poll will not poll the network if there are non system PR_FileDesc's
+  // that are ready - so we can get into a deadlock waiting for the system IO
+  // to come back here if we don't force the send loop manually.
+  ForceSend();
+
 }
 
 void
 Http2Session::TransactionHasDataToWrite(Http2Stream *stream)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
   LOG3(("Http2Session::TransactionHasDataToWrite %p stream=%p ID=0x%x",
         this, stream, stream->StreamID()));
 
   mReadyForWrite.Push(stream);
   SetWriteCallbacks();
+  ForceSend();
 }
 
 bool
 Http2Session::IsPersistent()
 {
   return true;
 }
 
--- a/netwerk/protocol/http/Http2Session.h
+++ b/netwerk/protocol/http/Http2Session.h
@@ -20,16 +20,17 @@
 
 class nsISocketTransport;
 
 namespace mozilla {
 namespace net {
 
 class Http2PushedStream;
 class Http2Stream;
+class nsHttpTransaction;
 
 class Http2Session MOZ_FINAL : public ASpdySession
   , public nsAHttpConnection
   , public nsAHttpSegmentReader
   , public nsAHttpSegmentWriter
 {
 public:
   NS_DECL_ISUPPORTS
@@ -440,14 +441,23 @@ private:
 
   // used as a temporary buffer while enumerating the stream hash during GoAway
   nsDeque  mGoAwayStreamsToRestart;
 
   // Each session gets a unique serial number because the push cache is correlated
   // by the load group and the serial number can be used as part of the cache key
   // to make sure streams aren't shared across sessions.
   uint64_t        mSerial;
+
+private:
+/// connect tunnels
+  void DispatchOnTunnel(nsAHttpTransaction *, nsIInterfaceRequestor *);
+  void RegisterTunnel(Http2Stream *);
+  void UnRegisterTunnel(Http2Stream *);
+  uint32_t FindTunnelCount(nsHttpConnectionInfo *);
+
+  nsDataHashtable<nsCStringHashKey, uint32_t> mTunnelHash;
 };
 
 } // namespace mozilla::net
 } // namespace mozilla
 
 #endif // mozilla_net_Http2Session_h
--- a/netwerk/protocol/http/Http2Stream.cpp
+++ b/netwerk/protocol/http/Http2Stream.cpp
@@ -14,16 +14,17 @@
 #define LOG_ENABLED() LOG5_ENABLED()
 
 #include <algorithm>
 
 #include "Http2Compression.h"
 #include "Http2Session.h"
 #include "Http2Stream.h"
 #include "Http2Push.h"
+#include "TunnelUtils.h"
 
 #include "mozilla/Telemetry.h"
 #include "nsAlgorithm.h"
 #include "nsHttp.h"
 #include "nsHttpHandler.h"
 #include "nsHttpRequestHead.h"
 #include "nsISocketTransport.h"
 #include "prnetdb.h"
@@ -62,16 +63,17 @@ Http2Stream::Http2Stream(nsAHttpTransact
   , mTxInlineFrameUsed(0)
   , mTxStreamFrameSize(0)
   , mRequestBodyLenRemaining(0)
   , mLocalUnacked(0)
   , mBlockedOnRwin(false)
   , mTotalSent(0)
   , mTotalRead(0)
   , mPushSource(nullptr)
+  , mIsTunnel(false)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
   LOG3(("Http2Stream::Http2Stream %p", this));
 
   mServerReceiveWindow = session->GetServerInitialStreamWindow();
   mClientReceiveWindow = session->PushAllowance();
 
@@ -91,16 +93,17 @@ Http2Stream::Http2Stream(nsAHttpTransact
     httpPriority = kNormalPriority + priority;
   }
   MOZ_ASSERT(httpPriority >= 0);
   SetPriority(static_cast<uint32_t>(httpPriority));
 }
 
 Http2Stream::~Http2Stream()
 {
+  ClearTransactionsBlockedOnTunnel();
   mStreamID = Http2Session::kDeadStreamID;
 }
 
 // ReadSegments() is used to write data down the socket. Generally, HTTP
 // request data is pulled from the approriate transaction and
 // converted to HTTP/2 data. Sometimes control data like a window-update is
 // generated instead.
 
@@ -136,16 +139,19 @@ Http2Stream::ReadSegments(nsAHttpSegment
   case GENERATING_BODY:
   case SENDING_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 = nullptr;
 
+    LOG3(("Http2Stream::ReadSegments %p trans readsegments rv %x read=%d\n",
+          this, rv, *countRead));
+
     // Check to see if the transaction's request could be written out now.
     // If not, mark the stream for callback when writing can proceed.
     if (NS_SUCCEEDED(rv) &&
         mUpstreamState == GENERATING_HEADERS &&
         !mAllHeadersSent)
       mSession->TransactionHasDataToWrite(this);
 
     // mTxinlineFrameUsed represents any queued un-sent frame. It might
@@ -160,17 +166,17 @@ Http2Stream::ReadSegments(nsAHttpSegment
     if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mTxInlineFrameUsed)
       mRequestBlockedOnRead = 1;
 
     // If the sending flow control window is open (!mBlockedOnRwin) then
     // continue sending the request
     if (!mBlockedOnRwin &&
         !mTxInlineFrameUsed && NS_SUCCEEDED(rv) && (!*countRead)) {
       LOG3(("Http2Stream::ReadSegments %p 0x%X: Sending request data complete, "
-            "mUpstreamState=%x",this, mStreamID, mUpstreamState));
+            "mUpstreamState=%x\n",this, mStreamID, mUpstreamState));
       if (mSentFin) {
         ChangeState(UPSTREAM_COMPLETE);
       } else {
         GenerateDataFrameHeader(0, true);
         ChangeState(SENDING_FIN_STREAM);
         mSession->TransactionHasDataToWrite(this);
         rv = NS_BASE_STREAM_WOULD_BLOCK;
       }
@@ -263,16 +269,17 @@ Http2Stream::ParseHttpRequestHeaders(con
 
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
   MOZ_ASSERT(mUpstreamState == GENERATING_HEADERS);
 
   LOG3(("Http2Stream::ParseHttpRequestHeaders %p avail=%d state=%x",
         this, avail, mUpstreamState));
 
   mFlatHttpRequestHeaders.Append(buf, avail);
+  nsHttpRequestHead *head = mTransaction->RequestHead();
 
   // We can use the simple double crlf because firefox is the
   // only client we are parsing
   int32_t endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n");
 
   if (endHeader == kNotFound) {
     // We don't have all the headers yet
     LOG3(("Http2Stream::ParseHttpRequestHeaders %p "
@@ -285,27 +292,27 @@ Http2Stream::ParseHttpRequestHeaders(con
   // 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.
   uint32_t oldLen = mFlatHttpRequestHeaders.Length();
   mFlatHttpRequestHeaders.SetLength(endHeader + 2);
   *countUsed = avail - (oldLen - endHeader) + 4;
   mAllHeadersSent = 1;
 
-  nsAutoCString hostHeader;
+  nsAutoCString authorityHeader;
   nsAutoCString hashkey;
-  mTransaction->RequestHead()->GetHeader(nsHttp::Host, hostHeader);
+  head->GetHeader(nsHttp::Host, authorityHeader);
 
-  CreatePushHashKey(NS_LITERAL_CSTRING("https"),
-                    hostHeader, mSession->Serial(),
-                    mTransaction->RequestHead()->RequestURI(),
+  CreatePushHashKey(nsDependentCString(head->IsHTTPS() ? "https" : "http"),
+                    authorityHeader, mSession->Serial(),
+                    head->RequestURI(),
                     mOrigin, hashkey);
 
   // check the push cache for GET
-  if (mTransaction->RequestHead()->IsGet()) {
+  if (head->IsGet()) {
     // from :scheme, :authority, :path
     nsILoadGroupConnectionInfo *loadGroupCI = mTransaction->LoadGroupConnectionInfo();
     SpdyPushCache *cache = nullptr;
     if (loadGroupCI)
       loadGroupCI->GetSpdyPushCache(&cache);
 
     Http2PushedStream *pushedStream = nullptr;
     // we remove the pushedstream from the push cache so that
@@ -342,17 +349,17 @@ Http2Stream::ParseHttpRequestHeaders(con
 
   // It is now OK to assign a streamID that we are assured will
   // be monotonically increasing amongst new streams on this
   // session
   mStreamID = mSession->RegisterStreamID(this);
   MOZ_ASSERT(mStreamID & 1, "Http2 Stream Channel ID must be odd");
   LOG3(("Stream ID 0x%X [session=%p] for URI %s\n",
         mStreamID, mSession,
-        nsCString(mTransaction->RequestHead()->RequestURI()).get()));
+        nsCString(head->RequestURI()).get()));
 
   if (mStreamID >= 0x80000000) {
     // streamID must fit in 31 bits. Evading This is theoretically possible
     // because stream ID assignment is asynchronous to stream creation
     // because of the protocol requirement that the new stream ID
     // be monotonically increasing. In reality this is really not possible
     // because new streams stop being added to a session with millions of
     // IDs still available and no race condition is going to bridge that gap;
@@ -361,38 +368,56 @@ Http2Stream::ParseHttpRequestHeaders(con
     LOG3(("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 HTTP/2 headers by writing to mTxInlineFrame{sz}
 
   nsCString compressedData;
+  nsDependentCString scheme(head->IsHTTPS() ? "https" : "http");
+  if (head->IsConnect()) {
+    MOZ_ASSERT(mTransaction->QuerySpdyConnectTransaction());
+    mIsTunnel = true;
+    mRequestBodyLenRemaining = 0x0fffffffffffffffULL;
+
+    // Our normal authority has an implicit port, best to use an
+    // explicit one with a tunnel
+    nsHttpConnectionInfo *ci = mTransaction->ConnectionInfo();
+    if (!ci) {
+      return NS_ERROR_UNEXPECTED;
+    }
+    authorityHeader = ci->GetHost();
+    authorityHeader.AppendLiteral(":");
+    authorityHeader.AppendInt(ci->Port());
+  }
+
   mSession->Compressor()->EncodeHeaderBlock(mFlatHttpRequestHeaders,
-                                            mTransaction->RequestHead()->Method(),
-                                            mTransaction->RequestHead()->RequestURI(),
-                                            hostHeader,
-                                            NS_LITERAL_CSTRING("https"),
+                                            head->Method(),
+                                            head->RequestURI(),
+                                            authorityHeader,
+                                            scheme,
+                                            head->IsConnect(),
                                             compressedData);
 
   // Determine whether to put the fin bit on the header frame or whether
   // to wait for a data packet to put it on.
   uint8_t firstFrameFlags =  Http2Session::kFlag_PRIORITY;
 
-  if (mTransaction->RequestHead()->IsGet() ||
-      mTransaction->RequestHead()->IsConnect() ||
-      mTransaction->RequestHead()->IsHead()) {
-    // for GET, CONNECT, and HEAD place the fin bit right on the
+  if (head->IsGet() ||
+      head->IsHead()) {
+    // for GET and HEAD place the fin bit right on the
     // header packet
 
     SetSentFin(true);
     firstFrameFlags |= Http2Session::kFlag_END_STREAM;
-  } else if (mTransaction->RequestHead()->IsPost() ||
-             mTransaction->RequestHead()->IsPut() ||
-             mTransaction->RequestHead()->IsOptions()) {
+  } else if (head->IsPost() ||
+             head->IsPut() ||
+             head->IsConnect() ||
+             head->IsOptions()) {
     // place fin in a data frame even for 0 length messages for iterop
   } else if (!mRequestBodyLenRemaining) {
     // for other HTTP extension methods, rely on the content-length
     // to determine whether or not to put fin on headers
     SetSentFin(true);
     firstFrameFlags |= Http2Session::kFlag_END_STREAM;
   }
 
@@ -468,17 +493,17 @@ Http2Stream::ParseHttpRequestHeaders(con
     outputOffset += frameLen;
   }
 
   Telemetry::Accumulate(Telemetry::SPDY_SYN_SIZE, compressedData.Length());
 
   // The size of the input headers is approximate
   uint32_t ratio =
     compressedData.Length() * 100 /
-    (11 + mTransaction->RequestHead()->RequestURI().Length() +
+    (11 + head->RequestURI().Length() +
      mFlatHttpRequestHeaders.Length());
 
   const char *beginBuffer = mFlatHttpRequestHeaders.BeginReading();
   int32_t crlfIndex = mFlatHttpRequestHeaders.Find("\r\n");
   while (true) {
     int32_t startIndex = crlfIndex + 2;
 
     crlfIndex = mFlatHttpRequestHeaders.Find("\r\n", false, startIndex);
@@ -594,16 +619,20 @@ Http2Stream::AdjustPushedPriority()
   LOG3(("AdjustPushedPriority %p id 0x%X to weight %X\n", this, mPushSource->mStreamID,
         mPriorityWeight));
 }
 
 void
 Http2Stream::UpdateTransportReadEvents(uint32_t count)
 {
   mTotalRead += count;
+  if (!mSocketTransport) {
+    return;
+  }
+
   mTransaction->OnTransportStatus(mSocketTransport,
                                   NS_NET_STATUS_RECEIVING_FROM,
                                   mTotalRead);
 }
 
 void
 Http2Stream::UpdateTransportSendEvents(uint32_t count)
 {
@@ -826,26 +855,39 @@ Http2Stream::ConvertResponseHeaders(Http
 
   nsAutoCString status;
   decompressor->GetStatus(status);
   if (status.IsEmpty()) {
     LOG3(("Http2Stream::ConvertHeaders %p Error - no status\n", this));
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
+  if (mIsTunnel) {
+    nsresult errcode;
+    if (status.ToInteger(&errcode) != 200) {
+      LOG3(("Http2Stream %p Tunnel not 200", this));
+      return NS_ERROR_ABORT;
+    }
+  }
+
   if (aHeadersIn.Length() && aHeadersOut.Length()) {
     Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_SIZE, aHeadersIn.Length());
     uint32_t ratio =
       aHeadersIn.Length() * 100 / aHeadersOut.Length();
     Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_RATIO, ratio);
   }
 
   aHeadersIn.Truncate();
   aHeadersOut.Append(NS_LITERAL_CSTRING("X-Firefox-Spdy: " NS_HTTP2_DRAFT_TOKEN "\r\n\r\n"));
   LOG (("decoded response headers are:\n%s", aHeadersOut.BeginReading()));
+  if (mIsTunnel) {
+    aHeadersOut.Truncate();
+    LOG(("SpdyStream3::ConvertHeaders %p 0x%X headers removed for tunnel\n",
+         this, mStreamID));
+  }
   return NS_OK;
 }
 
 // ConvertHeaders is used to convert the response headers
 // into HTTP/1 format and report some telemetry
 nsresult
 Http2Stream::ConvertPushHeaders(Http2Decompressor *decompressor,
                                 nsACString &aHeadersIn,
@@ -886,16 +928,23 @@ Http2Stream::ConvertPushHeaders(Http2Dec
 }
 
 void
 Http2Stream::Close(nsresult reason)
 {
   mTransaction->Close(reason);
 }
 
+nsresult
+Http2Stream::SetAllHeadersReceived(bool aStatus)
+{
+  mAllHeadersReceived = aStatus ? 1 : 0;
+  return NS_OK;
+}
+
 bool
 Http2Stream::AllowFlowControlledWrite()
 {
   return (mSession->ServerSessionWindow() > 0) && (mServerReceiveWindow > 0);
 }
 
 void
 Http2Stream::UpdateServerReceiveWindow(int32_t delta)
@@ -1052,21 +1101,25 @@ Http2Stream::OnReadSegment(const char *b
           "avail=%d chunksize=%d stream window=%d session window=%d "
           "max frame=%d USING=%d\n", this, mStreamID,
           count, mChunkSize, mServerReceiveWindow, mSession->ServerSessionWindow(),
           Http2Session::kMaxFrameData, dataLength));
 
     mSession->DecrementServerSessionWindow(dataLength);
     mServerReceiveWindow -= dataLength;
 
-    LOG3(("Http2Stream %p id %x request len remaining %d, "
-          "count avail %d, chunk used %d",
+    LOG3(("Http2Stream %p id %x request len remaining %u, "
+          "count avail %u, chunk used %u",
           this, mStreamID, mRequestBodyLenRemaining, count, dataLength));
-    if (dataLength > mRequestBodyLenRemaining)
+    if (!dataLength && mRequestBodyLenRemaining) {
+      return NS_BASE_STREAM_WOULD_BLOCK;
+    }
+    if (dataLength > mRequestBodyLenRemaining) {
       return NS_ERROR_UNEXPECTED;
+    }
     mRequestBodyLenRemaining -= dataLength;
     GenerateDataFrameHeader(dataLength, !mRequestBodyLenRemaining);
     ChangeState(SENDING_BODY);
     // NO BREAK
 
   case SENDING_BODY:
     MOZ_ASSERT(mTxInlineFrameUsed, "OnReadSegment Send Data Header 0b");
     rv = TransmitFrame(buf, countRead, false);
@@ -1122,11 +1175,33 @@ Http2Stream::OnWriteSegment(char *buf,
   rv = mPushSource->GetBufferedData(buf, count, countWritten);
   if (NS_FAILED(rv))
     return rv;
 
   mSession->ConnectPushedStream(this);
   return NS_OK;
 }
 
+/// connect tunnels
+
+void
+Http2Stream::ClearTransactionsBlockedOnTunnel()
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+
+  if (!mIsTunnel) {
+    return;
+  }
+  gHttpHandler->ConnMgr()->ProcessPendingQ(mTransaction->ConnectionInfo());
+}
+
+void
+Http2Stream::MapStreamToHttpConnection()
+{
+  nsRefPtr<SpdyConnectTransaction> qiTrans(mTransaction->QuerySpdyConnectTransaction());
+  MOZ_ASSERT(qiTrans);
+  qiTrans->MapStreamToHttpConnection(mSocketTransport,
+                                     mTransaction->ConnectionInfo());
+}
+
 } // namespace mozilla::net
 } // namespace mozilla
 
--- a/netwerk/protocol/http/Http2Stream.h
+++ b/netwerk/protocol/http/Http2Stream.h
@@ -76,22 +76,25 @@ public:
   bool RecvdReset() { return mRecvdReset; }
 
   void SetSentReset(bool aStatus);
   bool SentReset() { return mSentReset; }
 
   void SetCountAsActive(bool aStatus) { mCountAsActive = aStatus ? 1 : 0; }
   bool CountAsActive() { return mCountAsActive; }
 
-  void SetAllHeadersReceived(bool aStatus) { mAllHeadersReceived = aStatus ? 1 : 0; }
+ // returns failure if stream cannot be made ready and stream
+ // should be canceled
+  nsresult SetAllHeadersReceived(bool aStatus);
   bool AllHeadersReceived() { return mAllHeadersReceived; }
 
   void UpdateTransportSendEvents(uint32_t count);
   void UpdateTransportReadEvents(uint32_t count);
 
+  // NS_ERROR_ABORT terminates stream, other failure terminates session
   nsresult ConvertResponseHeaders(Http2Decompressor *, nsACString &, nsACString &);
   nsresult ConvertPushHeaders(Http2Decompressor *, nsACString &, nsACString &);
 
   bool AllowFlowControlledWrite();
   void UpdateServerReceiveWindow(int32_t delta);
   int64_t ServerReceiveWindow() { return mServerReceiveWindow; }
 
   void DecrementClientReceiveWindow(uint32_t delta) {
@@ -229,17 +232,18 @@ private:
 
   // Buffer for request header compression.
   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.
+  // problems with the google servers. Connect does rely on stream
+  // close by setting this to the max value.
   int64_t                      mRequestBodyLenRemaining;
 
   uint32_t                     mPriority;
   uint8_t                      mPriorityWeight;
 
   // mClientReceiveWindow, mServerReceiveWindow, and mLocalUnacked are for flow control.
   // *window are signed because the race conditions in asynchronous SETTINGS
   // messages can force them temporarily negative.
@@ -262,14 +266,23 @@ private:
   bool                         mBlockedOnRwin;
 
   // For Progress Events
   uint64_t                     mTotalSent;
   uint64_t                     mTotalRead;
 
   // For Http2Push
   Http2PushedStream *mPushSource;
+
+/// connect tunnels
+public:
+  bool IsTunnel() { return mIsTunnel; }
+private:
+  void ClearTransactionsBlockedOnTunnel();
+  void MapStreamToHttpConnection();
+
+  bool mIsTunnel;
 };
 
 } // namespace mozilla::net
 } // namespace mozilla
 
 #endif // mozilla_net_Http2Stream_h
--- a/netwerk/protocol/http/SpdySession31.cpp
+++ b/netwerk/protocol/http/SpdySession31.cpp
@@ -346,33 +346,33 @@ SpdySession31::AddStream(nsAHttpTransact
 
   // integrity check
   if (mStreamTransactionHash.Get(aHttpTransaction)) {
     LOG3(("   New transaction already present\n"));
     MOZ_ASSERT(false, "AddStream duplicate transaction pointer");
     return false;
   }
 
-  // assert that
-  // a] in the case we have a connection, that the new transaction connection
-  //    is either undefined or on the same connection
-  // b] in the case we don't have a connection, that the new transaction
-  //    connection is defined so we can adopt it
-  MOZ_ASSERT((mConnection && (!aHttpTransaction->Connection() ||
-                              mConnection == aHttpTransaction->Connection())) ||
-             (!mConnection && aHttpTransaction->Connection()));
-
   if (!mConnection) {
     mConnection = aHttpTransaction->Connection();
   }
+
   aHttpTransaction->SetConnection(this);
+
+  if (aUseTunnel) {
+    LOG3(("SpdySession31::AddStream session=%p trans=%p OnTunnel",
+          this, aHttpTransaction));
+    DispatchOnTunnel(aHttpTransaction, aCallbacks);
+    return true;
+  }
+
   SpdyStream31 *stream = new SpdyStream31(aHttpTransaction, this, aPriority);
 
-  LOG3(("SpdySession31::AddStream session=%p stream=%p NextID=0x%X (tentative)",
-        this, stream, mNextStreamID));
+  LOG3(("SpdySession31::AddStream session=%p stream=%p serial=%u "
+        "NextID=0x%X (tentative)", this, stream, mSerial, mNextStreamID));
 
   mStreamTransactionHash.Put(aHttpTransaction, stream);
 
   if (RoomForMoreConcurrent()) {
     LOG3(("SpdySession31::AddStream %p stream %p activated immediately.",
           this, stream));
     ActivateStream(stream);
   }
@@ -941,16 +941,20 @@ SpdySession31::CloseStream(SpdyStream31 
   if (aStream == mInputFrameDataStream) {
     LOG3(("Stream had active partial read frame on close"));
     ChangeDownstreamState(DISCARDING_DATA_FRAME);
     mInputFrameDataStream = nullptr;
   }
 
   RemoveStreamFromQueues(aStream);
 
+  if (aStream->IsTunnel()) {
+    UnRegisterTunnel(aStream);
+  }
+
   // Send the stream the close() indication
   aStream->Close(aResult);
 }
 
 nsresult
 SpdySession31::HandleSynStream(SpdySession31 *self)
 {
   MOZ_ASSERT(self->mFrameControlType == CONTROL_TYPE_SYN_STREAM);
@@ -1062,17 +1066,23 @@ SpdySession31::HandleSynStream(SpdySessi
   // ownership of the pushed stream is by the transaction hash, just as it
   // is for a client initiated stream. Errors that aren't fatal to the
   // whole session must call cleanupStream() after this point in order
   // to remove the stream from that hash.
   self->mStreamTransactionHash.Put(transactionBuffer, pushedStream);
   self->mPushedStreams.AppendElement(pushedStream);
 
   // The pushed stream is unidirectional so it is fully open immediately
-  pushedStream->SetFullyOpen();
+  rv = pushedStream->SetFullyOpen();
+  if (NS_FAILED(rv)) {
+    LOG(("SpdySession31::HandleSynStream pushedstream fully open failed\n"));
+    self->CleanupStream(pushedStream, rv, RST_CANCEL);
+    self->ResetDownstreamState();
+    return NS_OK;
+  }
 
   // Uncompress the response headers into a stream specific buffer, leaving them
   // in spdy format for the time being.
   rv = pushedStream->Uncompress(&self->mDownstreamZlib,
                                 self->mInputFrameBuffer + 18,
                                 self->mInputFrameDataSize - 10);
   if (NS_FAILED(rv)) {
     LOG(("SpdySession31::HandleSynStream uncompress failed\n"));
@@ -1135,20 +1145,20 @@ SpdySession31::HandleSynReply(SpdySessio
 
   if (self->mInputFrameDataSize < 4) {
     LOG3(("SpdySession31::HandleSynReply %p SYN REPLY too short data=%d",
           self, self->mInputFrameDataSize));
     // A framing error is a session wide error that cannot be recovered
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
-  LOG3(("SpdySession31::HandleSynReply %p lookup via streamID in syn_reply.\n",
-        self));
   uint32_t streamID =
     PR_ntohl(reinterpret_cast<uint32_t *>(self->mInputFrameBuffer.get())[2]);
+  LOG3(("SpdySession31::HandleSynReply %p lookup via streamID 0x%X in syn_reply.\n",
+        self, streamID));
   nsresult rv = self->SetInputFrameDataStream(streamID);
   if (NS_FAILED(rv))
     return rv;
 
   if (!self->mInputFrameDataStream) {
     // Cannot find stream. We can continue the SPDY session, but we need to
     // uncompress the header block to maintain the correct compression context
 
@@ -1201,17 +1211,29 @@ SpdySession31::HandleSynReply(SpdySessio
           self->mInputFrameDataStream->RecvdFin()));
 
     self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ALREADY_OPENED,
                         self->mInputFrameDataStream->RecvdFin() ?
                         RST_STREAM_ALREADY_CLOSED : RST_STREAM_IN_USE);
     self->ResetDownstreamState();
     return NS_OK;
   }
-  self->mInputFrameDataStream->SetFullyOpen();
+
+  rv = self->mInputFrameDataStream->SetFullyOpen();
+  if (NS_FAILED(rv)) {
+    LOG(("SpdySession31::HandleSynReply SetFullyOpen failed\n"));
+    if (self->mInputFrameDataStream->IsTunnel()) {
+      gHttpHandler->ConnMgr()->CancelTransactions(
+        self->mInputFrameDataStream->Transaction()->ConnectionInfo(),
+        NS_ERROR_CONNECTION_REFUSED);
+    }
+    self->CleanupStream(self->mInputFrameDataStream, rv, RST_CANCEL);
+    self->ResetDownstreamState();
+    return NS_OK;
+  }
 
   self->mInputFrameDataLast = self->mInputFrameBuffer[4] & kFlag_Data_FIN;
   self->mInputFrameDataStream->UpdateTransportReadEvents(self->mInputFrameDataSize);
   self->mLastDataReadEpoch = self->mLastReadEpoch;
 
   if (self->mInputFrameBuffer[4] & ~kFlag_Data_FIN) {
     LOG3(("SynReply %p had undefined flag set 0x%X\n", self, streamID));
     self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ILLEGAL_VALUE,
@@ -2062,42 +2084,42 @@ SpdySession31::WriteSegments(nsAHttpSegm
   if (mDownstreamState == PROCESSING_DATA_FRAME ||
       mDownstreamState == PROCESSING_COMPLETE_HEADERS) {
 
     // The cleanup stream should only be set while stream->WriteSegments is
     // on the stack and then cleaned up in this code block afterwards.
     MOZ_ASSERT(!mNeedsCleanup, "cleanup stream set unexpectedly");
     mNeedsCleanup = nullptr;                     /* just in case */
 
+    SpdyStream31 *stream = mInputFrameDataStream;
     mSegmentWriter = writer;
     rv = mInputFrameDataStream->WriteSegments(this, count, countWritten);
     mSegmentWriter = nullptr;
 
     mLastDataReadEpoch = mLastReadEpoch;
 
     if (SoftStreamError(rv)) {
       // This will happen when the transaction figures out it is EOF, generally
       // due to a content-length match being made. Return OK from this function
       // otherwise the whole session would be torn down.
-      SpdyStream31 *stream = mInputFrameDataStream;
 
       // if we were doing PROCESSING_COMPLETE_HEADERS need to pop the state
       // back to PROCESSING_DATA_FRAME where we came from
       mDownstreamState = PROCESSING_DATA_FRAME;
 
       if (mInputFrameDataRead == mInputFrameDataSize)
         ResetDownstreamState();
       LOG3(("SpdySession31::WriteSegments session=%p stream=%p 0x%X "
             "needscleanup=%p. cleanup stream based on "
             "stream->writeSegments returning code %X\n",
             this, stream, stream ? stream->StreamID() : 0,
             mNeedsCleanup, rv));
       CleanupStream(stream, NS_OK, RST_CANCEL);
-      MOZ_ASSERT(!mNeedsCleanup, "double cleanup out of data frame");
-      mNeedsCleanup = nullptr;                     /* just in case */
+      MOZ_ASSERT(!mNeedsCleanup || mNeedsCleanup == stream);
+      mNeedsCleanup = nullptr;
       return NS_OK;
     }
 
     if (mNeedsCleanup) {
       LOG3(("SpdySession31::WriteSegments session=%p stream=%p 0x%X "
             "cleanup stream based on mNeedsCleanup.\n",
             this, mNeedsCleanup, mNeedsCleanup ? mNeedsCleanup->StreamID() : 0));
       CleanupStream(mNeedsCleanup, NS_OK, RST_CANCEL);
@@ -2574,16 +2596,89 @@ SpdySession31::SetNeedsCleanup()
 
 void
 SpdySession31::ConnectPushedStream(SpdyStream31 *stream)
 {
   mReadyForRead.Push(stream);
   ForceRecv();
 }
 
+uint32_t
+SpdySession31::FindTunnelCount(nsHttpConnectionInfo *aConnInfo)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  uint32_t rv = 0;
+  mTunnelHash.Get(aConnInfo->HashKey(), &rv);
+  return rv;
+}
+
+void
+SpdySession31::RegisterTunnel(SpdyStream31 *aTunnel)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
+  uint32_t newcount = FindTunnelCount(ci) + 1;
+  mTunnelHash.Remove(ci->HashKey());
+  mTunnelHash.Put(ci->HashKey(), newcount);
+  LOG3(("SpdySession31::RegisterTunnel %p stream=%p tunnels=%d [%s]",
+        this, aTunnel, newcount, ci->HashKey().get()));
+}
+
+void
+SpdySession31::UnRegisterTunnel(SpdyStream31 *aTunnel)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
+  MOZ_ASSERT(FindTunnelCount(ci));
+  uint32_t newcount = FindTunnelCount(ci) - 1;
+  mTunnelHash.Remove(ci->HashKey());
+  if (newcount) {
+    mTunnelHash.Put(ci->HashKey(), newcount);
+  }
+  LOG3(("SpdySession31::UnRegisterTunnel %p stream=%p tunnels=%d [%s]",
+        this, aTunnel, newcount, ci->HashKey().get()));
+}
+
+void
+SpdySession31::DispatchOnTunnel(nsAHttpTransaction *aHttpTransaction,
+                                nsIInterfaceRequestor *aCallbacks)
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+  nsHttpTransaction *trans = aHttpTransaction->QueryHttpTransaction();
+  nsHttpConnectionInfo *ci = aHttpTransaction->ConnectionInfo();
+  MOZ_ASSERT(trans);
+
+  LOG3(("SpdySession31::DispatchOnTunnel %p trans=%p", this, trans));
+
+  aHttpTransaction->SetConnection(nullptr);
+
+  // this transaction has done its work of setting up a tunnel, let
+  // the connection manager queue it if necessary
+  trans->SetDontRouteViaWildCard(true);
+
+  if (FindTunnelCount(ci) < gHttpHandler->MaxConnectionsPerOrigin()) {
+    LOG3(("SpdySession31::DispatchOnTunnel %p create on new tunnel %s",
+          this, ci->HashKey().get()));
+    nsRefPtr<SpdyConnectTransaction> connectTrans =
+      new SpdyConnectTransaction(ci, aCallbacks,
+                                 trans->Caps(), trans, this);
+    AddStream(connectTrans, trans->Priority(),
+              false, nullptr);
+    SpdyStream31 *tunnel = mStreamTransactionHash.Get(connectTrans);
+    MOZ_ASSERT(tunnel);
+    RegisterTunnel(tunnel);
+  }
+
+  // requeue it. The connection manager is responsible for actually putting
+  // this on the tunnel connection with the specific ci now that it
+  // has DontRouteViaWildCard set.
+  trans->EnableKeepAlive();
+  gHttpHandler->InitiateTransaction(trans, trans->Priority());
+}
+
 nsresult
 SpdySession31::BufferOutput(const char *buf,
                             uint32_t count,
                             uint32_t *countRead)
 {
   nsAHttpSegmentReader *old = mSegmentReader;
   mSegmentReader = nullptr;
   nsresult rv = OnReadSegment(buf, count, countRead);
@@ -2611,27 +2706,33 @@ SpdySession31::TransactionHasDataToWrite
     return;
   }
 
   LOG3(("SpdySession31::TransactionHasDataToWrite %p ID is 0x%X\n",
         this, stream->StreamID()));
 
   mReadyForWrite.Push(stream);
   SetWriteCallbacks();
+
+  // NSPR poll will not poll the network if there are non system PR_FileDesc's
+  // that are ready - so we can get into a deadlock waiting for the system IO
+  // to come back here if we don't force the send loop manually.
+  ForceSend();
 }
 
 void
 SpdySession31::TransactionHasDataToWrite(SpdyStream31 *stream)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
   LOG3(("SpdySession31::TransactionHasDataToWrite %p stream=%p ID=%x",
         this, stream, stream->StreamID()));
 
   mReadyForWrite.Push(stream);
   SetWriteCallbacks();
+  ForceSend();
 }
 
 bool
 SpdySession31::IsPersistent()
 {
   return true;
 }
 
--- a/netwerk/protocol/http/SpdySession31.h
+++ b/netwerk/protocol/http/SpdySession31.h
@@ -18,16 +18,17 @@
 #include "zlib.h"
 
 class nsISocketTransport;
 
 namespace mozilla { namespace net {
 
 class SpdyPushedStream31;
 class SpdyStream31;
+class nsHttpTransaction;
 
 class SpdySession31 MOZ_FINAL : public ASpdySession
                               , public nsAHttpConnection
                               , public nsAHttpSegmentReader
                               , public nsAHttpSegmentWriter
 {
 public:
   NS_DECL_ISUPPORTS
@@ -397,13 +398,22 @@ private:
 
   // used as a temporary buffer while enumerating the stream hash during GoAway
   nsDeque  mGoAwayStreamsToRestart;
 
   // Each session gets a unique serial number because the push cache is correlated
   // by the load group and the serial number can be used as part of the cache key
   // to make sure streams aren't shared across sessions.
   uint64_t        mSerial;
+
+private:
+/// connect tunnels
+  void DispatchOnTunnel(nsAHttpTransaction *, nsIInterfaceRequestor *);
+  void RegisterTunnel(SpdyStream31 *);
+  void UnRegisterTunnel(SpdyStream31 *);
+  uint32_t FindTunnelCount(nsHttpConnectionInfo *);
+
+  nsDataHashtable<nsCStringHashKey, uint32_t> mTunnelHash;
 };
 
 }} // namespace mozilla::net
 
 #endif // mozilla_net_SpdySession31_h
--- a/netwerk/protocol/http/SpdyStream31.cpp
+++ b/netwerk/protocol/http/SpdyStream31.cpp
@@ -19,16 +19,17 @@
 #include "nsHttpHandler.h"
 #include "nsHttpRequestHead.h"
 #include "nsISocketTransport.h"
 #include "nsISupportsPriority.h"
 #include "prnetdb.h"
 #include "SpdyPush31.h"
 #include "SpdySession31.h"
 #include "SpdyStream31.h"
+#include "TunnelUtils.h"
 
 #include <algorithm>
 
 #ifdef DEBUG
 // defined by the socket transport service while active
 extern PRThread *gSocketThread;
 #endif
 
@@ -63,30 +64,32 @@ SpdyStream31::SpdyStream31(nsAHttpTransa
   , mDecompressedBytes(0)
   , mRequestBodyLenRemaining(0)
   , mPriority(priority)
   , mLocalUnacked(0)
   , mBlockedOnRwin(false)
   , mTotalSent(0)
   , mTotalRead(0)
   , mPushSource(nullptr)
+  , mIsTunnel(false)
 {
   MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
 
   LOG3(("SpdyStream31::SpdyStream31 %p", this));
 
   mRemoteWindow = spdySession->GetServerInitialStreamWindow();
   mLocalWindow = spdySession->PushAllowance();
 
   mTxInlineFrame = new uint8_t[mTxInlineFrameSize];
   mDecompressBuffer = new char[mDecompressBufferSize];
 }
 
 SpdyStream31::~SpdyStream31()
 {
+  ClearTransactionsBlockedOnTunnel();
   mStreamID = SpdySession31::kDeadStreamID;
 }
 
 // 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.
 
@@ -114,17 +117,18 @@ SpdyStream31::ReadSegments(nsAHttpSegmen
   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 = nullptr;
-
+    LOG3(("SpdyStream31::ReadSegments %p trans readsegments rv %x read=%d\n",
+          this, rv, *countRead));
     // Check to see if the transaction's request could be written out now.
     // If not, mark the stream for callback when writing can proceed.
     if (NS_SUCCEEDED(rv) &&
         mUpstreamState == GENERATING_SYN_STREAM &&
         !mSynFrameComplete)
       mSession->TransactionHasDataToWrite(this);
 
     // mTxinlineFrameUsed represents any queued un-sent frame. It might
@@ -139,17 +143,18 @@ SpdyStream31::ReadSegments(nsAHttpSegmen
     if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mTxInlineFrameUsed)
       mRequestBlockedOnRead = 1;
 
     // If the sending flow control window is open (!mBlockedOnRwin) then
     // continue sending the request
     if (!mBlockedOnRwin &&
         !mTxInlineFrameUsed && NS_SUCCEEDED(rv) && (!*countRead)) {
       LOG3(("SpdyStream31::ReadSegments %p 0x%X: Sending request data complete, "
-            "mUpstreamState=%x",this, mStreamID, mUpstreamState));
+            "mUpstreamState=%x finondata=%d",this, mStreamID,
+            mUpstreamState, mSentFinOnData));
       if (mSentFinOnData) {
         ChangeState(UPSTREAM_COMPLETE);
       }
       else {
         GenerateDataFrameHeader(0, true);
         ChangeState(SENDING_FIN_STREAM);
         mSession->TransactionHasDataToWrite(this);
         rv = NS_BASE_STREAM_WOULD_BLOCK;
@@ -459,58 +464,85 @@ SpdyStream31::ParseHttpRequestHeaders(co
 
   mTxInlineFrameUsed = 18;
 
   // Do not naively log the request headers here beacuse they might
   // contain auth. The http transaction already logs the sanitized request
   // headers at this same level so it is not necessary to do so here.
 
   const char *methodHeader = mTransaction->RequestHead()->Method().get();
+  LOG3(("Stream method %p 0x%X %s\n", this, mStreamID, methodHeader));
 
   // The header block length
-  uint16_t count = hdrHash.Count() + 5; /* method, path, version, host, scheme */
+  uint16_t count = hdrHash.Count() + 4; /* :method, :path, :version, :host */
+  if (mTransaction->RequestHead()->IsConnect()) {
+    mRequestBodyLenRemaining = 0x0fffffffffffffffULL;
+  } else {
+    ++count; // :scheme used if not connect
+  }
   CompressToFrame(count);
 
   // :method, :path, :version comprise a HTTP/1 request line, so send those first
   // to make life easy for any gateways
   CompressToFrame(NS_LITERAL_CSTRING(":method"));
   CompressToFrame(methodHeader, strlen(methodHeader));
+
   CompressToFrame(NS_LITERAL_CSTRING(":path"));
-  CompressToFrame(mTransaction->RequestHead()->RequestURI());
+  if (!mTransaction->RequestHead()->IsConnect()) {
+    CompressToFrame(mTransaction->RequestHead()->RequestURI());
+  } else {
+    MOZ_ASSERT(mTransaction->QuerySpdyConnectTransaction());
+    mIsTunnel = true;
+    // Connect places host:port in :path. Don't use default port.
+    nsHttpConnectionInfo *ci = mTransaction->ConnectionInfo();
+    if (!ci) {
+      return NS_ERROR_UNEXPECTED;
+    }
+    nsAutoCString route;
+    route = ci->GetHost();
+    route.AppendLiteral(":");
+    route.AppendInt(ci->Port());
+    CompressToFrame(route);
+  }
+
   CompressToFrame(NS_LITERAL_CSTRING(":version"));
   CompressToFrame(versionHeader);
 
   CompressToFrame(NS_LITERAL_CSTRING(":host"));
   CompressToFrame(hostHeader);
-  CompressToFrame(NS_LITERAL_CSTRING(":scheme"));
-  CompressToFrame(nsDependentCString(mTransaction->RequestHead()->IsHTTPS() ? "https" : "http"));
+
+  if (!mTransaction->RequestHead()->IsConnect()) {
+    // no :scheme with connect
+    CompressToFrame(NS_LITERAL_CSTRING(":scheme"));
+    CompressToFrame(nsDependentCString(mTransaction->RequestHead()->IsHTTPS() ? "https" : "http"));
+  }
 
   hdrHash.Enumerate(hdrHashEnumerate, this);
   CompressFlushFrame();
 
   // 4 to 7 are length and flags, which we can now fill in
   (reinterpret_cast<uint32_t *>(mTxInlineFrame.get()))[1] =
     PR_htonl(mTxInlineFrameUsed - 8);
 
   MOZ_ASSERT(!mTxInlineFrame[4], "Size greater than 24 bits");
 
   // Determine whether to put the fin bit on the syn stream frame or whether
   // to wait for a data packet to put it on.
 
   if (mTransaction->RequestHead()->IsGet() ||
-      mTransaction->RequestHead()->IsConnect() ||
       mTransaction->RequestHead()->IsHead()) {
-    // for GET, CONNECT, and HEAD place the fin bit right on the
+    // for GET and HEAD place the fin bit right on the
     // syn stream packet
 
     mSentFinOnData = 1;
     mTxInlineFrame[4] = SpdySession31::kFlag_Data_FIN;
   }
   else if (mTransaction->RequestHead()->IsPost() ||
            mTransaction->RequestHead()->IsPut() ||
+           mTransaction->RequestHead()->IsConnect() ||
            mTransaction->RequestHead()->IsOptions()) {
     // place fin in a data frame even for 0 length messages, I've seen
     // the google gateway be unhappy with fin-on-syn for 0 length POST
   }
   else if (!mRequestBodyLenRemaining) {
     // for other HTTP extension methods, rely on the content-length
     // to determine whether or not to put fin on syn
     mSentFinOnData = 1;
@@ -595,16 +627,19 @@ SpdyStream31::AdjustInitialWindow()
   LOG3(("AdjustInitialwindow %p 0x%X %u\n",
         this, stream->mStreamID, PR_ntohl(toack)));
 }
 
 void
 SpdyStream31::UpdateTransportReadEvents(uint32_t count)
 {
   mTotalRead += count;
+  if (!mSocketTransport) {
+    return;
+  }
 
   mTransaction->OnTransportStatus(mSocketTransport,
                                   NS_NET_STATUS_RECEIVING_FROM,
                                   mTotalRead);
 }
 
 void
 SpdyStream31::UpdateTransportSendEvents(uint32_t count)
@@ -1257,16 +1292,22 @@ SpdyStream31::ConvertHeaders(nsACString 
   LOG (("decoded response headers are:\n%s",
         aHeadersOut.BeginReading()));
 
   // The spdy formatted buffer isnt needed anymore - free it up
   mDecompressBuffer = nullptr;
   mDecompressBufferSize = 0;
   mDecompressBufferUsed = 0;
 
+  if (mIsTunnel) {
+    aHeadersOut.Truncate();
+    LOG(("SpdyStream31::ConvertHeaders %p 0x%X headers removed for tunnel\n",
+         this, mStreamID));
+  }
+
   return NS_OK;
 }
 
 void
 SpdyStream31::ExecuteCompress(uint32_t flushMode)
 {
   // Expect mZlib->avail_in and mZlib->next_in to be set.
   // Append the compressed version of next_in to mTxInlineFrame
@@ -1322,16 +1363,48 @@ SpdyStream31::CompressToFrame(const char
 void
 SpdyStream31::CompressFlushFrame()
 {
   mZlib->next_in = (unsigned char *) "";
   mZlib->avail_in = 0;
   ExecuteCompress(Z_SYNC_FLUSH);
 }
 
+bool
+SpdyStream31::GetFullyOpen()
+{
+  return mFullyOpen;
+}
+
+nsresult
+SpdyStream31::SetFullyOpen()
+{
+  MOZ_ASSERT(!mFullyOpen);
+  mFullyOpen = 1;
+  if (mIsTunnel) {
+    nsDependentCSubstring statusSubstring;
+    nsresult rv = FindHeader(NS_LITERAL_CSTRING(":status"),
+                             statusSubstring);
+    if (NS_SUCCEEDED(rv)) {
+      nsCString status(statusSubstring);
+      nsresult errcode;
+
+      if (status.ToInteger(&errcode) != 200) {
+        LOG3(("SpdyStream31::SetFullyOpen %p Tunnel not 200", this));
+        return NS_ERROR_FAILURE;
+      }
+      LOG3(("SpdyStream31::SetFullyOpen %p Tunnel 200 OK", this));
+    }
+
+    MapStreamToHttpConnection();
+    ClearTransactionsBlockedOnTunnel();
+  }
+  return NS_OK;
+}
+
 void
 SpdyStream31::Close(nsresult reason)
 {
   mTransaction->Close(reason);
 }
 
 void
 SpdyStream31::UpdateRemoteWindow(int32_t delta)
@@ -1415,21 +1488,25 @@ SpdyStream31::OnReadSegment(const char *
 
     LOG3(("SpdyStream31 this=%p id 0x%X remote window is stream %ld and "
           "session %ld. Chunk is %d\n",
           this, mStreamID, mRemoteWindow, mSession->RemoteSessionWindow(),
           dataLength));
     mRemoteWindow -= dataLength;
     mSession->DecrementRemoteSessionWindow(dataLength);
 
-    LOG3(("SpdyStream31 %p id %x request len remaining %d, "
-          "count avail %d, chunk used %d",
+    LOG3(("SpdyStream31 %p id %x request len remaining %u, "
+          "count avail %u, chunk used %u",
           this, mStreamID, mRequestBodyLenRemaining, count, dataLength));
-    if (dataLength > mRequestBodyLenRemaining)
+    if (!dataLength && mRequestBodyLenRemaining) {
+      return NS_BASE_STREAM_WOULD_BLOCK;
+    }
+    if (dataLength > mRequestBodyLenRemaining) {
       return NS_ERROR_UNEXPECTED;
+    }
     mRequestBodyLenRemaining -= dataLength;
     GenerateDataFrameHeader(dataLength, !mRequestBodyLenRemaining);
     ChangeState(SENDING_REQUEST_BODY);
     // NO BREAK
 
   case SENDING_REQUEST_BODY:
     MOZ_ASSERT(mTxInlineFrameUsed, "OnReadSegment Send Data Header 0b");
     rv = TransmitFrame(buf, countRead, false);
@@ -1485,11 +1562,32 @@ SpdyStream31::OnWriteSegment(char *buf,
   rv = mPushSource->GetBufferedData(buf, count, countWritten);
   if (NS_FAILED(rv))
     return rv;
 
   mSession->ConnectPushedStream(this);
   return NS_OK;
 }
 
+/// connect tunnels
+
+void
+SpdyStream31::ClearTransactionsBlockedOnTunnel()
+{
+  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
+
+  if (!mIsTunnel) {
+    return;
+  }
+  gHttpHandler->ConnMgr()->ProcessPendingQ(mTransaction->ConnectionInfo());
+}
+
+void
+SpdyStream31::MapStreamToHttpConnection()
+{
+  nsRefPtr<SpdyConnectTransaction> qiTrans(mTransaction->QuerySpdyConnectTransaction());
+  MOZ_ASSERT(qiTrans);
+  qiTrans->MapStreamToHttpConnection(mSocketTransport,
+                                     mTransaction->ConnectionInfo());
+}
+
 } // namespace mozilla::net
 } // namespace mozilla
-
--- a/netwerk/protocol/http/SpdyStream31.h
+++ b/netwerk/protocol/http/SpdyStream31.h
@@ -29,23 +29,20 @@ public:
 
   const nsAFlatCString &Origin()   const { return mOrigin; }
 
   bool RequestBlockedOnRead()
   {
     return static_cast<bool>(mRequestBlockedOnRead);
   }
 
-  // returns false if called more than once
-  bool GetFullyOpen() {return mFullyOpen;}
-  void SetFullyOpen()
-  {
-    MOZ_ASSERT(!mFullyOpen);
-    mFullyOpen = 1;
-  }
+  bool GetFullyOpen();
+  // returns failure if stream cannot be made ready and stream
+  // should be canceled
+  nsresult SetFullyOpen();
 
   bool HasRegisteredID() { return mStreamID != 0; }
 
   nsAHttpTransaction *Transaction() { return mTransaction; }
   virtual nsILoadGroupConnectionInfo *LoadGroupConnectionInfo()
   {
     return mTransaction ? mTransaction->LoadGroupConnectionInfo() : nullptr;
   }
@@ -210,17 +207,18 @@ private:
   uint32_t             mDecompressBufferUsed;
   uint32_t             mDecompressedBytes;
   nsAutoArrayPtr<char> mDecompressBuffer;
 
   // 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.
+  // problems with the google servers. Connect does rely on stream
+  // close by setting this to the max value.
   int64_t                      mRequestBodyLenRemaining;
 
   // based on nsISupportsPriority definitions
   int32_t                      mPriority;
 
   // mLocalWindow, mRemoteWindow, and mLocalUnacked are for flow control.
   // *window are signed because the race conditions in asynchronous SETTINGS
   // messages can force them temporarily negative.
@@ -243,13 +241,22 @@ private:
   bool                         mBlockedOnRwin;
 
   // For Progress Events
   uint64_t                     mTotalSent;
   uint64_t                     mTotalRead;
 
   // For SpdyPush
   SpdyPushedStream31 *mPushSource;
+
+/// connect tunnels
+public:
+  bool IsTunnel() { return mIsTunnel; }
+private:
+  void ClearTransactionsBlockedOnTunnel();
+  void MapStreamToHttpConnection();
+
+  bool mIsTunnel;
 };
 
 }} // namespace mozilla::net
 
 #endif // mozilla_net_SpdyStream31_h