Bug 1363284 - HTTP/2 anonymous/onymous session (connection) coalescing, r=mayhemer
authorHonza Bambas <honzab.moz@firemni.cz>
Thu, 15 Feb 2018 09:10:00 +0200
changeset 460655 00ba29877dafcf59abeba25f8ab1597cdcf92323
parent 460654 38fc9527e92a1aa98f0be7715e8c17a493e01bd3
child 460656 6188bb22f81e855084b4d266525d23fb8e1030bd
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer
bugs1363284
milestone60.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 1363284 - HTTP/2 anonymous/onymous session (connection) coalescing, r=mayhemer
netwerk/protocol/http/nsHttpConnection.cpp
netwerk/protocol/http/nsHttpConnection.h
netwerk/protocol/http/nsHttpConnectionMgr.cpp
netwerk/socket/nsISSLSocketControl.idl
netwerk/test/unit/test_anonymous-coalescing.js
netwerk/test/unit/xpcshell.ini
security/manager/ssl/nsNSSIOLayer.cpp
--- a/netwerk/protocol/http/nsHttpConnection.cpp
+++ b/netwerk/protocol/http/nsHttpConnection.cpp
@@ -2575,10 +2575,31 @@ nsHttpConnection::SetEvent(nsresult aSta
   case NS_NET_STATUS_TLS_HANDSHAKE_ENDED:
     mBootstrappedTimings.connectEnd = TimeStamp::Now();
     break;
   default:
     break;
   }
 }
 
+bool
+nsHttpConnection::NoClientCertAuth() const
+{
+  if (!mSocketTransport) {
+    return false;
+  }
+
+  nsCOMPtr<nsISupports> secInfo;
+  mSocketTransport->GetSecurityInfo(getter_AddRefs(secInfo));
+  if (!secInfo) {
+    return false;
+  }
+
+  nsCOMPtr<nsISSLSocketControl> ssc(do_QueryInterface(secInfo));
+  if (!ssc) {
+    return false;
+  }
+
+  return !ssc->GetClientCertSent();
+}
+
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/nsHttpConnection.h
+++ b/netwerk/protocol/http/nsHttpConnection.h
@@ -238,16 +238,21 @@ public:
 
     void SetFastOpenStatus(uint8_t tfoStatus);
     uint8_t GetFastOpenStatus() {
       return mFastOpenStatus;
     }
 
     void SetEvent(nsresult aStatus);
 
