From 281502cac622528475f715e2c54b639974704366 Mon Sep 17 00:00:00 2001 From: Honza Bambas Date: Thu, 15 Feb 2018 09:10:00 +0200 Subject: [PATCH] Bug 1363284 - HTTP/2 anonymous/onymous session (connection) coalescing, r=mayhemer --HG-- extra : rebase_source : 66192a32668de8a9cd99722d1e0860cce2f84030 --- netwerk/protocol/http/nsHttpConnection.cpp | 21 ++ netwerk/protocol/http/nsHttpConnection.h | 5 + netwerk/protocol/http/nsHttpConnectionMgr.cpp | 15 ++ netwerk/socket/nsISSLSocketControl.idl | 6 + .../test/unit/test_anonymous-coalescing.js | 180 ++++++++++++++++++ netwerk/test/unit/xpcshell.ini | 3 + security/manager/ssl/nsNSSIOLayer.cpp | 7 + 7 files changed, 237 insertions(+) create mode 100644 netwerk/test/unit/test_anonymous-coalescing.js diff --git a/netwerk/protocol/http/nsHttpConnection.cpp b/netwerk/protocol/http/nsHttpConnection.cpp index 301f106116c5..27b4df4954a8 100644 --- a/netwerk/protocol/http/nsHttpConnection.cpp +++ b/netwerk/protocol/http/nsHttpConnection.cpp @@ -2580,5 +2580,26 @@ nsHttpConnection::SetEvent(nsresult aStatus) } } +bool +nsHttpConnection::NoClientCertAuth() const +{ + if (!mSocketTransport) { + return false; + } + + nsCOMPtr secInfo; + mSocketTransport->GetSecurityInfo(getter_AddRefs(secInfo)); + if (!secInfo) { + return false; + } + + nsCOMPtr ssc(do_QueryInterface(secInfo)); + if (!ssc) { + return false; + } + + return !ssc->GetClientCertSent(); +} + } // namespace net } // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpConnection.h b/netwerk/protocol/http/nsHttpConnection.h index fd4cbfde8acf..c640af7bcd82 100644 --- a/netwerk/protocol/http/nsHttpConnection.h +++ b/netwerk/protocol/http/nsHttpConnection.h @@ -243,6 +243,11 @@ public: 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 { diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.cpp b/netwerk/protocol/http/nsHttpConnectionMgr.cpp index a5de491ec529..88ae15593105 100644 --- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp +++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp @@ -3791,6 +3791,21 @@ nsHttpConnectionMgr::GetOrCreateConnectionEntry(nsHttpConnectionInfo *specificCI 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 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; } diff --git a/netwerk/socket/nsISSLSocketControl.idl b/netwerk/socket/nsISSLSocketControl.idl index 488df3ecbc35..9f113eefaf97 100644 --- a/netwerk/socket/nsISSLSocketControl.idl +++ b/netwerk/socket/nsISSLSocketControl.idl @@ -143,6 +143,12 @@ interface nsISSLSocketControl : nsISupports { */ 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. diff --git a/netwerk/test/unit/test_anonymous-coalescing.js b/netwerk/test/unit/test_anonymous-coalescing.js new file mode 100644 index 000000000000..221202ec1890 --- /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(); +} diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index ec5cbe238c4b..62b57ce95b56 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -260,6 +260,9 @@ skip-if = os == "win" [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] diff --git a/security/manager/ssl/nsNSSIOLayer.cpp b/security/manager/ssl/nsNSSIOLayer.cpp index 0e881190ce6b..cd5d793931ff 100644 --- a/security/manager/ssl/nsNSSIOLayer.cpp +++ b/security/manager/ssl/nsNSSIOLayer.cpp @@ -220,6 +220,13 @@ nsNSSSocketInfo::SetClientCert(nsIX509Cert* aClientCert) return NS_OK; } +NS_IMETHODIMP +nsNSSSocketInfo::GetClientCertSent(bool* arg) +{ + *arg = mSentClientCert; + return NS_OK; +} + NS_IMETHODIMP nsNSSSocketInfo::GetBypassAuthentication(bool* arg) {