blob: 7cf6d83d2bb6dbf5041f8078bf6baea14f046646 [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
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050029if six.PY2:
30 ord_func = ord
31else:
32 ord_func = int
33
Michelle Mandel1f87a562016-07-15 17:11:33 -040034
35class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
36
37 @classmethod
38 def skip_checks(cls):
39 super(NoVNCConsoleTestJSON, cls).skip_checks()
40 if not CONF.compute_feature_enabled.vnc_console:
41 raise cls.skipException('VNC Console feature is disabled.')
42
43 def setUp(self):
44 super(NoVNCConsoleTestJSON, self).setUp()
45 self._websocket = None
46
47 def tearDown(self):
Michelle Mandel1f87a562016-07-15 17:11:33 -040048 super(NoVNCConsoleTestJSON, self).tearDown()
49 if self._websocket is not None:
50 self._websocket.close()
zhufle913e462018-07-25 17:16:10 +080051 # NOTE(zhufl): Because server_check_teardown will raise Exception
52 # which will prevent other cleanup steps from being executed, so
53 # server_check_teardown should be called after super's tearDown.
54 self.server_check_teardown()
Michelle Mandel1f87a562016-07-15 17:11:33 -040055
56 @classmethod
57 def setup_clients(cls):
58 super(NoVNCConsoleTestJSON, cls).setup_clients()
59 cls.client = cls.servers_client
60
61 @classmethod
62 def resource_setup(cls):
63 super(NoVNCConsoleTestJSON, cls).resource_setup()
64 cls.server = cls.create_test_server(wait_until="ACTIVE")
zhuflc32ee7d2018-03-28 17:12:32 +080065 cls.use_get_remote_console = False
66 if not cls.is_requested_microversion_compatible('2.5'):
67 cls.use_get_remote_console = True
Michelle Mandel1f87a562016-07-15 17:11:33 -040068
69 def _validate_novnc_html(self, vnc_url):
70 """Verify we can connect to novnc and get back the javascript."""
71 resp = urllib3.PoolManager().request('GET', vnc_url)
72 # Make sure that the GET request was accepted by the novncproxy
73 self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050074 'initial call: ' + six.text_type(resp.status))
Michelle Mandel1f87a562016-07-15 17:11:33 -040075 # Do some basic validation to make sure it is an expected HTML document
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050076 resp_data = resp.data.decode()
melanie witt27ba9332019-04-26 02:33:20 +000077 # This is needed in the case of example: <html lang="en">
78 self.assertRegex(resp_data, '<html.*>',
79 'Not a valid html document in the response.')
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050080 self.assertIn('</html>', resp_data,
81 'Not a valid html document in the response.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040082 # Just try to make sure we got JavaScript back for noVNC, since we
83 # won't actually use it since not inside of a browser
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050084 self.assertIn('noVNC', resp_data,
85 'Not a valid noVNC javascript html document.')
86 self.assertIn('<script', resp_data,
87 'Not a valid noVNC javascript html document.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040088
89 def _validate_rfb_negotiation(self):
90 """Verify we can connect to novnc and do the websocket connection."""
91 # Turn the Socket into a WebSocket to do the communication
92 data = self._websocket.receive_frame()
Masayuki Igawa0c0f0142017-04-10 17:22:02 +090093 self.assertFalse(data is None or not data,
Michelle Mandel1f87a562016-07-15 17:11:33 -040094 'Token must be invalid because the connection '
95 'closed.')
96 # Parse the RFB version from the data to make sure it is valid
jianghua44a0f392017-03-13 04:14:26 +000097 # and belong to the known supported RFB versions.
Michelle Mandel1f87a562016-07-15 17:11:33 -040098 version = float("%d.%d" % (int(data[4:7], base=10),
99 int(data[8:11], base=10)))
jianghua44a0f392017-03-13 04:14:26 +0000100 # Add the max RFB versions supported
101 supported_versions = [3.3, 3.8]
102 self.assertIn(version, supported_versions,
103 'Bad RFB Version: ' + str(version))
104 # Send our RFB version to the server
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500105 self._websocket.send_frame(data)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400106 # Get the sever authentication type and make sure None is supported
107 data = self._websocket.receive_frame()
108 self.assertIsNotNone(data, 'Expected authentication type None.')
jianghua44a0f392017-03-13 04:14:26 +0000109 data_length = len(data)
110 if version == 3.3:
111 # For RFB 3.3: in the security handshake, rather than a two-way
112 # negotiation, the server decides the security type and sends a
113 # single word(4 bytes).
114 self.assertEqual(
115 data_length, 4, 'Expected authentication type None.')
116 self.assertIn(1, [ord_func(data[i]) for i in (0, 3)],
117 'Expected authentication type None.')
118 else:
119 self.assertGreaterEqual(
120 len(data), 2, 'Expected authentication type None.')
121 self.assertIn(
122 1,
123 [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
124 'Expected authentication type None.')
125 # Send to the server that we only support authentication
126 # type None
127 self._websocket.send_frame(six.int2byte(1))
128
129 # The server should send 4 bytes of 0's if security
130 # handshake succeeded
131 data = self._websocket.receive_frame()
132 self.assertEqual(
133 len(data), 4,
134 'Server did not think security was successful.')
135 self.assertEqual(
136 [ord_func(i) for i in data], [0, 0, 0, 0],
137 'Server did not think security was successful.')
138
Michelle Mandel1f87a562016-07-15 17:11:33 -0400139 # Say to leave the desktop as shared as part of client initialization
140 self._websocket.send_frame(six.int2byte(1))
141 # Get the server initialization packet back and make sure it is the
142 # right structure where bytes 20-24 is the name length and
143 # 24-N is the name
144 data = self._websocket.receive_frame()
145 data_length = len(data) if data is not None else 0
146 self.assertFalse(data_length <= 24 or
147 data_length != (struct.unpack(">L",
afazekas40fcb9b2019-03-08 11:25:11 +0100148 data[20:24])[0] + 24),
Michelle Mandel1f87a562016-07-15 17:11:33 -0400149 'Server initialization was not the right format.')
150 # Since the rest of the data on the screen is arbitrary, we will
151 # close the socket and end our validation of the data at this point
152 # Assert that the latest check was false, meaning that the server
153 # initialization was the right format
154 self.assertFalse(data_length <= 24 or
155 data_length != (struct.unpack(">L",
afazekas40fcb9b2019-03-08 11:25:11 +0100156 data[20:24])[0] + 24))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400157
158 def _validate_websocket_upgrade(self):
Leo Henkenfd01d152019-08-02 11:42:52 -0500159 """Verify that the websocket upgrade was successful.
160
161 Parses response and ensures that required response
162 fields are present and accurate.
163 (https://tools.ietf.org/html/rfc7231#section-6.2.2)
164 """
165
Michelle Mandel1f87a562016-07-15 17:11:33 -0400166 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500167 self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
Leo Henkenfd01d152019-08-02 11:42:52 -0500168 b'Protocols'),
169 'Incorrect HTTP return status code: {}'.format(
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200170 six.text_type(self._websocket.response)
171 )
172 )
Leo Henkenfd01d152019-08-02 11:42:52 -0500173 _required_header = 'upgrade: websocket'
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200174 _response = six.text_type(self._websocket.response).lower()
175 self.assertIn(
Leo Henkenfd01d152019-08-02 11:42:52 -0500176 _required_header,
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200177 _response,
178 'Did not get the expected WebSocket HTTP Response.'
179 )
Michelle Mandel1f87a562016-07-15 17:11:33 -0400180
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800181 @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400182 def test_novnc(self):
zhuflc32ee7d2018-03-28 17:12:32 +0800183 if self.use_get_remote_console:
184 body = self.client.get_remote_console(
185 self.server['id'], console_type='novnc',
186 protocol='vnc')['remote_console']
187 else:
188 body = self.client.get_vnc_console(self.server['id'],
189 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400190 self.assertEqual('novnc', body['type'])
191 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
192 self._validate_novnc_html(body['url'])
193 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
Markus Zoellerae36ce82017-03-20 16:27:26 +0100194 self._websocket = compute.create_websocket(body['url'])
shangxiaobj284d3112017-08-13 23:37:34 -0700195 # Validate that we successfully connected and upgraded to Web Sockets
Michelle Mandel1f87a562016-07-15 17:11:33 -0400196 self._validate_websocket_upgrade()
197 # Validate the RFB Negotiation to determine if a valid VNC session
198 self._validate_rfb_negotiation()
199
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800200 @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400201 def test_novnc_bad_token(self):
zhuflc32ee7d2018-03-28 17:12:32 +0800202 if self.use_get_remote_console:
203 body = self.client.get_remote_console(
204 self.server['id'], console_type='novnc',
205 protocol='vnc')['remote_console']
206 else:
207 body = self.client.get_vnc_console(self.server['id'],
208 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400209 self.assertEqual('novnc', body['type'])
210 # Do the WebSockify HTTP Request to novncproxy with a bad token
melanie witt27ba9332019-04-26 02:33:20 +0000211 parts = urlparse.urlparse(body['url'])
212 qparams = urlparse.parse_qs(parts.query)
213 if 'path' in qparams:
214 qparams['path'] = urlparse.unquote(qparams['path'][0]).replace(
215 'token=', 'token=bad')
216 elif 'token' in qparams:
217 qparams['token'] = 'bad' + qparams['token'][0]
218 new_query = urlparse.urlencode(qparams)
219 new_parts = urlparse.ParseResult(parts.scheme, parts.netloc,
220 parts.path, parts.params, new_query,
221 parts.fragment)
222 url = urlparse.urlunparse(new_parts)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100223 self._websocket = compute.create_websocket(url)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400224 # Make sure the novncproxy rejected the connection and closed it
225 data = self._websocket.receive_frame()
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900226 self.assertTrue(data is None or not data,
Michelle Mandel1f87a562016-07-15 17:11:33 -0400227 "The novnc proxy actually sent us some data, but we "
228 "expected it to close the connection.")