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
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