diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index f40992c289b..2839f78c5cc 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2643,11 +2643,6 @@ let BrowserOnClick = { let transportSecurityInfo = serhelper.deserializeObject(securityInfo); transportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo) - if (transportSecurityInfo.failedCertChain == null) { - Cu.reportError("transportSecurityInfo didn't have a failedCertChain for a failedChannel"); - return; - } - showReportStatus("activity"); /* @@ -2677,11 +2672,14 @@ let BrowserOnClick = { // Convert the nsIX509CertList into a format that can be parsed into // JSON let asciiCertChain = []; - let certs = transportSecurityInfo.failedCertChain.getEnumerator(); - while (certs.hasMoreElements()) { - let cert = certs.getNext(); - cert.QueryInterface(Ci.nsIX509Cert); - asciiCertChain.push(btoa(getDERString(cert))); + + if (transportSecurityInfo.failedCertChain) { + let certs = transportSecurityInfo.failedCertChain.getEnumerator(); + while (certs.hasMoreElements()) { + let cert = certs.getNext(); + cert.QueryInterface(Ci.nsIX509Cert); + asciiCertChain.push(btoa(getDERString(cert))); + } } let report = { diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 1585d3d4225..28498fc2c63 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -10,10 +10,10 @@ support-files = browser_bug479408_sample.html browser_bug678392-1.html browser_bug678392-2.html - browser_bug846489_content.js browser_bug970746.xhtml browser_fxa_oauth.html browser_registerProtocolHandler_notification.html + browser_ssl_error_reports_content.js browser_star_hsts.sjs browser_tab_dragdrop2_frame1.xul browser_web_channel.html @@ -153,7 +153,6 @@ skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and [browser_bug419612.js] skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s [browser_bug422590.js] -[browser_bug846489.js] [browser_bug423833.js] skip-if = true # bug 428712 [browser_bug424101.js] @@ -399,6 +398,7 @@ support-files = searchSuggestionUI.html searchSuggestionUI.js [browser_selectTabAtIndex.js] +[browser_ssl_error_reports.js] [browser_star_hsts.js] [browser_subframe_favicons_not_used.js] [browser_tabDrop.js] diff --git a/browser/base/content/test/general/browser_bug846489.js b/browser/base/content/test/general/browser_ssl_error_reports.js similarity index 84% rename from browser/base/content/test/general/browser_bug846489.js rename to browser/base/content/test/general/browser_ssl_error_reports.js index 9eb75b54f80..70a9a8a2746 100644 --- a/browser/base/content/test/general/browser_bug846489.js +++ b/browser/base/content/test/general/browser_ssl_error_reports.js @@ -1,14 +1,15 @@ -var badPin = "https://include-subdomains.pinning.example.com"; -var enabledPref = false; -var automaticPref = false; -var urlPref = "security.ssl.errorReporting.url"; -var enforcement_level = 1; +let badChainURL = "https://badchain.include-subdomains.pinning.example.com"; +let noCertURL = "https://fail-handshake.example.com"; +let enabledPref = false; +let automaticPref = false; +let urlPref = "security.ssl.errorReporting.url"; +let enforcement_level = 1; function loadFrameScript() { let mm = Cc["@mozilla.org/globalmessagemanager;1"] .getService(Ci.nsIMessageListenerManager); const ROOT = getRootDirectory(gTestPath); - mm.loadFrameScript(ROOT+"browser_bug846489_content.js", true); + mm.loadFrameScript(ROOT + "browser_ssl_error_reports_content.js", true); } add_task(function*(){ @@ -16,7 +17,8 @@ add_task(function*(){ loadFrameScript(); SimpleTest.requestCompleteLog(); yield testSendReportDisabled(); - yield testSendReportManual(); + yield testSendReportManual(badChainURL, "succeed"); + yield testSendReportManual(noCertURL, "nocert"); yield testSendReportAuto(); yield testSendReportError(); yield testSetAutomatic(); @@ -26,9 +28,9 @@ add_task(function*(){ function createNetworkErrorMessagePromise(aBrowser) { return new Promise(function(resolve, reject) { // Error pages do not fire "load" events, so use a progressListener. - var originalDocumentURI = aBrowser.contentDocument.documentURI; + let originalDocumentURI = aBrowser.contentDocument.documentURI; - var progressListener = { + let progressListener = { onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { // Make sure nothing other than an error page is loaded. if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) { @@ -42,7 +44,7 @@ function createNetworkErrorMessagePromise(aBrowser) { if (doc.getElementById("reportCertificateError")) { // Wait until the documentURI changes (from about:blank) this should // be the error page URI. - var documentURI = doc.documentURI; + let documentURI = doc.documentURI; if (documentURI == originalDocumentURI) { return; } @@ -50,13 +52,13 @@ function createNetworkErrorMessagePromise(aBrowser) { aWebProgress.removeProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_LOCATION | Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); - var matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI); + let matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI); if (!matchArray) { reject("no network error message found in URI") return; } - var errorMsg = matchArray[1]; + let errorMsg = matchArray[1]; resolve(decodeURIComponent(errorMsg)); } }, @@ -74,7 +76,7 @@ function createNetworkErrorMessagePromise(aBrowser) { // check we can set the 'automatically send' pref let testSetAutomatic = Task.async(function*() { setup(); - let tab = gBrowser.addTab(badPin, {skipAnimation: true}); + let tab = gBrowser.addTab(badChainURL, {skipAnimation: true}); let browser = tab.linkedBrowser; let mm = browser.messageManager; @@ -119,12 +121,13 @@ let testSetAutomatic = Task.async(function*() { }); // test that manual report sending (with button clicks) works -let testSendReportManual = Task.async(function*() { +let testSendReportManual = Task.async(function*(testURL, suffix) { setup(); Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", true); - Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?succeed"); + Services.prefs.setCharPref("security.ssl.errorReporting.url", + "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?" + suffix); - let tab = gBrowser.addTab(badPin, {skipAnimation: true}); + let tab = gBrowser.addTab(testURL, {skipAnimation: true}); let browser = tab.linkedBrowser; let mm = browser.messageManager; @@ -134,7 +137,8 @@ let testSendReportManual = Task.async(function*() { let netError = createNetworkErrorMessagePromise(browser); yield netError; netError.then(function(val){ - is(val.startsWith("An error occurred during a connection to include-subdomains.pinning.example.com"), true ,"ensure the correct error message came from about:neterror"); + is(val.startsWith("An error occurred during a connection to"), true, + "ensure the correct error message came from about:neterror"); }); // Check the report starts on click @@ -185,7 +189,7 @@ let testSendReportAuto = Task.async(function*() { Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", true); Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?succeed"); - let tab = gBrowser.addTab(badPin, {skipAnimation: true}); + let tab = gBrowser.addTab(badChainURL, {skipAnimation: true}); let browser = tab.linkedBrowser; let mm = browser.messageManager; @@ -234,7 +238,7 @@ let testSendReportError = Task.async(function*() { Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", true); Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?error"); - let tab = gBrowser.addTab(badPin, {skipAnimation: true}); + let tab = gBrowser.addTab(badChainURL, {skipAnimation: true}); let browser = tab.linkedBrowser; let mm = browser.messageManager; @@ -255,12 +259,12 @@ let testSendReportError = Task.async(function*() { let reportErrors = new Promise(function(resolve, reject) { mm.addMessageListener("ssler-test:SSLErrorReportStatus", function(message) { switch(message.data.reportStatus) { - case "complete": - reject(message.data.reportStatus); - break; - case "error": - resolve(message.data.reportStatus); - break; + case "complete": + reject(message.data.reportStatus); + break; + case "error": + resolve(message.data.reportStatus); + break; } }); }); @@ -276,7 +280,7 @@ let testSendReportDisabled = Task.async(function*() { Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", false); Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://offdomain.com"); - let tab = gBrowser.addTab(badPin, {skipAnimation: true}); + let tab = gBrowser.addTab(badChainURL, {skipAnimation: true}); let browser = tab.linkedBrowser; let mm = browser.messageManager; @@ -289,12 +293,12 @@ let testSendReportDisabled = Task.async(function*() { let reportErrors = new Promise(function(resolve, reject) { mm.addMessageListener("ssler-test:SSLErrorReportStatus", function(message) { switch(message.data.reportStatus) { - case "complete": - reject(message.data.reportStatus); - break; - case "error": - resolve(message.data.reportStatus); - break; + case "complete": + reject(message.data.reportStatus); + break; + case "error": + resolve(message.data.reportStatus); + break; } }); }); diff --git a/browser/base/content/test/general/browser_bug846489_content.js b/browser/base/content/test/general/browser_ssl_error_reports_content.js similarity index 100% rename from browser/base/content/test/general/browser_bug846489_content.js rename to browser/base/content/test/general/browser_ssl_error_reports_content.js diff --git a/browser/base/content/test/general/pinning_reports.sjs b/browser/base/content/test/general/pinning_reports.sjs index b4aa987eab3..16cf98d08f0 100644 --- a/browser/base/content/test/general/pinning_reports.sjs +++ b/browser/base/content/test/general/pinning_reports.sjs @@ -3,39 +3,67 @@ const EXPECTED_CHAIN = [ "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQwOTI1MjEyMTU0WhcNMjQwOTI1MjEyMTU0WjAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBT+BwAhO52IWgSIdZZifU9LHOs3IR/+8DCC0WP5d/OuyKlZ6Rqd0tsd3i7durhQyjHSbLf2lJStcnFjcVEbEnNI76RuvlN8xLLn5eV+2Ayr4cZYKztudwRmw+DV/iYAiMSy0hs7m3ssfX7qpoi1aNRjUanwU0VTCPQhF1bEKAC2du+C5Z8e92zN5t87w7bYr7lt+m8197XliXEu+0s9RgnGwGaZ296BIRz6NOoJYTa43n06LU1I1+Z4d6lPdzUFrSR0GBaMhUSurUBtOin3yWiMhg1VHX/KwqGc4als5GyCVXy8HGrA/0zQPOhetxrlhEVAdK/xBt7CZvByj1Rcc7AgMBAAGjEzARMA8GA1UdEwQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAJq/hogSRqzPWTwX4wTn/DVSNdWwFLv53qep9YrSMJ8ZsfbfK9Es4VP4dBLRQAVMJ0Z5mW1I6d/n0KayTanuUBvemYdxPi/qQNSs8UJcllqdhqWzmzAg6a0LxrMnEeKzPBPD6q8PwQ7tYP+B4sBN9tnnsnyPgti9ZiNZn5FwXZliHXseQ7FE9/SqHlLw5LXW3YtKjuti6RmuV6fq3j+D4oeC5vb1mKgIyoTqGN6ze57v8RHi+pQ8Q+kmoUn/L3Z2YmFe4SKN/4WoyXr8TdejpThGOCGCAd3565s5gOx5QfSQX11P8NZKO8hcN0tme3VzmGpHK0Z/6MTmdpNaTwQ6odk=" ]; -function handleRequest(request, response) -{ - if (request.queryString === "succeed") { - // read the report from the client - let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream); - inputStream.init(request.bodyInputStream, 0x01, 0004, 0); +function parseReport(request) { + // read the report from the request + let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream); + inputStream.init(request.bodyInputStream, 0x01, 0004, 0); - let body = ""; - if (inputStream) { - while (inputStream.available()) { - body = body + inputStream.read(inputStream.available()); - } + let body = ""; + if (inputStream) { + while (inputStream.available()) { + body = body + inputStream.read(inputStream.available()); } - // parse the report - let report = JSON.parse(body); - let certChain = report.failedCertChain; + } + // parse the report + return JSON.parse(body); +} - // ensure the cert chain is what we expect - for (idx in certChain) { - if (certChain[idx] !== EXPECTED_CHAIN[idx]) { - // if the chain differs, send an error response to cause test - // failure +function handleRequest(request, response) { + let report = {}; + let certChain = []; + + switch (request.queryString) { + case "succeed": + report = parseReport(request); + certChain = report.failedCertChain; + + // ensure the cert chain is what we expect + for (idx in certChain) { + if (certChain[idx] !== EXPECTED_CHAIN[idx]) { + // if the chain differs, send an error response to cause test + // failure + response.setStatusLine("1.1", 500, "Server error"); + response.write("The report contained an unexpected chain"); + return; + } + } + + // if all is as expected, send the 201 the client expects + response.setStatusLine("1.1", 201, "Created"); + response.write("OK"); + break; + case "nocert": + report = parseReport(request); + certChain = report.failedCertChain; + + if (certChain && certChain.length > 0) { + // We're not expecting a chain; if there is one, send an error response.setStatusLine("1.1", 500, "Server error"); response.write("The report contained an unexpected chain"); return; } - } - // if all is as expected, send the 201 the client expects - response.setStatusLine("1.1", 201, "Created"); - response.write("OK"); - } else if (request.queryString === "error") { - response.setStatusLine("1.1", 500, "Server error"); - response.write("server error"); + // if all is as expected, send the 201 the client expects + response.setStatusLine("1.1", 201, "Created"); + response.write("OK"); + break; + case "error": + response.setStatusLine("1.1", 500, "Server error"); + response.write("server error"); + break; + default: + response.setStatusLine("1.1", 500, "Server error"); + response.write("succeed, nocert or error expected"); + break; } } diff --git a/build/pgo/server-locations.txt b/build/pgo/server-locations.txt index ac4ec59de6a..d5e34714e81 100644 --- a/build/pgo/server-locations.txt +++ b/build/pgo/server-locations.txt @@ -20,7 +20,8 @@ # number is the default for the protocol. # # Unrecognized options are ignored. Recognized options are "primary" and -# "privileged", "nocert", "cert=some_cert_nickname", "redir=hostname". +# "privileged", "nocert", "cert=some_cert_nickname", "redir=hostname" and +# "failHandshake". # # "primary" denotes a location which is the canonical location of # the server; this location is the one assumed for requests which don't @@ -32,6 +33,9 @@ # "nocert" makes sense only for https:// hosts and means there is not # any certificate automatically generated for this host. # +# "failHandshake" causes the tls handshake to fail (by sending a client hello to +# the client). +# # "cert=nickname" tells the pgo server to use a particular certificate # for this host. The certificate is referenced by its nickname that must # not contain any spaces. The certificate key files (PKCS12 modules) @@ -227,7 +231,8 @@ https://include-subdomains.pinning-dynamic.example.com:443 privileged,cer https://bad.include-subdomains.pinning-dynamic.example.com:443 privileged,cert=dynamicPinningBad # Host for static pin tests -https://include-subdomains.pinning.example.com:443 privileged,cert=staticPinningBad +https://badchain.include-subdomains.pinning.example.com:443 privileged,cert=staticPinningBad +https://fail-handshake.example.com:443 privileged,failHandshake # Hosts for sha1 console warning tests https://sha1ee.example.com:443 privileged,cert=sha1_end_entity diff --git a/testing/mochitest/runtests.py b/testing/mochitest/runtests.py index 8ba68cfc418..8ce2839b757 100644 --- a/testing/mochitest/runtests.py +++ b/testing/mochitest/runtests.py @@ -914,7 +914,7 @@ class SSLTunnel: config.write("redirhost:%s:%s:%s:%s\n" % (loc.host, loc.port, self.sslPort, redirhost)) - if self.useSSLTunnelExts and option in ('ssl3', 'rc4'): + if self.useSSLTunnelExts and option in ('ssl3', 'rc4', 'failHandshake'): config.write("%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort)) def buildConfig(self, locations): diff --git a/testing/mochitest/ssltunnel/ssltunnel.cpp b/testing/mochitest/ssltunnel/ssltunnel.cpp index 187244ac9f0..2f8855e1b8e 100644 --- a/testing/mochitest/ssltunnel/ssltunnel.cpp +++ b/testing/mochitest/ssltunnel/ssltunnel.cpp @@ -155,6 +155,7 @@ typedef struct { PLHashTable* host_redir_table; PLHashTable* host_ssl3_table; PLHashTable* host_rc4_table; + PLHashTable* host_failhandshake_table; } server_info_t; typedef struct { @@ -259,10 +260,17 @@ void SignalShutdown() PR_Unlock(shutdown_lock); } +// available flags +enum { + USE_SSL3 = 1 << 0, + USE_RC4 = 1 << 1, + FAIL_HANDSHAKE = 1 << 2 +}; + bool ReadConnectRequest(server_info_t* server_info, relayBuffer& buffer, int32_t* result, string& certificate, client_auth_option* clientauth, string& host, string& location, - bool* ssl3, bool* rc4) + int32_t* flags) { if (buffer.present() < 4) { LOG_DEBUG((" !! only %d bytes present in the buffer", (int)buffer.present())); @@ -311,9 +319,17 @@ bool ReadConnectRequest(server_info_t* server_info, if (redir) location = static_cast(redir); - *ssl3 = !!PL_HashTableLookup(server_info->host_ssl3_table, token); + if (PL_HashTableLookup(server_info->host_ssl3_table, token)) { + *flags |= USE_SSL3; + } - *rc4 = !!PL_HashTableLookup(server_info->host_rc4_table, token); + if (PL_HashTableLookup(server_info->host_rc4_table, token)) { + *flags |= USE_RC4; + } + + if (PL_HashTableLookup(server_info->host_failhandshake_table, token)) { + *flags |= FAIL_HANDSHAKE; + } token = strtok2(_caret, "/", &_caret); if (strcmp(token, "HTTP")) { @@ -326,7 +342,7 @@ bool ReadConnectRequest(server_info_t* server_info, } bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, const string &certificate, - const client_auth_option clientAuth, bool ssl3, bool rc4) + const client_auth_option clientAuth, int32_t flags) { const char* certnick = certificate.empty() ? si->cert_nickname.c_str() : certificate.c_str(); @@ -349,6 +365,12 @@ bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, const strin return false; } + if (flags & FAIL_HANDSHAKE) { + // deliberately cause handshake to fail by sending the client a client hello + SSL_ResetHandshake(ssl_socket, false); + return true; + } + SSLKEAType certKEA = NSS_FindCertKEAType(cert); if (SSL_ConfigSecureServer(ssl_socket, cert, privKey, certKEA) != SECSuccess) { @@ -366,13 +388,13 @@ bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, const strin SSL_OptionSet(ssl_socket, SSL_REQUIRE_CERTIFICATE, clientAuth == caRequire); } - if (ssl3) { + if (flags & USE_SSL3) { SSLVersionRange range = { SSL_LIBRARY_VERSION_3_0, SSL_LIBRARY_VERSION_3_0 }; SSL_VersionRangeSet(ssl_socket, &range); } - if (rc4) { + if (flags & USE_RC4) { for (uint16_t i = 0; i < SSL_NumImplementedCiphers; ++i) { uint16_t cipher_id = SSL_ImplementedCiphers[i]; switch (cipher_id) { @@ -580,8 +602,7 @@ void HandleConnection(void* data) string locationHeader; client_auth_option clientAuth; string fullHost; - bool ssl3 = false; - bool rc4 = false; + int32_t flags = 0; LOG_DEBUG(("SSLTUNNEL(%p)): incoming connection csock(0)=%p, ssock(1)=%p\n", static_cast(data), @@ -595,8 +616,8 @@ void HandleConnection(void* data) if (!do_http_proxy) { - if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone, - ssl3, rc4)) + if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, + certificateToUse, caNone, flags)) client_error = true; else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout)) client_error = true; @@ -715,7 +736,7 @@ void HandleConnection(void* data) int32_t response; if (!connect_accepted && ReadConnectRequest(ci->server_info, buffers[s], &response, certificateToUse, &clientAuth, fullHost, locationHeader, - &ssl3, &rc4)) + &flags)) { // Mark this as a proxy-only connection (no SSL) if the CONNECT // request didn't come for port 443 or from any of the server's @@ -737,6 +758,9 @@ void HandleConnection(void* data) PL_HashTableEnumerateEntries(ci->server_info->host_rc4_table, match_hostname, &match); + PL_HashTableEnumerateEntries(ci->server_info->host_failhandshake_table, + match_hostname, + &match); ci->http_proxy_only = !match.matched; } else @@ -872,7 +896,7 @@ void HandleConnection(void* data) LOG_DEBUG((" not updating to SSL based on http_proxy_only for this socket")); } else if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, - certificateToUse, clientAuth, ssl3, rc4)) + certificateToUse, clientAuth, flags)) { LOG_ERRORD((" failed to config server socket\n")); client_error = true; @@ -1004,6 +1028,11 @@ PLHashTable* get_rc4_table(server_info_t* server) return server->host_rc4_table; } +PLHashTable* get_failhandshake_table(server_info_t* server) +{ + return server->host_failhandshake_table; +} + int parseWeakCryptoConfig(char* const& keyword, char*& _caret, PLHashTable* (*get_table)(server_info_t*)) { @@ -1179,6 +1208,14 @@ int processConfigLine(char* configLine) return 1; } + server.host_failhandshake_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings, + PL_CompareStrings, nullptr, nullptr);; + if (!server.host_failhandshake_table) + { + LOG_ERROR(("Internal, could not create hash table\n")); + return 1; + } + servers.push_back(server); } @@ -1298,6 +1335,10 @@ int processConfigLine(char* configLine) return parseWeakCryptoConfig(keyword, _caret, get_rc4_table); } + if (!strcmp(keyword, "failHandshake")) { + return parseWeakCryptoConfig(keyword, _caret, get_failhandshake_table); + } + // Configure the NSS certificate database directory if (!strcmp(keyword, "certdbdir")) { @@ -1518,11 +1559,13 @@ int main(int argc, char** argv) PL_HashTableEnumerateEntries(it->host_redir_table, freeHostRedirHashItems, nullptr); PL_HashTableEnumerateEntries(it->host_ssl3_table, freeSSL3HashItems, nullptr); PL_HashTableEnumerateEntries(it->host_rc4_table, freeRC4HashItems, nullptr); + PL_HashTableEnumerateEntries(it->host_failhandshake_table, freeRC4HashItems, nullptr); PL_HashTableDestroy(it->host_cert_table); PL_HashTableDestroy(it->host_clientauth_table); PL_HashTableDestroy(it->host_redir_table); PL_HashTableDestroy(it->host_ssl3_table); PL_HashTableDestroy(it->host_rc4_table); + PL_HashTableDestroy(it->host_failhandshake_table); } PR_Cleanup();