Bug 1203503 - part 1. change necko to allow CONNECT-only requests r+mayhemer
authorPaul Vitale <paul.m.vitale@gmail.com>
Thu, 19 Jul 2018 11:41:57 -0500
changeset 503026 457ec4c616e332f5d28d5cce2c1a86f0ecd4cf54
parent 503025 e7f8e9c7301bc4cd4088bcf244ca55b877fbb034
child 503027 d87037beffaabb3af5cc2fb4780e0cbadab6e979
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1203503
milestone65.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 1203503 - part 1. change necko to allow CONNECT-only requests r+mayhemer Necko does not allow for a CONNECT only request to happen. This adds a flag to signal an http channel should only CONNECT if proxied. This flag can only be set if an HTTPUpgrade handler has been assigned. As proposed by rfc7639, an alpn header will be included in the CONNECT request. Its value is determined by the upgrade protocol passed to HTTPUpgrade. The flag is added as part of nsIHttpChannelInternal because it is dependent on HTTPUpgrade. It exists as a capability flag since the channel's transaction needs to know to complete after a successful CONNECT. Also the transaction's connection needs to know to stop writing transaction data after it CONNECTs or if there's no proxy, and to not tell the transaction to reset. If an nsHttpChannel has this flag set then the upgrade handler will receive the socket after the CONNECT succeeds without doing tls if https. In order to support xpcshell-test for this change, nsHttpConnectionMgr does a little check to see if HTTPUpgrade callback is in JavaScript. If it is then the callback is invoked on the main thread.
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/TrackingDummyChannel.cpp
netwerk/protocol/http/nsHttp.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpConnection.cpp
netwerk/protocol/http/nsHttpConnectionMgr.cpp
netwerk/protocol/http/nsHttpTransaction.cpp
netwerk/protocol/http/nsIHttpChannelInternal.idl
netwerk/test/unit/test_proxyconnect.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -32,16 +32,17 @@
 #include "nsURLHelper.h"
 #include "nsICookieService.h"
 #include "nsIStreamConverterService.h"
 #include "nsCRT.h"
 #include "nsContentUtils.h"
 #include "nsIMutableArray.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIObserverService.h"
+#include "nsIProtocolProxyService.h"
 #include "nsProxyRelease.h"
 #include "nsPIDOMWindow.h"
 #include "nsIDocShell.h"
 #include "nsINetworkInterceptController.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/PerformanceStorage.h"
 #include "mozilla/NullPrincipal.h"
 #include "mozilla/Services.h"
@@ -2745,16 +2746,43 @@ HttpBaseChannel::HTTPUpgrade(const nsACS
     NS_ENSURE_ARG_POINTER(aListener);
 
     mUpgradeProtocol = aProtocolName;
     mUpgradeProtocolCallback = aListener;
     return NS_OK;
 }
 
 NS_IMETHODIMP
