blob: daf6a06900f66af66e8207cb557bf76811df30f7 [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
Michelle Mandel1f87a562016-07-15 17:11:33 -040019import urllib3
20
21from tempest.api.compute import base
Markus Zoellerae36ce82017-03-20 16:27:26 +010022from tempest.common import compute
Michelle Mandel1f87a562016-07-15 17:11:33 -040023from tempest import config
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -080024from tempest.lib import decorators
Michelle Mandel1f87a562016-07-15 17:11:33 -040025
26CONF = config.CONF
27
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050028if six.PY2:
29 ord_func = ord
30else:
31 ord_func = int
32
Michelle Mandel1f87a562016-07-15 17:11:33 -040033
34class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
35
36 @classmethod
37 def skip_checks(cls):
38 super(NoVNCConsoleTestJSON, cls).skip_checks()
39 if not CONF.compute_feature_enabled.vnc_console:
40 raise cls.skipException('VNC Console feature is disabled.')
41
42 def setUp(self):
43 super(NoVNCConsoleTestJSON, self).setUp()
44 self._websocket = None
45
46 def tearDown(self):
Michelle Mandel1f87a562016-07-15 17:11:33 -040047 super(NoVNCConsoleTestJSON, self).tearDown()
48 if self._websocket is not None:
49 self._websocket.close()
zhufle913e462018-07-25 17:16:10 +080050 # NOTE(zhufl): Because server_check_teardown will raise Exception
51 # which will prevent other cleanup steps from being executed, so
52 # server_check_teardown should be called after super's tearDown.
53 self.server_check_teardown()
Michelle Mandel1f87a562016-07-15 17:11:33 -040054
55 @classmethod
56 def setup_clients(cls):
57 super(NoVNCConsoleTestJSON, cls).setup_clients()
58 cls.client = cls.servers_client
59
60 @classmethod
61 def resource_setup(cls):
62 super(NoVNCConsoleTestJSON, cls).resource_setup()
63 cls.server = cls.create_test_server(wait_until="ACTIVE")
zhuflc32ee7d2018-03-28 17:12:32 +080064 cls.use_get_remote_console = False
65 if not cls.is_requested_microversion_compatible('2.5'):
66 cls.use_get_remote_console = True
Michelle Mandel1f87a562016-07-15 17:11:33 -040067
68 def _validate_novnc_html(self, vnc_url):
69 """Verify we can connect to novnc and get back the javascript."""
70 resp = urllib3.PoolManager().request('GET', vnc_url)
71 # Make sure that the GET request was accepted by the novncproxy
72 self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050073 'initial call: ' + six.text_type(resp.status))
Michelle Mandel1f87a562016-07-15 17:11:33 -040074 # Do some basic validation to make sure it is an expected HTML document
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050075 resp_data = resp.data.decode()
76 self.assertIn('<html>', resp_data,
77 'Not a valid html document in the response.')
78 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.')
114 self.assertIn(1, [ord_func(data[i]) for i in (0, 3)],
115 'Expected authentication type None.')
116 else:
117 self.assertGreaterEqual(
118 len(data), 2, 'Expected authentication type None.')
119 self.assertIn(
120 1,
121 [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
122 '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(
134 [ord_func(i) for i in data], [0, 0, 0, 0],
135 '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):
157 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500158 self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
159 b'Protocols\r\n'),
Alex Savatieiev82b6aeb2018-03-28 17:56:49 +0200160 'Did not get the expected 101 on the {} call: {}'.format(
161 CONF.compute_feature_enabled.vnc_server_header,
162 six.text_type(self._websocket.response)
163 )
164 )
165 # Since every other server type returns Headers with different case
166 # (for example 'nginx'), lowercase must be applied to eliminate issues.
167 _desired_header = "server: {0}".format(
168 CONF.compute_feature_enabled.vnc_server_header
169 ).lower()
170 _response = six.text_type(self._websocket.response).lower()
171 self.assertIn(
172 _desired_header,
173 _response,
174 'Did not get the expected WebSocket HTTP Response.'
175 )
Michelle Mandel1f87a562016-07-15 17:11:33 -0400176
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800177 @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400178 def test_novnc(self):
zhuflc32ee7d2018-03-28 17:12:32 +0800179 if self.use_get_remote_console:
180 body = self.client.get_remote_console(
181 self.server['id'], console_type='novnc',
182 protocol='vnc')['remote_console']
183 else:
184 body = self.client.get_vnc_console(self.server['id'],
185 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400186 self.assertEqual('novnc', body['type'])
187 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
188 self._validate_novnc_html(body['url'])
189 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
Markus Zoellerae36ce82017-03-20 16:27:26 +0100190 self._websocket = compute.create_websocket(body['url'])
shangxiaobj284d3112017-08-13 23:37:34 -0700191 # Validate that we successfully connected and upgraded to Web Sockets
Michelle Mandel1f87a562016-07-15 17:11:33 -0400192 self._validate_websocket_upgrade()
193 # Validate the RFB Negotiation to determine if a valid VNC session
194 self._validate_rfb_negotiation()
195
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800196 @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400197 def test_novnc_bad_token(self):
zhuflc32ee7d2018-03-28 17:12:32 +0800198 if self.use_get_remote_console:
199 body = self.client.get_remote_console(
200 self.server['id'], console_type='novnc',
201 protocol='vnc')['remote_console']
202 else:
203 body = self.client.get_vnc_console(self.server['id'],
204 type='novnc')['console']
Michelle Mandel1f87a562016-07-15 17:11:33 -0400205 self.assertEqual('novnc', body['type'])
206 # Do the WebSockify HTTP Request to novncproxy with a bad token
207 url = body['url'].replace('token=', 'token=bad')
Markus Zoellerae36ce82017-03-20 16:27:26 +0100208 self._websocket = compute.create_websocket(url)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400209 # Make sure the novncproxy rejected the connection and closed it
210 data = self._websocket.receive_frame()
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900211 self.assertTrue(data is None or not data,
Michelle Mandel1f87a562016-07-15 17:11:33 -0400212 "The novnc proxy actually sent us some data, but we "
213 "expected it to close the connection.")