From 9621ccaf05900009d67cdadeb1aac27368114a61 Mon Sep 17 00:00:00 2001 From: Nikola Dipanov Date: Wed, 1 Apr 2015 14:42:00 +0100 Subject: [PATCH] websocketproxy: Make protocol validation use connection_info With this change, we avoid the need to have config '*_baseurl' config options correctly set up on nodes running console-proxy services, as the access_url is now available after token validation. UpgradeImpact: Websocket proxies need to be upgraded in a lockstep with the API nodes up to this commit (or when upgrading to Kilo), as older API nodes will not be sending the access_url when authorizing console access, and newer proxy services (this commit and onward) would fail to authorize such requests. Change-Id: I721bca407bf9d3fb33a3461c04d392284448d4a6 Closes-bug: #1442048 --- nova/console/websocketproxy.py | 43 +++----- .../tests/unit/console/test_websocketproxy.py | 98 ++++--------------- 2 files changed, 33 insertions(+), 108 deletions(-) diff --git a/nova/console/websocketproxy.py b/nova/console/websocketproxy.py index b544e8a3f915..9e59c2a2e3a0 100644 --- a/nova/console/websocketproxy.py +++ b/nova/console/websocketproxy.py @@ -30,15 +30,9 @@ from nova.consoleauth import rpcapi as consoleauth_rpcapi from nova import context from nova import exception from nova.i18n import _ -from oslo_config import cfg LOG = logging.getLogger(__name__) -CONF = cfg.CONF -CONF.import_opt('novncproxy_base_url', 'nova.vnc') -CONF.import_opt('html5proxy_base_url', 'nova.spice', group='spice') -CONF.import_opt('base_url', 'nova.console.serial', group='serial_console') - class NovaProxyRequestHandlerBase(object): def address_string(self): @@ -47,27 +41,21 @@ class NovaProxyRequestHandlerBase(object): # deployments due to DNS configuration and break VNC access completely return str(self.client_address[0]) - def verify_origin_proto(self, console_type, origin_proto): - if console_type == 'novnc': - expected_protos = \ - [urlparse.urlparse(CONF.novncproxy_base_url).scheme] - elif console_type == 'spice-html5': - expected_protos = \ - [urlparse.urlparse(CONF.spice.html5proxy_base_url).scheme] - elif console_type == 'serial': - expected_protos = \ - [urlparse.urlparse(CONF.serial_console.base_url).scheme] - # NOTE: For serial consoles the expected protocol could be ws or - # wss which correspond to http and https respectively in terms of - # security. - if 'ws' in expected_protos: - expected_protos.append('http') - if 'wss' in expected_protos: - expected_protos.append('https') - else: - detail = _("Invalid Console Type for WebSocketProxy: '%s'") % \ - console_type + def verify_origin_proto(self, connection_info, origin_proto): + access_url = connection_info.get('access_url') + if not access_url: + detail = _("No access_url in connection_info. " + "Cannot validate protocol") raise exception.ValidationError(detail=detail) + expected_protos = [urlparse.urlparse(access_url).scheme] + # NOTE: For serial consoles the expected protocol could be ws or + # wss which correspond to http and https respectively in terms of + # security. + if 'ws' in expected_protos: + expected_protos.append('http') + if 'wss' in expected_protos: + expected_protos.append('https') + return origin_proto in expected_protos def new_websocket_client(self): @@ -125,8 +113,7 @@ class NovaProxyRequestHandlerBase(object): 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): + if not self.verify_origin_proto(connect_info, origin_scheme): detail = _("Origin header protocol does not match this host.") raise exception.ValidationError(detail=detail) diff --git a/nova/tests/unit/console/test_websocketproxy.py b/nova/tests/unit/console/test_websocketproxy.py index 0fc62732f9ad..4bebc0947b18 100644 --- a/nova/tests/unit/console/test_websocketproxy.py +++ b/nova/tests/unit/console/test_websocketproxy.py @@ -35,14 +35,6 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): 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') - CONF.set_override('base_url', - 'ws://example.net:6080', - 'serial_console') def _fake_getheader(self, header): if header == 'cookie': @@ -109,7 +101,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): check_token.return_value = { 'host': 'node1', 'port': '10000', - 'console_type': 'novnc' + 'console_type': 'novnc', + 'access_url': 'https://example.net:6080' } self.wh.socket.return_value = '' self.wh.path = "http://127.0.0.1/?token=123-456-789" @@ -132,58 +125,14 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): self.wh.new_websocket_client) check_token.assert_called_with(mock.ANY, token="XXX") - @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_novnc(self, check_token): - check_token.return_value = { - 'host': 'node1', - 'port': '10000', - 'console_type': 'novnc' - } - self.wh.socket.return_value = '' - self.wh.path = "http://127.0.0.1/" - self.wh.headers.getheader = self._fake_getheader - - self.wh.new_websocket_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('') - - @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_serial(self, check_token): - check_token.return_value = { - 'host': 'node1', - 'port': '10000', - 'console_type': 'serial' - } - self.wh.socket.return_value = '' - self.wh.path = "http://127.0.0.1/" - self.wh.headers.getheader = self._fake_getheader_http - - self.wh.new_websocket_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('') - - @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_novnc_token_invalid(self, check_token): - check_token.return_value = False - - self.wh.path = "http://127.0.0.1/" - self.wh.headers.getheader = self._fake_getheader_bad_token - - self.assertRaises(exception.InvalidToken, - self.wh.new_websocket_client) - check_token.assert_called_with(mock.ANY, token="XXX") - @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') def test_new_websocket_client_internal_access_path(self, check_token): check_token.return_value = { 'host': 'node1', 'port': '10000', 'internal_access_path': 'vmid', - 'console_type': 'novnc' + 'console_type': 'novnc', + 'access_url': 'https://example.net:6080' } tsock = mock.MagicMock() @@ -205,7 +154,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): 'host': 'node1', 'port': '10000', 'internal_access_path': 'xxx', - 'console_type': 'novnc' + 'console_type': 'novnc', + 'access_url': 'https://example.net:6080' } tsock = mock.MagicMock() @@ -227,7 +177,8 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): check_token.return_value = { 'host': 'node1', 'port': '10000', - 'console_type': 'novnc' + 'console_type': 'novnc', + 'access_url': 'https://example.net:6080' } self.wh.socket.return_value = '' self.wh.path = "http://127.0.0.1/?token=123-456-789" @@ -319,42 +270,29 @@ class NovaProxyRequestHandlerBaseTestCase(test.NoDBTestCase): self.wh.do_proxy.assert_called_with('') @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_novnc_bad_origin_proto_vnc(self, - check_token): + def test_new_websocket_client_novnc_https_origin_proto_http(self, + check_token): check_token.return_value = { 'host': 'node1', 'port': '10000', - 'console_type': 'novnc' + 'console_type': 'novnc', + 'access_url': 'http://example.net:6080' } - self.wh.path = "http://127.0.0.1/" - self.wh.headers.getheader = self._fake_getheader_http + self.wh.path = "https://127.0.0.1/" + self.wh.headers.getheader = self._fake_getheader self.assertRaises(exception.ValidationError, self.wh.new_websocket_client) @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_novnc_bad_origin_proto_spice(self, - check_token): + def test_new_websocket_client_novnc_https_origin_proto_ws(self, + check_token): check_token.return_value = { 'host': 'node1', 'port': '10000', - 'console_type': 'spice-html5' - } - - self.wh.path = "http://127.0.0.1/" - self.wh.headers.getheader = self._fake_getheader_http - - self.assertRaises(exception.ValidationError, - self.wh.new_websocket_client) - - @mock.patch('nova.consoleauth.rpcapi.ConsoleAuthAPI.check_token') - def test_new_websocket_client_novnc_https_origin_proto_serial(self, - check_token): - check_token.return_value = { - 'host': 'node1', - 'port': '10000', - 'console_type': 'serial' + 'console_type': 'serial', + 'access_url': 'ws://example.net:6080' } self.wh.path = "https://127.0.0.1/"