blob: 6ebdbdb086d33cc1977ac20d7c0a0e02828674fb [file] [log] [blame]
Matthew Treinish90e2a6d2017-02-06 19:56:43 -05001# Copyright 2016-2017 OpenStack Foundation
Michelle Mandel1f87a562016-07-15 17:11:33 -04002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Michelle Mandel1f87a562016-07-15 17:11:33 -040016import struct
17
18import six
melanie witt27ba9332019-04-26 02:33:20 +000019import six.moves.urllib.parse as urlparse
Michelle Mandel1f87a562016-07-15 17:11:33 -040020import urllib3
21
22from tempest.api.compute import base
Markus Zoellerae36ce82017-03-20 16:27:26 +010023from tempest.common import compute
Michelle Mandel1f87a562016-07-15 17:11:33 -040024from tempest import config
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -080025from tempest.lib import decorators
Michelle Mandel1f87a562016-07-15 17:11:33 -040026
27CONF = config.CONF
28
29
30class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
zhufl67b3d382020-05-25 13:35:12 +080031 """Test novnc console"""
32
Eric Friedbfaa50f2020-01-09 12:04:54 -060033 create_default_network = True
Michelle Mandel1f87a562016-07-15 17:11:33 -040034
35 @classmethod
36 def skip_checks(cls):
37 super(NoVNCConsoleTestJSON, cls).skip_checks()
38 if not CONF.compute_feature_enabled.vnc_console:
39 raise cls.skipException('VNC Console feature is disabled.')
40
41 def setUp(self):
42 super(NoVNCConsoleTestJSON, self).setUp()
43 self._websocket = None
44
45 def tearDown(self):
Michelle Mandel1f87a562016-07-15 17:11:33 -040046 super(NoVNCConsoleTestJSON, self).tearDown()
47 if self._websocket is not None:
48 self._websocket.close()
zhufle913e462018-07-25 17:16:10 +080049 # NOTE(zhufl): Because server_check_teardown will raise Exception
50 # which will prevent other cleanup steps from being executed, so
51 # server_check_teardown should be called after super's tearDown.
52 self.server_check_teardown()
Michelle Mandel1f87a562016-07-15 17:11:33 -040053
54 @classmethod
55 def setup_clients(cls):
56 super(NoVNCConsoleTestJSON, cls).setup_clients()
57 cls.client = cls.servers_client
58
59 @classmethod
60 def resource_setup(cls):
61 super(NoVNCConsoleTestJSON, cls).resource_setup()
62 cls.server = cls.create_test_server(wait_until="ACTIVE")
zhuflc32ee7d2018-03-28 17:12:32 +080063 cls.use_get_remote_console = False
64 if not cls.is_requested_microversion_compatible('2.5'):
65 cls.use_get_remote_console = True
Michelle Mandel1f87a562016-07-15 17:11:33 -040066
67 def _validate_novnc_html(self, vnc_url):
68 """Verify we can connect to novnc and get back the javascript."""
69 resp = urllib3.PoolManager().request('GET', vnc_url)
70 # Make sure that the GET request was accepted by the novncproxy
71 self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050072 'initial call: ' + six.text_type(resp.status))
Michelle Mandel1f87a562016-07-15 17:11:33 -040073 # Do some basic validation to make sure it is an expected HTML document
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050074 resp_data = resp.data.decode()
melanie witt27ba9332019-04-26 02:33:20 +000075 # This is needed in the case of example: <html lang="en">
76 self.assertRegex(resp_data, '<html.*>',
77 'Not a valid html document in the response.')
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050078 self.assertIn('</html>', resp_data,
79 'Not a valid html document in the response.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040080 # Just try to make sure we got JavaScript back for noVNC, since we
81 # won't actually use it since not inside of a browser
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050082 self.assertIn('noVNC', resp_data,
83 'Not a valid noVNC javascript html document.')
84 self.assertIn('<script', resp_data,
85 'Not a valid noVNC javascript html document.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040086
87 def _validate_rfb_negotiation(self):
88 """Verify we can connect to novnc and do the websocket connection."""
89 # Turn the Socket into a WebSocket to do the communication
90 data = self._websocket.receive_frame()
Masayuki Igawa0c0f0142017-04-10 17:22:02 +090091 self.assertFalse(data is None or not data,
Michelle Mandel1f87a562016-07-15 17:11:33 -040092 'Token must be invalid because the connection '
93 'closed.')
94 # Parse the RFB version from the data to make sure it is valid
jianghua44a0f392017-03-13 04:14:26 +000095 # and belong to the known supported RFB versions.
Michelle Mandel1f87a562016-07-15 17:11:33 -040096 version = float("%d.%d" % (int(data[4:7], base=10),
97 int(data[8:11], base=10)))
jianghua44a0f392017-03-13 04:14:26 +000098 # Add the max RFB versions supported
99 supported_versions = [3.3, 3.8]
100 self.assertIn(version, supported_versions,
101 'Bad RFB Version: ' + str(version))
102 # Send our RFB version to the server
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500103 self._websocket.send_frame(data)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400104 # Get the sever authentication type and make sure None is supported
105 data = self._websocket.receive_frame()
106 self.assertIsNotNone(data, 'Expected authentication type None.')
jianghua44a0f392017-03-13 04:14:26 +0000107 data_length = len(data)
108 if version == 3.3:
109 # For RFB 3.3: in the security handshake, rather than a two-way
110 # negotiation, the server decides the security type and sends a
111 # single word(4 bytes).
112 self.assertEqual(
113 data_length, 4, 'Expected authentication type None.')
likui7d91c872020-09-22 12:29:16 +0800114 self.assertIn(1, [int(data[i]) for i in (0, 3)],
jianghua44a0f392017-03-13 04:14:26 +0000115 'Expected authentication type None.')
116 else:
117 self.assertGreaterEqual(
118 len(data), 2, 'Expected authentication type None.')
119 self.assertIn(
120 1,
likui7d91c872020-09-22 12:29:16 +0800121 [int(data[i + 1]) for i in range(int(data[0]))],
jianghua44a0f392017-03-13 04:14:26 +0000122 'Expected authentication type None.')
123 # Send to the server that we only support authentication
124 # type None
125 self._websocket.send_frame(six.int2byte(1))
126
127 # The server should send 4 bytes of 0's if security
128 # handshake succeeded
129 data = self._websocket.receive_frame()
130 self.assertEqual(
131 len(data), 4,
132 'Server did not think security was successful.')
133 self.assertEqual(
likui7d91c872020-09-22 12:29:16 +0800134 [int(i) for i in data], [0, 0, 0, 0],
jianghua44a0f392017-03-13 04:14:26 +0000135 'Server did not think security was successful.')
136
Michelle Mandel1f87a562016-07-15 17:11:33 -0400137 # Say to leave the desktop as shared as part of client initialization
138 self._websocket.send_frame(six.int2byte(1))
139 # Get the server initialization packet back and make sure it is the
140 # right structure where bytes 20-24 is the name length and
141 # 24-N is the name
142 data = self._websocket.receive_frame()
143 data_length = len(data) if data is not None else 0
144 self.assertFalse(data_length <= 24 or
145 data_length != (struct.unpack(">L",
afazekas40fcb9b2019-03-08 11:25:11 +0100146 data[20:24])[0] + 24),
Michelle Mandel1f87a562016-07-15 17:11:33 -0400147 'Server initialization was not the right format.')
148 # Since the rest of the data on the screen is arbitrary, we will
149 # close the socket and end our validation of the data at this point
150 # Assert that the latest check was false, meaning that the server
151 # initialization was the right format
152 self.assertFalse(data_length <= 24 or
153 data_length != (struct.unpack(">L",
afazekas40fcb9b2019-03-08 11:25:11 +0100154 data[20:24])[0] + 24))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400155
156 def _validate_websocket_upgrade(self):
Leo Henkenfd01d152019-08-02 11:42:52 -0500157 """Verify that the websocket upgrade was successful.
158
159 Parses response and ensures that required response
160 fields are present and accurate.
161 (https://tools.ietf.org/html/rfc7231#section-6.2.2)
162 """
163
Michelle Mandel1f87a562016-07-15 17:11:33 -0400164 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500165 self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
Leo Henkenfd01d152019-08-02 11:42:52 -0500166 b'Protocols'),
167 'Incorrect HTTP return status code: {}'.format(
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200168 six.text_type(self._websocket.response)
169 )
170 )
Leo Henkenfd01d152019-08-02 11:42:52 -0500171 _required_header = 'upgrade: websocket'
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200172 _response = six.text_type(self._websocket.response).lower()
173 self.assertIn(
Leo Henkenfd01d152019-08-02 11:42:52 -0500174 _required_header,
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200175 _response,
176 'Did not get the expected WebSocket HTTP Response.'
177 )
Michelle Mandel1f87a562016-07-15 17:11:33 -0400178
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800179 @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400180 def test_novnc(self):
zhufl67b3d382020-05-25 13:35:12 +0800181 """Test accessing novnc console of server"""
zhuflc32ee7d2018-03-28 17:12:32 +0800182 if self.use_get_remote_console:
183 body = self.client.get_remote_console(
184 self.server['id'], console_type='novnc',
185 protocol='vnc')['remote_console']
186 else:
187 body = self.client.get_vnc_console(self.server['id'],
188 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400189 self.assertEqual('novnc', body['type'])
190 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
191 self._validate_novnc_html(body['url'])
192 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
Markus Zoellerae36ce82017-03-20 16:27:26 +0100193 self._websocket = compute.create_websocket(body['url'])
shangxiaobj284d3112017-08-13 23:37:34 -0700194 # Validate that we successfully connected and upgraded to Web Sockets
Michelle Mandel1f87a562016-07-15 17:11:33 -0400195 self._validate_websocket_upgrade()
196 # Validate the RFB Negotiation to determine if a valid VNC session
197 self._validate_rfb_negotiation()
198
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800199 @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400200 def test_novnc_bad_token(self):
zhufl67b3d382020-05-25 13:35:12 +0800201 """Test accessing novnc console with bad token
202
203 Do the WebSockify HTTP Request to novnc proxy with a bad token,
204 the novnc proxy should reject the connection and closed it.
205 """
zhuflc32ee7d2018-03-28 17:12:32 +0800206 if self.use_get_remote_console:
207 body = self.client.get_remote_console(
208 self.server['id'], console_type='novnc',
209 protocol='vnc')['remote_console']
210 else:
211 body = self.client.get_vnc_console(self.server['id'],
212 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400213 self.assertEqual('novnc', body['type'])
214 # Do the WebSockify HTTP Request to novncproxy with a bad token
melanie witt27ba9332019-04-26 02:33:20 +0000215 parts = urlparse.urlparse(body['url'])
216 qparams = urlparse.parse_qs(parts.query)
217 if 'path' in qparams:
218 qparams['path'] = urlparse.unquote(qparams['path'][0]).replace(
219 'token=', 'token=bad')
220 elif 'token' in qparams:
221 qparams['token'] = 'bad' + qparams['token'][0]
222 new_query = urlparse.urlencode(qparams)
223 new_parts = urlparse.ParseResult(parts.scheme, parts.netloc,
224 parts.path, parts.params, new_query,
225 parts.fragment)
226 url = urlparse.urlunparse(new_parts)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100227 self._websocket = compute.create_websocket(url)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400228 # Make sure the novncproxy rejected the connection and closed it
229 data = self._websocket.receive_frame()
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900230 self.assertTrue(data is None or not data,
Michelle Mandel1f87a562016-07-15 17:11:33 -0400231 "The novnc proxy actually sent us some data, but we "
232 "expected it to close the connection.")