Merge "Websocket Proxy should verify Origin header" into stable/icehouse

This commit is contained in:
Jenkins 2015-03-13 00:44:31 +00:00 committed by Gerrit Code Review
commit ecac4bf998
2 changed files with 261 additions and 0 deletions

View File

@ -20,13 +20,22 @@ Leverages websockify.py by Joel Martin
import Cookie
import socket
import urlparse
import websockify
from nova.consoleauth import rpcapi as consoleauth_rpcapi
from nova import context
from nova import exception
from nova.openstack.common.gettextutils import _
from nova.openstack.common import log as logging
from oslo.config import cfg
CONF = cfg.CONF
CONF.import_opt('novncproxy_base_url', 'nova.vnc')
CONF.import_opt('html5proxy_base_url', 'nova.spice', group='spice')
CONF.import_opt('vnc_enabled', 'nova.vnc')
CONF.import_opt('enabled', 'nova.spice', group='spice')
LOG = logging.getLogger(__name__)
@ -37,6 +46,20 @@ class NovaWebSocketProxy(websockify.WebSocketProxy):
target_cfg=None,
ssl_target=None, *args, **kwargs)
def verify_origin_proto(self, console_type, origin_proto):
if console_type == 'novnc':
expected_proto = \
urlparse.urlparse(CONF.novncproxy_base_url).scheme
elif console_type == 'spice-html5':
expected_proto = \
urlparse.urlparse(CONF.spice.html5proxy_base_url).scheme
else:
detail = _("Invalid Console Type for WebSocketProxy: '%s'") % \
console_type
LOG.audit(detail)
raise exception.ValidationError(detail=detail)
return origin_proto == expected_proto
def new_client(self):
"""Called after a new WebSocket connection has been established."""
# Reopen the eventlet hub to make sure we don't share an epoll
@ -55,6 +78,28 @@ class NovaWebSocketProxy(websockify.WebSocketProxy):
LOG.audit("Invalid Token: %s", token)
raise Exception(_("Invalid Token"))
# Verify Origin
expected_origin_hostname = self.headers.getheader('Host')
if ':' in expected_origin_hostname:
e = expected_origin_hostname
expected_origin_hostname = e.split(':')[0]
origin_url = self.headers.getheader('Origin')
# missing origin header indicates non-browser client which is OK
if origin_url is not None:
origin = urlparse.urlparse(origin_url)
origin_hostname = origin.hostname
origin_scheme = origin.scheme
if origin_hostname == '' or origin_scheme == '':
detail = _("Origin header not valid.")
raise exception.ValidationError(detail=detail)
if expected_origin_hostname != origin_hostname:
detail = _("Origin header does not match this host.")
raise exception.ValidationError(detail=detail)
if not self.verify_origin_proto(connect_info['console_type'],
origin.scheme):
detail = _("Origin header protocol does not match this host.")
raise exception.ValidationError(detail=detail)
host = connect_info['host']
port = int(connect_info['port'])

View File

@ -0,0 +1,216 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Tests for nova websocketproxy."""
import mock
from nova.console import websocketproxy
from nova import exception
from nova import test
from oslo.config import cfg
CONF = cfg.CONF
class NovaProxyRequestHandlerBaseTestCase(test.TestCase):
def setUp(self):
super(NovaProxyRequestHandlerBaseTestCase, self).setUp()
self.wh = websocketproxy.NovaWebSocketProxy()
self.wh.socket = mock.MagicMock()
self.wh.msg = mock.MagicMock()
self.wh.do_proxy = mock.MagicMock()
self.wh.headers = mock.MagicMock()
CONF.set_override('novncproxy_base_url',
'https://example.net:6080/vnc_auto.html')
CONF.set_override('html5proxy_base_url',
'https://example.net:6080/vnc_auto.html',
'spice')
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_new_client(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return 'https://example.net:6080'
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.wh.new_client()
check_token.assert_called_with(mock.ANY, token="123-456-789")
self.wh.socket.assert_called_with('node1', 10000, connect=True)
self.wh.do_proxy.assert_called_with('<socket>')
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_new_client_raises_with_invalid_origin(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return 'https://bad-origin-example.net:6080'
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.assertRaises(exception.ValidationError,
self.wh.new_client)
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_new_client_raises_with_blank_origin(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return ''
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.assertRaises(exception.ValidationError,
self.wh.new_client)
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_new_client_with_no_origin(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return None
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.wh.new_client()
check_token.assert_called_with(mock.ANY, token="123-456-789")
self.wh.socket.assert_called_with('node1', 10000, connect=True)
self.wh.do_proxy.assert_called_with('<socket>')
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_new_client_raises_with_wrong_proto_vnc(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return 'http://example.net:6080'
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'novnc'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.assertRaises(exception.ValidationError,
self.wh.new_client)
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_raises_with_wrong_proto_spice(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return 'http://example.net:6080'
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'spice-html5'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.assertRaises(exception.ValidationError,
self.wh.new_client)
@mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token')
def test_raises_with_bad_console_type(self, check_token):
def _fake_getheader(header):
if header == 'cookie':
return 'token="123-456-789"'
elif header == 'Origin':
return 'https://example.net:6080'
elif header == 'Host':
return 'example.net:6080'
else:
return
check_token.return_value = {
'host': 'node1',
'port': '10000',
'console_type': 'bad-console-type'
}
self.wh.socket.return_value = '<socket>'
self.wh.path = "http://127.0.0.1/?token=123-456-789"
self.wh.headers.getheader = _fake_getheader
self.assertRaises(exception.ValidationError,
self.wh.new_client)