bug 664284: Add HSTS support for websockets. r=mcmanus

This commit is contained in:
Patrick McManus 2011-12-15 15:23:00 -08:00
parent 6d7a2597ed
commit ea03e8ad77
7 changed files with 212 additions and 44 deletions

View File

@ -304,6 +304,7 @@ nsWebSocket::OnStart(nsISupports *aContext)
}
mChannel->GetExtensions(mEstablishedExtensions);
UpdateURI();
SetReadyState(nsIMozWebSocket::OPEN);
return NS_OK;
@ -1040,6 +1041,27 @@ nsWebSocket::DontKeepAliveAnyMore()
mCheckMustKeepAlive = false;
}
nsresult
nsWebSocket::UpdateURI()
{
// Check for Redirections
nsCOMPtr<nsIURI> uri;
nsresult rv = mChannel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, rv);
nsCAutoString spec;
rv = uri->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
CopyUTF8toUTF16(spec, mEffectiveURL);
bool isWSS = false;
rv = uri->SchemeIs("wss", &isWSS);
NS_ENSURE_SUCCESS(rv, rv);
mSecure = isWSS ? true : false;
return NS_OK;
}
NS_IMETHODIMP
nsWebSocket::RemoveEventListener(const nsAString& aType,
nsIDOMEventListener* aListener,
@ -1081,7 +1103,11 @@ nsWebSocket::AddEventListener(const nsAString& aType,
NS_IMETHODIMP
nsWebSocket::GetUrl(nsAString& aURL)
{
aURL = mOriginalURL;
if (mEffectiveURL.IsEmpty()) {
aURL = mOriginalURL;
} else {
aURL = mEffectiveURL;
}
return NS_OK;
}

View File

@ -147,6 +147,8 @@ protected:
// (and possibly collected).
void DontKeepAliveAnyMore();
nsresult UpdateURI();
nsCOMPtr<nsIWebSocketChannel> mChannel;
nsRefPtr<nsDOMEventListenerWrapper> mOnOpenListener;
@ -156,6 +158,7 @@ protected:
// related to the WebSocket constructor steps
nsString mOriginalURL;
nsString mEffectiveURL; // after redirects
bool mSecure; // if true it is using SSL and the wss scheme,
// otherwise it is using the ws scheme with no SSL

View File

@ -15,22 +15,19 @@ def web_socket_do_extra_handshake(request):
if request.ws_protocol == "test-2.1":
time.sleep(3)
pass
elif request.ws_protocol == "test-9":
time.sleep(3)
pass
elif request.ws_protocol == "test-10":
time.sleep(3)
pass
elif request.ws_protocol == "test-19":
raise ValueError('Aborting (test-19)')
elif request.ws_protocol == "test-20" or request.ws_protocol == "test-17":
time.sleep(3)
pass
elif request.ws_protocol == "test-22":
# The timeout is 5 seconds
time.sleep(13)
pass
elif request.ws_protocol == "test-41b":
request.sts = "max-age=100"
else:
pass

View File

@ -120,7 +120,7 @@ function ignoreError(e)
{
}
function CreateTestWS(ws_location, ws_protocol)
function CreateTestWS(ws_location, ws_protocol, no_increment_test)
{
var ws;
@ -134,7 +134,7 @@ function CreateTestWS(ws_location, ws_protocol)
ws._testNumber = current_test;
ws._receivedCloseEvent = false;
ok(true, "added testNumber: " + ws._testNumber +"\n");
ok(true, "Created websocket for test " + ws._testNumber +"\n");
ws.onerror = function(e)
{
@ -149,7 +149,9 @@ function CreateTestWS(ws_location, ws_protocol)
throw e;
}
finally {
current_test++;
if (!no_increment_test) {
current_test++;
}
}
all_ws.push(ws);
@ -1138,8 +1140,59 @@ function test40()
function test41()
{
// reserve test41 for HSTS - bug 664284
doTest(42);
var ws = CreateTestWS("ws://example.com/tests/content/base/test/file_websocket", "test-41a", 1);
ws.onopen = function(e)
{
ok(true, "test 41a open");
ok(ws.url == "ws://example.com/tests/content/base/test/file_websocket",
"test 41a initial ws should not be redirected");
ws.close();
};
ws.onclose = function(e)
{
ok(true, "test 41a close");
// establish a hsts policy for example.com
var wsb = CreateTestWS("wss://example.com/tests/content/base/test/file_websocket", "test-41b", 1);
wsb.onopen = function(e)
{
ok(true, "test 41b open");
wsb.close();
}
wsb.onclose = function(e)
{
ok(true, "test 41b close");
// try ws:// again, it should be done over wss:// now due to hsts
var wsc = CreateTestWS("ws://example.com/tests/content/base/test/file_websocket", "test-41c");
wsc.onopen = function(e)
{
ok(true, "test 41c open");
ok(wsc.url == "wss://example.com/tests/content/base/test/file_websocket",
"test 41c ws should be redirected by hsts to wss");
wsc.close();
}
wsc.onclose = function(e)
{
ok(true, "test 41c close");
// clean up the STS state
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
const Cc = Components.classes;
const Ci = Components.interfaces;
var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var thehost = ios.newURI("http://example.com", null, null);
var stss = Cc["@mozilla.org/stsservice;1"].getService(Ci.nsIStrictTransportSecurityService);
stss.removeStsState(thehost);
doTest(42);
}
}
}
}
function test42()

View File

@ -372,9 +372,11 @@ NS_IMPL_THREADSAFE_ISUPPORTS1(OutboundEnqueuer, nsIRunnable)
// nsWSAdmissionManager
//-----------------------------------------------------------------------------
// Section 5.1 requires that a client rate limit its connects to a single
// TCP session in the CONNECTING state (i.e. anything before the 101 upgrade
// complete response comes back and an open javascript event is created)
// Section 4.1 requires that only a single websocket at a time can be CONNECTING
// to any given IP address (or hostname, if proxy doing DNS for us). This class
// ensures that we delay connecting until any pending connection for the same
// IP/addr is complete (i.e. until before the 101 upgrade complete response
// comes back and an 'open' javascript event is created)
class nsWSAdmissionManager
{
@ -414,6 +416,9 @@ public:
// we could hash this, but the dataset is expected to be
// small
// There may already be another WS channel connecting to this IP address, in
// which case we'll still create a new nsOpenConn but defer BeginOpen until
// that channel completes connecting.
bool found = (IndexOf(aStr) >= 0);
nsOpenConn *newdata = new nsOpenConn(aStr, ws);
mData.AppendElement(newdata);
@ -1938,34 +1943,60 @@ WebSocketChannel::AsyncOnChannelRedirect(
rv = newChannel->GetURI(getter_AddRefs(newuri));
NS_ENSURE_SUCCESS(rv, rv);
if (!mAutoFollowRedirects) {
nsCAutoString spec;
if (NS_SUCCEEDED(newuri->GetSpec(spec)))
LOG(("WebSocketChannel: Redirect to %s denied by configuration\n",
spec.get()));
callback->OnRedirectVerifyCallback(NS_ERROR_FAILURE);
return NS_OK;
}
bool isHttps = false;
rv = newuri->SchemeIs("https", &isHttps);
// newuri is expected to be http or https
bool newuriIsHttps = false;
rv = newuri->SchemeIs("https", &newuriIsHttps);
NS_ENSURE_SUCCESS(rv, rv);
if (mEncrypted && !isHttps) {
if (!mAutoFollowRedirects) {
// Even if redirects configured off, still allow them for HTTP Strict
// Transport Security (from ws://FOO to https://FOO (mapped to wss://FOO)
nsCOMPtr<nsIURI> clonedNewURI;
rv = newuri->Clone(getter_AddRefs(clonedNewURI));
NS_ENSURE_SUCCESS(rv, rv);
rv = clonedNewURI->SetScheme(NS_LITERAL_CSTRING("ws"));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> currentURI;
rv = GetURI(getter_AddRefs(currentURI));
NS_ENSURE_SUCCESS(rv, rv);
// currentURI is expected to be ws or wss
bool currentIsHttps = false;
rv = currentURI->SchemeIs("wss", &currentIsHttps);
NS_ENSURE_SUCCESS(rv, rv);
bool uriEqual = false;
rv = clonedNewURI->Equals(currentURI, &uriEqual);
NS_ENSURE_SUCCESS(rv, rv);
// It's only a HSTS redirect if we started with non-secure, are going to
// secure, and the new URI is otherwise the same as the old one.
if (!(!currentIsHttps && newuriIsHttps && uriEqual)) {
nsCAutoString newSpec;
rv = newuri->GetSpec(newSpec);
NS_ENSURE_SUCCESS(rv, rv);
LOG(("WebSocketChannel: Redirect to %s denied by configuration\n",
newSpec.get()));
return NS_ERROR_FAILURE;
}
}
if (mEncrypted && !newuriIsHttps) {
nsCAutoString spec;
if (NS_SUCCEEDED(newuri->GetSpec(spec)))
LOG(("WebSocketChannel: Redirect to %s violates encryption rule\n",
spec.get()));
callback->OnRedirectVerifyCallback(NS_ERROR_FAILURE);
return NS_OK;
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIHttpChannel> newHttpChannel = do_QueryInterface(newChannel, &rv);
if (NS_FAILED(rv)) {
LOG(("WebSocketChannel: Redirect could not QI to HTTP\n"));
callback->OnRedirectVerifyCallback(rv);
return NS_OK;
return rv;
}
nsCOMPtr<nsIHttpChannelInternal> newUpgradeChannel =
@ -1973,21 +2004,26 @@ WebSocketChannel::AsyncOnChannelRedirect(
if (NS_FAILED(rv)) {
LOG(("WebSocketChannel: Redirect could not QI to HTTP Upgrade\n"));
callback->OnRedirectVerifyCallback(rv);
return NS_OK;
return rv;
}
// The redirect is likely OK
newChannel->SetNotificationCallbacks(this);
mURI = newuri;
mEncrypted = newuriIsHttps;
newuri->Clone(getter_AddRefs(mURI));
if (mEncrypted)
rv = mURI->SetScheme(NS_LITERAL_CSTRING("wss"));
else
rv = mURI->SetScheme(NS_LITERAL_CSTRING("ws"));
mHttpChannel = newHttpChannel;
mChannel = newUpgradeChannel;
rv = SetupRequest();
if (NS_FAILED(rv)) {
LOG(("WebSocketChannel: Redirect could not SetupRequest()\n"));
callback->OnRedirectVerifyCallback(rv);
return NS_OK;
return rv;
}
// We cannot just tell the callback OK right now due to the 1 connect at a
@ -2004,8 +2040,8 @@ WebSocketChannel::AsyncOnChannelRedirect(
rv = ApplyForAdmission();
if (NS_FAILED(rv)) {
LOG(("WebSocketChannel: Redirect failed due to DNS failure\n"));
callback->OnRedirectVerifyCallback(rv);
mRedirectCallback = nsnull;
return rv;
}
return NS_OK;

View File

@ -34,13 +34,8 @@ STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
# MOZILLA: do not normalize away symlinks in mochitest
# path = os.path.realpath(path)
- There's also some code in mod_pywebsocket/_stream_base.py that may or may not
need to change to support Python 2.5:
#raise ConnectionTerminatedException(
# 'Receiving %d byte failed. Peer (%r) closed connection' %
# (length, (self._request.connection.remote_addr,)))
raise ConnectionTerminatedException('connection terminated: read failed')
- We need to apply the patch to hybi.py that makes HSTS work: (attached at end
of this README)
- Test and make sure the code works:
@ -49,3 +44,53 @@ STEPS TO UPDATE MOZILLA TO NEWER PYWEBSOCKET VERSION
- If this doesn't take a look at the pywebsocket server log,
$OBJDIR/_tests/testing/mochitest/websock.log
- Upgrade the svnversion number at top of this file to whatever version we're
now based off of.
--------------------------------------------------------------------------------
PATCH TO hybi.py for HSTS support:
diff --git a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
--- a/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
+++ b/testing/mochitest/pywebsocket/mod_pywebsocket/handshake/hybi.py
@@ -227,16 +227,19 @@ class Handshaker(object):
def _check_version(self):
unused_value = validate_mandatory_header(
self._request, common.SEC_WEBSOCKET_VERSION_HEADER,
str(common.VERSION_HYBI_LATEST), fail_status=426)
def _set_protocol(self):
self._request.ws_protocol = None
+ # MOZILLA
+ self._request.sts = None
+ # /MOZILLA
protocol_header = self._request.headers_in.get(
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
if not protocol_header:
self._request.ws_requested_protocols = None
return
@@ -311,16 +314,21 @@ class Handshaker(object):
response.append(format_header(
common.SEC_WEBSOCKET_PROTOCOL_HEADER,
self._request.ws_protocol))
if (self._request.ws_extensions is not None and
len(self._request.ws_extensions) != 0):
response.append(format_header(
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
format_extensions(self._request.ws_extensions)))
+ # MOZILLA: Add HSTS header if requested to
+ if self._request.sts is not None:
+ response.append(format_header("Strict-Transport-Security",
+ self._request.sts))
+ # /MOZILLA
response.append('\r\n')
raw_response = ''.join(response)
self._logger.debug('Opening handshake response: %r', raw_response)
self._request.connection.write(raw_response)

View File

@ -232,6 +232,9 @@ class Handshaker(object):
def _set_protocol(self):
self._request.ws_protocol = None
# MOZILLA
self._request.sts = None
# /MOZILLA
protocol_header = self._request.headers_in.get(
common.SEC_WEBSOCKET_PROTOCOL_HEADER)
@ -316,6 +319,11 @@ class Handshaker(object):
response.append(format_header(
common.SEC_WEBSOCKET_EXTENSIONS_HEADER,
format_extensions(self._request.ws_extensions)))
# MOZILLA: Add HSTS header if requested to
if self._request.sts is not None:
response.append(format_header("Strict-Transport-Security",
self._request.sts))
# /MOZILLA
response.append('\r\n')
raw_response = ''.join(response)