+HttpBaseChannel::GetOnlyConnect(bool* aOnlyConnect)
+{
+  NS_ENSURE_ARG_POINTER(aOnlyConnect);
+
+  *aOnlyConnect = mCaps & NS_HTTP_CONNECT_ONLY;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+HttpBaseChannel::SetConnectOnly()
+{
+  ENSURE_CALLED_BEFORE_CONNECT();
+
+  if (!mUpgradeProtocolCallback) {
+    return NS_ERROR_FAILURE;
+  }
+
+  mCaps |= NS_HTTP_CONNECT_ONLY;
+  mProxyResolveFlags = nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY |
+                       nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL;
+  return SetLoadFlags(nsIRequest::INHIBIT_CACHING |
+                      nsIChannel::LOAD_ANONYMOUS |
+                      nsIRequest::LOAD_BYPASS_CACHE |
+                      nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
+}
+
+NS_IMETHODIMP
 HttpBaseChannel::GetAllowSpdy(bool *aAllowSpdy)
 {
   NS_ENSURE_ARG_POINTER(aAllowSpdy);
 
   *aAllowSpdy = mAllowSpdy;
   return NS_OK;
 }
 
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -240,16 +240,18 @@ public:
   NS_IMETHOD GetCanceled(bool *aCanceled) override;
   NS_IMETHOD GetChannelIsForDownload(bool *aChannelIsForDownload) override;
   NS_IMETHOD SetChannelIsForDownload(bool aChannelIsForDownload) override;
   NS_IMETHOD SetCacheKeysRedirectChain(nsTArray<nsCString> *cacheKeys) override;
   NS_IMETHOD GetLocalAddress(nsACString& addr) override;
   NS_IMETHOD GetLocalPort(int32_t* port) override;
   NS_IMETHOD GetRemoteAddress(nsACString& addr) override;
   NS_IMETHOD GetRemotePort(int32_t* port) override;
+  NS_IMETHOD GetOnlyConnect(bool* aOnlyConnect) override;
+  NS_IMETHOD SetConnectOnly() override;
   NS_IMETHOD GetAllowSpdy(bool *aAllowSpdy) override;
   NS_IMETHOD SetAllowSpdy(bool aAllowSpdy) override;
   NS_IMETHOD GetAllowAltSvc(bool *aAllowAltSvc) override;
   NS_IMETHOD SetAllowAltSvc(bool aAllowAltSvc) override;
   NS_IMETHOD GetBeConservative(bool *aBeConservative) override;
   NS_IMETHOD SetBeConservative(bool aBeConservative) override;
   NS_IMETHOD GetTrr(bool *aTRR) override;
   NS_IMETHOD SetTrr(bool aTRR) override;
--- a/netwerk/protocol/http/TrackingDummyChannel.cpp
+++ b/netwerk/protocol/http/TrackingDummyChannel.cpp
@@ -478,16 +478,28 @@ TrackingDummyChannel::SetCacheKeysRedire
 NS_IMETHODIMP
 TrackingDummyChannel::HTTPUpgrade(const nsACString& aProtocolName,
                                   nsIHttpUpgradeListener* aListener)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
+TrackingDummyChannel::GetOnlyConnect(bool* aOnlyConnect)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TrackingDummyChannel::SetConnectOnly()
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
 TrackingDummyChannel::GetAllowSpdy(bool* aAllowSpdy)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
 TrackingDummyChannel::SetAllowSpdy(bool aAllowSpdy)
 {
--- a/netwerk/protocol/http/nsHttp.h
+++ b/netwerk/protocol/http/nsHttp.h
@@ -111,16 +111,21 @@ namespace net {
 // for use with TRR implementations themselves
 #define NS_HTTP_DISABLE_TRR (1<<14)
 
 // Allow re-using a spdy/http2 connection with NS_HTTP_ALLOW_KEEPALIVE not set.
 // This is primarily used to allow connection sharing for websockets over http/2
 // without accidentally allowing it for websockets not over http/2
 #define NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE (1<<15)
 
+// Only permit CONNECTing to a proxy. A channel with this flag will not send an
+// http request after CONNECT or setup tls. An http upgrade handler MUST be
+// set. An ALPN header is set using the upgrade protocol.
+#define NS_HTTP_CONNECT_ONLY            (1<<16)
+
 //-----------------------------------------------------------------------------
 // some default values
 //-----------------------------------------------------------------------------
 
 #define NS_HTTP_DEFAULT_PORT  80
 #define NS_HTTPS_DEFAULT_PORT 443
 
 #define NS_HTTP_HEADER_SEPS ", \t"
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -7767,20 +7767,26 @@ nsHttpChannel::OnStopRequest(nsIRequest 
         // if this transaction has been replaced, then bail.
         if (mTransactionReplaced) {
             LOG(("Transaction replaced\n"));
             // This was just the network check for a 304 response.
             mFirstResponseSource = RESPONSE_PENDING;
             return NS_OK;
         }
 
-        if (mUpgradeProtocolCallback && stickyConn &&
+        bool upgradeWebsocket = mUpgradeProtocolCallback && stickyConn &&
             mResponseHead &&
             ((mResponseHead->Status() == 101 && mResponseHead->Version() == HttpVersion::v1_1) ||
-             (mResponseHead->Status() == 200 && mResponseHead->Version() == HttpVersion::v2_0))) {
+             (mResponseHead->Status() == 200 && mResponseHead->Version() == HttpVersion::v2_0));
+
+        bool upgradeConnect = mUpgradeProtocolCallback && stickyConn &&
+            (mCaps & NS_HTTP_CONNECT_ONLY) && mResponseHead &&
+            mResponseHead->Status() == 200;
+
+        if (upgradeWebsocket || upgradeConnect) {
             nsresult rv =
                 gHttpHandler->ConnMgr()->CompleteUpgrade(stickyConn,
                                                          mUpgradeProtocolCallback);
             if (NS_FAILED(rv)) {
                 LOG(("  CompleteUpgrade failed with %08x",
                      static_cast<uint32_t>(rv)));
             }
         }
--- a/netwerk/protocol/http/nsHttpConnection.cpp
+++ b/netwerk/protocol/http/nsHttpConnection.cpp
@@ -1280,39 +1280,61 @@ nsHttpConnection::OnHeadersAvailable(nsA
     // socket write request.
     if (mProxyConnectStream) {
         MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE,
                    "SPDY NPN Complete while using proxy connect stream");
         mProxyConnectStream = nullptr;
         bool isHttps =
             mTransaction ? mTransaction->ConnectionInfo()->EndToEndSSL() :
             mConnInfo->EndToEndSSL();
+        bool onlyConnect = mTransactionCaps & NS_HTTP_CONNECT_ONLY;
 
         if (responseStatus == 200) {
-            LOG(("proxy CONNECT succeeded! endtoendssl=%d\n", isHttps));
-            *reset = true;
+            LOG(("proxy CONNECT succeeded! endtoendssl=%d onlyconnect=%d\n",
+                 isHttps, onlyConnect));
+            // If we're only connecting, we don't need to reset the transaction
+            // state. We need to upgrade the socket now without doing the actual
+            // http request.
+            if (!onlyConnect) {
+                *reset = true;
+            }
             nsresult rv;
+            // CONNECT only flag doesn't do the tls setup. https here only
+            // ensures a proxy tunnel was used not that tls is setup.
             if (isHttps) {
-                if (mConnInfo->UsingHttpsProxy()) {
-                    LOG(("%p new TLSFilterTransaction %s %d\n",
-                         this, mConnInfo->Origin(), mConnInfo->OriginPort()));
-                    SetupSecondaryTLS();
+                if (!onlyConnect) {
+                    if (mConnInfo->UsingHttpsProxy()) {
+                        LOG(("%p new TLSFilterTransaction %s %d\n",
+                             this,
+                             mConnInfo->Origin(),
+                             mConnInfo->OriginPort()));
+                        SetupSecondaryTLS();
+                    }
+
+                    rv = InitSSLParams(false, true);
+                    LOG(("InitSSLParams [rv=%" PRIx32 "]\n",
+                         static_cast<uint32_t>(rv)));
+                } else {
+                    // We have an https protocol but the CONNECT only flag was
+                    // specified. The consumer only wants a raw socket to the
+                    // proxy. We have to mark this as complete to finish the
+                    // transaction and be upgraded. OnSocketReadable() uses this
+                    // to detect an inactive tunnel and blocks completion.
+                    mNPNComplete = true;
                 }
-
-                rv = InitSSLParams(false, true);
-                LOG(("InitSSLParams [rv=%" PRIx32 "]\n", static_cast<uint32_t>(rv)));
             }
             mCompletedProxyConnect = true;
             mProxyConnectInProgress = false;
             rv = mSocketOut->AsyncWait(this, 0, 0, nullptr);
             // XXX what if this fails -- need to handle this error
             MOZ_ASSERT(NS_SUCCEEDED(rv), "mSocketOut->AsyncWait failed");
         }
         else {
-            LOG(("proxy CONNECT failed! endtoendssl=%d\n", isHttps));
+            LOG(("proxy CONNECT failed! endtoendssl=%d onlyconnect=%d\n",
+                 isHttps, onlyConnect));
             mTransaction->SetProxyConnectFailed();
         }
     }
 
     nsAutoCString upgradeReq;
     bool hasUpgradeReq = NS_SUCCEEDED(requestHead->GetHeader(nsHttp::Upgrade,
                                                              upgradeReq));
     // Don't use persistent connection for Upgrade unless there's an auth failure:
@@ -1876,16 +1898,35 @@ nsHttpConnection::OnSocketWritable()
     bool again = true;
 
     // Prevent STS thread from being blocked by single OnOutputStreamReady callback.
     const uint32_t maxWriteAttempts = 128;
     uint32_t writeAttempts = 0;
 
     mForceSendDuringFastOpenPending = false;
 
+    if (mTransactionCaps & NS_HTTP_CONNECT_ONLY) {
+        if (!mCompletedProxyConnect && !mProxyConnectStream) {
+            // A CONNECT has been requested for this connection but will never
+            // be performed. Fail here to let request callbacks happen.
+            LOG(("return failure because proxy connect will never happen\n"));
+            return NS_ERROR_FAILURE;
+        }
+
+        if (mCompletedProxyConnect) {
+            // Don't need to check this each write attempt since it is only
+            // updated after OnSocketWritable completes.
+            // We've already done primary tls (if needed) and sent our CONNECT.
+            // If we're doing a CONNECT only request there's no need to write
+            // the http transaction or do the SSL handshake here.
+            LOG(("return ok because proxy connect successful\n"));
+            return NS_OK;
+        }
+    }
+
     do {
         ++writeAttempts;
         rv = mSocketOutCondition = NS_OK;
         transactionBytes = 0;
 
         // The SSL handshake must be completed before the transaction->readsegments()
         // processing can proceed because we need to know how to format the
         // request differently for http/1, http/2, spdy, etc.. and that is
@@ -2037,16 +2078,24 @@ nsHttpConnection::OnSocketReadable()
     LOG(("nsHttpConnection::OnSocketReadable [this=%p]\n", this));
 
     PRIntervalTime now = PR_IntervalNow();
     PRIntervalTime delta = now - mLastReadTime;
 
     // Reset mResponseTimeoutEnabled to stop response timeout checks.
     mResponseTimeoutEnabled = false;
 
+    if ((mTransactionCaps & NS_HTTP_CONNECT_ONLY) &&
+        !mCompletedProxyConnect && !mProxyConnectStream) {
+        // A CONNECT has been requested for this connection but will never
+        // be performed. Fail here to let request callbacks happen.
+        LOG(("return failure because proxy connect will never happen\n"));
+        return NS_ERROR_FAILURE;
+    }
+
     if (mKeepAliveMask && (delta >= mMaxHangTime)) {
         LOG(("max hang time exceeded!\n"));
         // give the handler a chance to create a new persistent connection to
         // this host if we've been busy for too long.
         mKeepAliveMask = false;
         Unused << gHttpHandler->ProcessPendingQ(mConnInfo);
     }
 
@@ -2195,16 +2244,28 @@ nsHttpConnection::MakeConnectString(nsAH
                          nsHttp::Proxy_Authorization,
                          val))) {
         // we don't know for sure if this authorization is intended for the
         // SSL proxy, so we add it just in case.
         rv = request->SetHeader(nsHttp::Proxy_Authorization, val);
         MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
 
+    if ((trans->Caps() & NS_HTTP_CONNECT_ONLY) &&
+        NS_SUCCEEDED(trans->RequestHead()->GetHeader(
+                         nsHttp::Upgrade,
+                         val))) {
+        // rfc7639 proposes using the ALPN header to indicate the protocol used
+        // in CONNECT when not used for TLS. The protocol is stored in Upgrade.
+        // We have to copy this header here since a new HEAD request is created
+        // for the CONNECT.
+        rv = request->SetHeader(NS_LITERAL_CSTRING("ALPN"), val);
+        MOZ_ASSERT(NS_SUCCEEDED(rv));
+    }
+
     result.Truncate();
     request->Flatten(result, false);
     result.AppendLiteral("\r\n");
     return NS_OK;
 }
 
 nsresult
 nsHttpConnection::SetupProxyConnect()
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -30,16 +30,17 @@
 #include "nsITransport.h"
 #include "nsInterfaceRequestorAgg.h"
 #include "nsIRequestContext.h"
 #include "nsISocketTransportService.h"
 #include <algorithm>
 #include "mozilla/ChaosMode.h"
 #include "mozilla/Unused.h"
 #include "nsIURI.h"
+#include "nsIPropertyBag.h"
 
 #include "mozilla/Move.h"
 #include "mozilla/Telemetry.h"
 
 namespace mozilla {
 namespace net {
 
 //-----------------------------------------------------------------------------
@@ -554,40 +555,54 @@ nsHttpConnectionMgr::GetSocketThreadTarg
 
 nsresult
 nsHttpConnectionMgr::ReclaimConnection(nsHttpConnection *conn)
 {
     LOG(("nsHttpConnectionMgr::ReclaimConnection [conn=%p]\n", conn));
     return PostEvent(&nsHttpConnectionMgr::OnMsgReclaimConnection, 0, conn);
 }
 
-// A structure used to marshall 2 pointers across the various necessary
+// A structure used to marshall 5 pointers across the various necessary
 // threads to complete an HTTP upgrade.
 class nsCompleteUpgradeData : public ARefBase
 {
 public:
     nsCompleteUpgradeData(nsAHttpConnection *aConn,
-                          nsIHttpUpgradeListener *aListener)
+                          nsIHttpUpgradeListener *aListener,
+                          bool aJsWrapped)
         : mConn(aConn)
-        , mUpgradeListener(aListener) { }
+        , mUpgradeListener(aListener)
+        , mJsWrapped(aJsWrapped) { }
 
     NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsCompleteUpgradeData, override)
 
     RefPtr<nsAHttpConnection> mConn;
     nsCOMPtr<nsIHttpUpgradeListener> mUpgradeListener;
+
+    nsCOMPtr<nsISocketTransport> mSocketTransport;
+    nsCOMPtr<nsIAsyncInputStream> mSocketIn;
+    nsCOMPtr<nsIAsyncOutputStream> mSocketOut;
+
+    bool mJsWrapped;
 private:
     virtual ~nsCompleteUpgradeData() = default;
 };
 
 nsresult
 nsHttpConnectionMgr::CompleteUpgrade(nsAHttpConnection *aConn,
                                      nsIHttpUpgradeListener *aUpgradeListener)
 {
+    // test if aUpgradeListener is a wrapped JsObject
+    // bit of a HACK
+    nsCOMPtr<nsIPropertyBag> wrapper = do_QueryInterface(aUpgradeListener);
+
+    bool wrapped = !!wrapper;
+
     RefPtr<nsCompleteUpgradeData> data =
-        new nsCompleteUpgradeData(aConn, aUpgradeListener);
+        new nsCompleteUpgradeData(aConn, aUpgradeListener, wrapped);
     return PostEvent(&nsHttpConnectionMgr::OnMsgCompleteUpgrade, 0, data);
 }
 
 nsresult
 nsHttpConnectionMgr::UpdateParam(nsParamName name, uint16_t value)
 {
     uint32_t param = (uint32_t(name) << 16) | uint32_t(value);
     return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateParam,
@@ -2893,39 +2908,57 @@ nsHttpConnectionMgr::OnMsgReclaimConnect
     }
 
     OnMsgProcessPendingQ(0, ci);
 }
 
 void
 nsHttpConnectionMgr::OnMsgCompleteUpgrade(int32_t, ARefBase *param)
 {
-    MOZ_ASSERT(OnSocketThread(), "not on socket thread");
     nsCompleteUpgradeData *data = static_cast<nsCompleteUpgradeData *>(param);
+    MOZ_ASSERT(OnSocketThread() || (data->mJsWrapped == NS_IsMainThread()), "not on socket thread");
     LOG(("nsHttpConnectionMgr::OnMsgCompleteUpgrade "
-         "this=%p conn=%p listener=%p\n", this, data->mConn.get(),
-         data->mUpgradeListener.get()));
-
-    nsCOMPtr<nsISocketTransport> socketTransport;
-    nsCOMPtr<nsIAsyncInputStream> socketIn;
-    nsCOMPtr<nsIAsyncOutputStream> socketOut;
-
-    nsresult rv;
-    rv = data->mConn->TakeTransport(getter_AddRefs(socketTransport),
-                                    getter_AddRefs(socketIn),
-                                    getter_AddRefs(socketOut));
+         "this=%p conn=%p listener=%p wrapped=%d\n", this, data->mConn.get(),
+         data->mUpgradeListener.get(),
+         data->mJsWrapped));
+
+    nsresult rv = NS_OK;
+    if (!data->mSocketTransport) {
+        rv = data->mConn->TakeTransport(getter_AddRefs(data->mSocketTransport),
+                                        getter_AddRefs(data->mSocketIn),
+                                        getter_AddRefs(data->mSocketOut));
+    }
 
     if (NS_SUCCEEDED(rv)) {
-        rv = data->mUpgradeListener->OnTransportAvailable(socketTransport,
-                                                          socketIn,
-                                                          socketOut);
-        if (NS_FAILED(rv)) {
+        if (!data->mJsWrapped || !OnSocketThread()) {
+            rv = data->mUpgradeListener->OnTransportAvailable(
+                data->mSocketTransport,
+                data->mSocketIn,
+                data->mSocketOut);
+            if (NS_FAILED(rv)) {
+                LOG(("nsHttpConnectionMgr::OnMsgCompleteUpgrade "
+                     "this=%p conn=%p listener=%p wrapped=%d\n", this,
+                     data->mConn.get(),
+                     data->mUpgradeListener.get(),
+                     data->mJsWrapped));
+            }
+        } else {
             LOG(("nsHttpConnectionMgr::OnMsgCompleteUpgrade "
-                 "this=%p conn=%p listener=%p\n", this, data->mConn.get(),
-                 data->mUpgradeListener.get()));
+                 "this=%p conn=%p listener=%p wrapped=%d pass to main thread\n",
+                 this,
+                 data->mConn.get(),
+                 data->mUpgradeListener.get(),
+                 data->mJsWrapped));
+
+            nsCOMPtr<nsIRunnable> event = new ConnEvent(
+                this,
+                &nsHttpConnectionMgr::OnMsgCompleteUpgrade,
+                0,
+                param);
+            NS_DispatchToMainThread(event);
         }
     }
 }
 
 void
 nsHttpConnectionMgr::OnMsgUpdateParam(int32_t inParam, ARefBase *)
 {
     uint32_t param = static_cast<uint32_t>(inParam);
--- a/netwerk/protocol/http/nsHttpTransaction.cpp
+++ b/netwerk/protocol/http/nsHttpTransaction.cpp
@@ -1713,16 +1713,31 @@ nsHttpTransaction::HandleContentStart()
                 if (mConnection->Version() == HttpVersion::v2_0) {
                     mReuseOnRestart = true;
                 }
                 return NS_ERROR_NET_RESET;
             }
             break;
         }
 