+    // Return true when the socket this connection is using has not been
+    // authenticated using a client certificate.  Before SSL negotiation
+    // has finished this returns false.
+    bool NoClientCertAuth() const;
+
 private:
     // Value (set in mTCPKeepaliveConfig) indicates which set of prefs to use.
     enum TCPKeepaliveConfig {
       kTCPKeepaliveDisabled = 0,
       kTCPKeepaliveShortLivedConfig,
       kTCPKeepaliveLongLivedConfig
     };
 
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -3786,16 +3786,31 @@ nsHttpConnectionMgr::GetOrCreateConnecti
                                                 bool prohibitWildCard)
 {
     // step 1
     nsConnectionEntry *specificEnt = mCT.GetWeak(specificCI->HashKey());
     if (specificEnt && specificEnt->AvailableForDispatchNow()) {
         return specificEnt;
     }
 
+    // step 1 repeated for an inverted anonymous flag; we return an entry
+    // only when it has an h2 established connection that is not authenticated
+    // with a client certificate.
+    RefPtr<nsHttpConnectionInfo> anonInvertedCI(specificCI->Clone());
+    anonInvertedCI->SetAnonymous(!specificCI->GetAnonymous());
+    nsConnectionEntry *invertedEnt = mCT.GetWeak(anonInvertedCI->HashKey());
+    if (invertedEnt) {
+        nsHttpConnection* h2conn = GetSpdyActiveConn(invertedEnt);
+        if (h2conn && h2conn->IsExperienced() && h2conn->NoClientCertAuth()) {
+            MOZ_ASSERT(h2conn->UsingSpdy());
+            LOG(("GetOrCreateConnectionEntry is coalescing h2 an/onymous connections, ent=%p", invertedEnt));
+            return invertedEnt;
+        }
+    }
+
     if (!specificCI->UsingHttpsProxy()) {
         prohibitWildCard = true;
     }
 
     // step 2
     if (!prohibitWildCard) {
         RefPtr<nsHttpConnectionInfo> wildCardProxyCI;
         DebugOnly<nsresult> rv = specificCI->CreateWildCard(getter_AddRefs(wildCardProxyCI));
--- a/netwerk/socket/nsISSLSocketControl.idl
+++ b/netwerk/socket/nsISSLSocketControl.idl
@@ -139,16 +139,22 @@ interface nsISSLSocketControl : nsISuppo
     /**
      * If set before the server requests a client cert (assuming it does so at
      * all), then this cert will be presented to the server, instead of asking
      * the user or searching the set of rememebered user cert decisions.
      */
     attribute nsIX509Cert clientCert;
 
     /**
+     * True iff a client cert has been sent to the server - i.e. this
+     * socket has been client-cert authenticated.
+     */
+    [infallible] readonly attribute boolean clientCertSent;
+
+    /**
      * bypassAuthentication is true if the server certificate checks are
      * not be enforced. This is to enable non-secure transport over TLS.
      */
     [infallible] readonly attribute boolean bypassAuthentication;
 
     /*
      * failedVerification is true if any enforced certificate checks have failed.
      * Connections that have not yet tried to verify, have verifications bypassed,
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_anonymous-coalescing.js
@@ -0,0 +1,180 @@
+ChromeUtils.import("resource://testing-common/httpd.js");
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+/*
+- test to check we use only a single connection for both onymous and anonymous requests over an existing h2 session
+- request from a domain w/o LOAD_ANONYMOUS flag
+- request again from the same domain, but different URI, with LOAD_ANONYMOUS flag, check the client is using the same conn
+- close all and do it in the opposite way (do an anonymous req first)
+*/
+
+var h2Port;
+var prefs;
+var spdypref;
+var http2pref;
+var extpref;
+
+function run_test() {
+  var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  h2Port = env.get("MOZHTTP2_PORT");
+  Assert.notEqual(h2Port, null);
+  Assert.notEqual(h2Port, "");
+
+  // Set to allow the cert presented by our H2 server
+  do_get_profile();
+  prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+  spdypref = prefs.getBoolPref("network.http.spdy.enabled");
+  http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2");
+  extpref = prefs.getBoolPref("network.http.originextension");
+
+  prefs.setBoolPref("network.http.spdy.enabled", true);
+  prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+  prefs.setBoolPref("network.http.originextension", true);
+  prefs.setCharPref("network.dns.localDomains", "foo.example.com, alt1.example.com");
+
+  // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by CA.cert.der
+  // so add that cert to the trust list as a signing cert.
+  let certdb = Cc["@mozilla.org/security/x509certdb;1"]
+                  .getService(Ci.nsIX509CertDB);
+  addCertFromFile(certdb, "CA.cert.der", "CTu,u,u");
+
+  doTest1();
+}
+
+function resetPrefs() {
+  prefs.setBoolPref("network.http.spdy.enabled", spdypref);
+  prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref);
+  prefs.setBoolPref("network.http.originextension", extpref);
+  prefs.clearUserPref("network.dns.localDomains");
+}
+
+function readFile(file) {
+  let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
+                  .createInstance(Ci.nsIFileInputStream);
+  fstream.init(file, -1, 0, 0);
+  let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+  fstream.close();
+  return data;
+}
+
+function addCertFromFile(certdb, filename, trustString) {
+  let certFile = do_get_file(filename, false);
+  let der = readFile(certFile);
+  certdb.addCert(der, trustString);
+}
+
+function makeChan(origin) {
+  return NetUtil.newChannel({
+    uri: origin,
+    loadUsingSystemPrincipal: true
+  }).QueryInterface(Ci.nsIHttpChannel);
+}
+
+var nextTest;
+var origin;
+var nextPortExpectedToBeSame = false;
+var currentPort = 0;
+var forceReload = false;
+var anonymous = false;
+
+var Listener = function() {};
+Listener.prototype.clientPort = 0;
+Listener.prototype = {
+  onStartRequest: function testOnStartRequest(request, ctx) {
+    Assert.ok(request instanceof Components.interfaces.nsIHttpChannel);
+
+    if (!Components.isSuccessCode(request.status)) {
+      do_throw("Channel should have a success code! (" + request.status + ")");
+    }
+    Assert.equal(request.responseStatus, 200);
+    this.clientPort = parseInt(request.getResponseHeader("x-client-port"));
+  },
+
+  onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) {
+    read_stream(stream, cnt);
+  },
+
+  onStopRequest: function testOnStopRequest(request, ctx, status) {
+    Assert.ok(Components.isSuccessCode(status));
+    if (nextPortExpectedToBeSame) {
+     Assert.equal(currentPort, this.clientPort);
+    } else {
+     Assert.notEqual(currentPort, this.clientPort);
+    }
+    currentPort = this.clientPort;
+    nextTest();
+    do_test_finished();
+  }
+};
+
+function testsDone()
+{
+  dump("testsDone\n");
+  resetPrefs();
+}
+
+function doTest()
+{
+  dump("execute doTest " + origin + "\n");
+
+  var loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+  if (anonymous) {
+    loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+  }
+  anonymous = false;
+  if (forceReload) {
+    loadFlags |= Ci.nsIRequest.LOAD_FRESH_CONNECTION;
+  }
+  forceReload = false;
+
+  var chan = makeChan(origin);
+  chan.loadFlags = loadFlags;
+
+  var listener = new Listener();
+  chan.asyncOpen2(listener);
+}
+
+function doTest1()
+{
+  dump("doTest1()\n");
+  origin = "https://foo.example.com:" + h2Port + "/origin-1";
+  nextTest = doTest2;
+  nextPortExpectedToBeSame = false;
+  do_test_pending();
+  doTest();
+}
+
+function doTest2()
+{
+  // connection expected to be reused for an anonymous request
+  dump("doTest2()\n");
+  origin = "https://foo.example.com:" + h2Port + "/origin-2";
+  nextTest = doTest3;
+  nextPortExpectedToBeSame = true;
+  anonymous = true;
+  do_test_pending();
+  doTest();
+}
+
+function doTest3()
+{
+  dump("doTest3()\n");
+  origin = "https://foo.example.com:" + h2Port + "/origin-3";
+  nextTest = doTest4;
+  nextPortExpectedToBeSame = false;
+  forceReload = true;
+  anonymous = true;
+  do_test_pending();
+  doTest();
+}
+
+function doTest4()
+{
+  dump("doTest4()\n");
+  origin = "https://foo.example.com:" + h2Port + "/origin-4";
+  nextTest = testsDone;
+  nextPortExpectedToBeSame = true;
+  do_test_pending();
+  doTest();
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -255,16 +255,19 @@ skip-if = (os == 'win' && ccov) # Bug 14
 [test_net_addr.js]
 # Bug 732363: test fails on windows for unknown reasons.
 skip-if = os == "win"
 [test_nojsredir.js]
 [test_offline_status.js]
 [test_origin.js]
 # node server not runinng on android
 skip-if = os == "android"
+[test_anonymous-coalescing.js]
+# node server not runinng on android
+skip-if = os == "android"
 [test_original_sent_received_head.js]
 [test_parse_content_type.js]
 [test_permmgr.js]
 [test_plaintext_sniff.js]
 [test_post.js]
 [test_private_necko_channel.js]
 [test_private_cookie_changed.js]
 [test_progress.js]
--- a/security/manager/ssl/nsNSSIOLayer.cpp
+++ b/security/manager/ssl/nsNSSIOLayer.cpp
@@ -216,16 +216,23 @@ nsNSSSocketInfo::GetClientCert(nsIX509Ce
 NS_IMETHODIMP
 nsNSSSocketInfo::SetClientCert(nsIX509Cert* aClientCert)
 {
   mClientCert = aClientCert;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsNSSSocketInfo::GetClientCertSent(bool* arg)
+{
+  *arg = mSentClientCert;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsNSSSocketInfo::GetBypassAuthentication(bool* arg)
 {
   *arg = mBypassAuthentication;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsNSSSocketInfo::GetFailedVerification(bool* arg)