+        // If we're only connecting then we're going to be upgrading this
+        // connection since we were successful. Any data from now on belongs to
+        // the upgrade handler. If we're not successful the content body doesn't
+        // matter. Proxy http errors are treated as network errors. This
+        // connection won't be reused since it's marked sticky and no
+        // keep-alive.
+        if (mCaps & NS_HTTP_CONNECT_ONLY) {
+            MOZ_ASSERT(!(mCaps & NS_HTTP_ALLOW_KEEPALIVE) &&
+                       (mCaps & NS_HTTP_STICKY_CONNECTION),
+                       "connection should be sticky and no keep-alive");
+            // The transaction will expect the server to close the socket if
+            // there's no content length instead of doing the upgrade.
+            mNoContent = true;
+        }
+
         if (mResponseHead->Status() == 200 &&
             mH2WSTransaction) {
             // http/2 websockets do not have response bodies
             mNoContent = true;
         }
 
         if (mResponseHead->Status() == 200 &&
             mConnection->IsProxyConnectInProgress()) {
--- a/netwerk/protocol/http/nsIHttpChannelInternal.idl
+++ b/netwerk/protocol/http/nsIHttpChannelInternal.idl
@@ -178,16 +178,33 @@ interface nsIHttpChannelInternal : nsISu
      *        The value of the HTTP Upgrade request header
      * @param aListener
      *        The callback object used to handle a successful upgrade
      */
     [must_use] void HTTPUpgrade(in ACString aProtocolName,
                                 in nsIHttpUpgradeListener aListener);
 
     /**
+     * Enable only CONNECT to a proxy. Fails if no HTTPUpgrade listener
+     * has been defined. An ALPN header is set using the upgrade protocol.
+     *
+     * Load flags are set with INHIBIT_CACHING, LOAD_ANONYMOUS,
+     * LOAD_BYPASS_CACHE, and LOAD_BYPASS_SERVICE_WORKER.
+     *
+     * Proxy resolve flags are set with RESOLVE_PREFER_HTTPS_PROXY and
+     * RESOLVE_ALWAYS_TUNNEL.
+     */
+    [must_use] void setConnectOnly();
+
+    /**
+     * True iff the channel is CONNECT only.
+     */
+    [must_use] readonly attribute boolean onlyConnect;
+
+    /**
      * Enable/Disable Spdy negotiation on per channel basis.
      * The network.http.spdy.enabled preference is still a pre-requisite
      * for starting spdy.
      */
     [must_use] attribute boolean allowSpdy;
 
     /**
      * This attribute en/disables the timeout for the first byte of an HTTP
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_proxyconnect.js
@@ -0,0 +1,328 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// test_connectonly tests happy path of proxy connect
+// 1. CONNECT to localhost:socketserver_port
+// 2. Write 200 Connection established
+// 3. Write data to the tunnel (and read server-side)
+// 4. Read data from the tunnel (and write server-side)
+// 5. done
+// test_connectonly_noproxy tests an http channel with only connect set but
+// no proxy configured.
+// 1. OnTransportAvailable callback NOT called (checked in step 2)
+// 2. StopRequest callback called
+// 3. done
+
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// -1 then initialized with an actual port from the serversocket
+var socketserver_port = -1;
+
+const CC = Components.Constructor;
+const ServerSocket = CC("@mozilla.org/network/server-socket;1",
+                        "nsIServerSocket",
+                        "init");
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
+                              "nsIBinaryOutputStream",
+                              "setOutputStream");
+
+const STATE_NONE = 0;
+const STATE_READ_CONNECT_REQUEST = 1;
+const STATE_WRITE_CONNECTION_ESTABLISHED = 2;
+const STATE_CHECK_WRITE = 3; // write to the tunnel
+const STATE_CHECK_WRITE_READ = 4; // wrote to the tunnel, check connection data
+const STATE_CHECK_READ = 5; // read from the tunnel
+const STATE_CHECK_READ_WROTE = 6; // wrote to connection, check tunnel data
+const STATE_COMPLETED = 100;
+
+const CONNECT_RESPONSE_STRING = 'HTTP/1.1 200 Connection established\r\n\r\n';
+const CHECK_WRITE_STRING = 'hello';
+const CHECK_READ_STRING = 'world';
+const ALPN = 'webrtc'
+
+var connectRequest = '';
+var checkWriteData = '';
+var checkReadData = '';
+
+var threadManager;
+var socket;
+var streamIn;
+var streamOut;
+var accepted = false;
+var acceptedSocket;
+var state = STATE_NONE;
+var transportAvailable = false;
+var listener = {
+  expectedCode: -1, // uninitialized
+
+  onStartRequest: function test_onStartR(request, ctx) {
+  },
+
+  onDataAvailable: function test_ODA() {
+    do_throw("Should not get any data!");
+  },
+
+  onStopRequest: function test_onStopR(request, ctx, status) {
+    if (state === STATE_COMPLETED) {
+      Assert.equal(transportAvailable, false, 'transport available not called');
+
+      nextTest();
+      return;
+    }
+
+    Assert.equal(accepted, true, 'socket accepted');
+    accepted = false;
+  }
+};
+
+var upgradeListener = {
+  onTransportAvailable: (transport, socketIn, socketOut) => {
+    if (!transport || !socketIn || !socketOut) {
+      do_throw('on transport available failed');
+    }
+
+    if (state !== STATE_CHECK_WRITE) {
+      do_throw('bad state');
+    }
+
+    transportAvailable = true;
+
+    socketIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+    socketOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+  },
+  QueryInterface: function qi(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIHttpUpgradeListener)) {
+      return this;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+}
+
+var connectHandler = {
+  onInputStreamReady: (input) => {
+    try {
+      const bis = new BinaryInputStream(input);
+      var data = bis.readByteArray(input.available());
+
+      dataAvailable(data);
+
+      if (state !== STATE_COMPLETED) {
+        input.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+      }
+    } catch(e) { do_throw(e); }
+  },
+  onOutputStreamReady: (output) => {
+    writeData(output);
+  },
+  QueryInterface: () => {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIInputStreamCallback) ||
+        iid.equals(Ci.nsIOutputStreamCallback)) {
+      return this;
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+}
+
+function dataAvailable(data) {
+  switch(state) {
+    case STATE_READ_CONNECT_REQUEST:
+      connectRequest += String.fromCharCode.apply(String, data);
+      const headerEnding = connectRequest.indexOf('\r\n\r\n');
+      const alpnHeaderIndex = connectRequest.indexOf(`ALPN: ${ALPN}`);
+
+      if (headerEnding != -1) {
+        const requestLine = `CONNECT localhost:${socketserver_port} HTTP/1.1`;
+        Assert.equal(connectRequest.indexOf(requestLine), 0, 'connect request');
+        Assert.equal(headerEnding, connectRequest.length - 4, 'req head only');
+        Assert.notEqual(alpnHeaderIndex, -1, 'alpn header found');
+
+        state = STATE_WRITE_CONNECTION_ESTABLISHED;
+        streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+      }
+
+      break;
+    case STATE_CHECK_WRITE_READ:
+      checkWriteData += String.fromCharCode.apply(String, data);
+
+      if (checkWriteData.length >= CHECK_WRITE_STRING.length) {
+        Assert.equal(checkWriteData, CHECK_WRITE_STRING, 'correct write data');
+
+        state = STATE_CHECK_READ;
+        streamOut.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+      }
+
+      break;
+    case STATE_CHECK_READ_WROTE:
+      checkReadData += String.fromCharCode.apply(String, data);
+
+      if (checkReadData.length >= CHECK_READ_STRING.length) {
+        Assert.equal(checkReadData, CHECK_READ_STRING, 'correct read data');
+
+        state = STATE_COMPLETED;
+
+        streamIn.asyncWait(null, 0, 0, null);
+        acceptedSocket.close(0);
+
+        nextTest();
+      }
+
+      break;
+    default:
+      do_throw('bad state: ' + state);
+  }
+}
+
+function writeData(output) {
+  let bos = new BinaryOutputStream(output);
+
+  switch(state) {
+    case STATE_WRITE_CONNECTION_ESTABLISHED:
+      bos.write(CONNECT_RESPONSE_STRING, CONNECT_RESPONSE_STRING.length);
+      state = STATE_CHECK_WRITE;
+      break;
+    case STATE_CHECK_READ:
+      bos.write(CHECK_READ_STRING, CHECK_READ_STRING.length);
+      state = STATE_CHECK_READ_WROTE;
+      break;
+    case STATE_CHECK_WRITE:
+      bos.write(CHECK_WRITE_STRING, CHECK_WRITE_STRING.length);
+      state = STATE_CHECK_WRITE_READ;
+      break;
+    default:
+      do_throw('bad state: ' + state);
+  }
+}
+
+function makeChan(url) {
+  if (!url) {
+    url = "https://localhost:" + socketserver_port + "/";
+  }
+
+  var flags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL |
+              Ci.nsILoadInfo.SEC_DONT_FOLLOW_REDIRECTS |
+              Ci.nsILoadInfo.SEC_COOKIES_OMIT;
+
+  var chan = NetUtil.newChannel({ uri: url,
+                                  loadUsingSystemPrincipal: true,
+                                  securityFlags: flags });
+  chan = chan.QueryInterface(Ci.nsIHttpChannel);
+
+  var internal = chan.QueryInterface(Ci.nsIHttpChannelInternal);
+  internal.HTTPUpgrade(ALPN, upgradeListener);
+  internal.setConnectOnly();
+
+  return chan;
+}
+
+function socketAccepted(socket, transport) {
+  accepted = true;
+
+  // copied from httpd.js
+  const SEGMENT_SIZE = 8192;
+  const SEGMENT_COUNT = 1024;
+
+  switch(state) {
+    case STATE_NONE:
+      state = STATE_READ_CONNECT_REQUEST;
+      break;
+    default:
+      return;
+  }
+
+  acceptedSocket = transport;
+
+  try
+  {
+    streamIn = transport.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+                        .QueryInterface(Ci.nsIAsyncInputStream);
+    streamOut = transport.openOutputStream(0, 0, 0);
+
+    streamIn.asyncWait(connectHandler, 0, 0, threadManager.mainThread);
+  }
+  catch (e)
+  {
+    transport.close(Cr.NS_BINDING_ABORTED);
+    do_throw(e);
+  }
+}
+
+function stopListening(socket, status) {
+  if (do_throw) {
+    do_throw('should never stop');
+  }
+}
+
+function createProxy() {
+  try {
+    threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+    socket = new ServerSocket(-1, true, 1);
+    socketserver_port = socket.port;
+
+    socket.asyncListen({
+      onSocketAccepted: socketAccepted,
+      onStopListening: stopListening
+    });
+  } catch(e) { do_throw(e); }
+}
+
+function test_connectonly() {
+  Services.prefs.setCharPref("network.proxy.ssl", "localhost");
+  Services.prefs.setIntPref("network.proxy.ssl_port", socketserver_port);
+  Services.prefs.setCharPref("network.proxy.no_proxies_on", "");
+  Services.prefs.setIntPref("network.proxy.type", 1);
+
+  var chan = makeChan();
+  chan.asyncOpen2(listener);
+
+  do_test_pending();
+}
+
+function test_connectonly_noproxy() {
+  clearPrefs()
+  var chan = makeChan();
+  chan.asyncOpen2(listener);
+
+  do_test_pending();
+}
+
+function nextTest() {
+  transportAvailable = false;
+
+  if (tests.length == 0) {
+    do_test_finished();
+    return;
+  }
+
+  (tests.shift())();
+  do_test_finished();
+}
+
+var tests = [
+  test_connectonly,
+  // test_connectonly_noproxy,
+];
+
+function clearPrefs() {
+  Services.prefs.clearUserPref("network.proxy.ssl");
+  Services.prefs.clearUserPref("network.proxy.ssl_port");
+  Services.prefs.clearUserPref("network.proxy.no_proxies_on");
+  Services.prefs.clearUserPref("network.proxy.type");
+}
+
+function run_test() {
+  createProxy();
+
+  registerCleanupFunction(clearPrefs);
+
+  nextTest();
+  do_test_pending();
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -411,16 +411,17 @@ skip-if = (verify && (os == 'linux'))
 # Test requires http/2, and http/2 server doesn't run on android.
 skip-if = os == "android"
 run-sequentially = node server exceptions dont replay well
 [test_trr.js]
 # http2-using tests require node available
 skip-if = os == "android"
 [test_ioservice.js]
 [test_substituting_protocol_handler.js]
+[test_proxyconnect.js]
 [test_captive_portal_service.js]
 skip-if = os == "android" # CP service is disabled on Android
 run-sequentially = node server exceptions dont replay well
 [test_esni_dns_fetch.js]
 # http2-using tests require node available
 skip-if = os == "android"
 [test_network_connectivity_service.js]
 skip-if = os == "android" # DNSv6 issues on android