blob: a72df5ef455f94c1f0f4a58ba2721c3cb35f8ad7 [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
16import socket
17import struct
18
19import six
20from six.moves.urllib import parse as urlparse
21import urllib3
22
23from tempest.api.compute import base
24from 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):
48 self.server_check_teardown()
49 super(NoVNCConsoleTestJSON, self).tearDown()
50 if self._websocket is not None:
51 self._websocket.close()
52
53 @classmethod
54 def setup_clients(cls):
55 super(NoVNCConsoleTestJSON, cls).setup_clients()
56 cls.client = cls.servers_client
57
58 @classmethod
59 def resource_setup(cls):
60 super(NoVNCConsoleTestJSON, cls).resource_setup()
61 cls.server = cls.create_test_server(wait_until="ACTIVE")
62
63 def _validate_novnc_html(self, vnc_url):
64 """Verify we can connect to novnc and get back the javascript."""
65 resp = urllib3.PoolManager().request('GET', vnc_url)
66 # Make sure that the GET request was accepted by the novncproxy
67 self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050068 'initial call: ' + six.text_type(resp.status))
Michelle Mandel1f87a562016-07-15 17:11:33 -040069 # Do some basic validation to make sure it is an expected HTML document
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050070 resp_data = resp.data.decode()
71 self.assertIn('<html>', resp_data,
72 'Not a valid html document in the response.')
73 self.assertIn('</html>', resp_data,
74 'Not a valid html document in the response.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040075 # Just try to make sure we got JavaScript back for noVNC, since we
76 # won't actually use it since not inside of a browser
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050077 self.assertIn('noVNC', resp_data,
78 'Not a valid noVNC javascript html document.')
79 self.assertIn('<script', resp_data,
80 'Not a valid noVNC javascript html document.')
Michelle Mandel1f87a562016-07-15 17:11:33 -040081
82 def _validate_rfb_negotiation(self):
83 """Verify we can connect to novnc and do the websocket connection."""
84 # Turn the Socket into a WebSocket to do the communication
85 data = self._websocket.receive_frame()
86 self.assertFalse(data is None or len(data) == 0,
87 'Token must be invalid because the connection '
88 'closed.')
89 # Parse the RFB version from the data to make sure it is valid
jianghua44a0f392017-03-13 04:14:26 +000090 # and belong to the known supported RFB versions.
Michelle Mandel1f87a562016-07-15 17:11:33 -040091 version = float("%d.%d" % (int(data[4:7], base=10),
92 int(data[8:11], base=10)))
jianghua44a0f392017-03-13 04:14:26 +000093 # Add the max RFB versions supported
94 supported_versions = [3.3, 3.8]
95 self.assertIn(version, supported_versions,
96 'Bad RFB Version: ' + str(version))
97 # Send our RFB version to the server
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050098 self._websocket.send_frame(data)
Michelle Mandel1f87a562016-07-15 17:11:33 -040099 # Get the sever authentication type and make sure None is supported
100 data = self._websocket.receive_frame()
101 self.assertIsNotNone(data, 'Expected authentication type None.')
jianghua44a0f392017-03-13 04:14:26 +0000102 data_length = len(data)
103 if version == 3.3:
104 # For RFB 3.3: in the security handshake, rather than a two-way
105 # negotiation, the server decides the security type and sends a
106 # single word(4 bytes).
107 self.assertEqual(
108 data_length, 4, 'Expected authentication type None.')
109 self.assertIn(1, [ord_func(data[i]) for i in (0, 3)],
110 'Expected authentication type None.')
111 else:
112 self.assertGreaterEqual(
113 len(data), 2, 'Expected authentication type None.')
114 self.assertIn(
115 1,
116 [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
117 'Expected authentication type None.')
118 # Send to the server that we only support authentication
119 # type None
120 self._websocket.send_frame(six.int2byte(1))
121
122 # The server should send 4 bytes of 0's if security
123 # handshake succeeded
124 data = self._websocket.receive_frame()
125 self.assertEqual(
126 len(data), 4,
127 'Server did not think security was successful.')
128 self.assertEqual(
129 [ord_func(i) for i in data], [0, 0, 0, 0],
130 'Server did not think security was successful.')
131
Michelle Mandel1f87a562016-07-15 17:11:33 -0400132 # Say to leave the desktop as shared as part of client initialization
133 self._websocket.send_frame(six.int2byte(1))
134 # Get the server initialization packet back and make sure it is the
135 # right structure where bytes 20-24 is the name length and
136 # 24-N is the name
137 data = self._websocket.receive_frame()
138 data_length = len(data) if data is not None else 0
139 self.assertFalse(data_length <= 24 or
140 data_length != (struct.unpack(">L",
141 data[20:24])[0] + 24),
142 'Server initialization was not the right format.')
143 # Since the rest of the data on the screen is arbitrary, we will
144 # close the socket and end our validation of the data at this point
145 # Assert that the latest check was false, meaning that the server
146 # initialization was the right format
147 self.assertFalse(data_length <= 24 or
148 data_length != (struct.unpack(">L",
149 data[20:24])[0] + 24))
150
151 def _validate_websocket_upgrade(self):
152 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500153 self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
154 b'Protocols\r\n'),
Michelle Mandel1f87a562016-07-15 17:11:33 -0400155 'Did not get the expected 101 on the websockify call: '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500156 + six.text_type(self._websocket.response))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400157 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500158 self._websocket.response.find(b'Server: WebSockify') > 0,
Michelle Mandel1f87a562016-07-15 17:11:33 -0400159 'Did not get the expected WebSocket HTTP Response.')
160
161 def _create_websocket(self, url):
162 url = urlparse.urlparse(url)
163 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
165 client_socket.connect((url.hostname, url.port))
166 # Turn the Socket into a WebSocket to do the communication
167 return _WebSocket(client_socket, url)
168
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800169 @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400170 def test_novnc(self):
171 body = self.client.get_vnc_console(self.server['id'],
172 type='novnc')['console']
173 self.assertEqual('novnc', body['type'])
174 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
175 self._validate_novnc_html(body['url'])
176 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
177 self._websocket = self._create_websocket(body['url'])
178 # Validate that we succesfully connected and upgraded to Web Sockets
179 self._validate_websocket_upgrade()
180 # Validate the RFB Negotiation to determine if a valid VNC session
181 self._validate_rfb_negotiation()
182
Ken'ichi Ohmichi44f01272017-01-27 18:44:14 -0800183 @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
Michelle Mandel1f87a562016-07-15 17:11:33 -0400184 def test_novnc_bad_token(self):
185 body = self.client.get_vnc_console(self.server['id'],
186 type='novnc')['console']
187 self.assertEqual('novnc', body['type'])
188 # Do the WebSockify HTTP Request to novncproxy with a bad token
189 url = body['url'].replace('token=', 'token=bad')
190 self._websocket = self._create_websocket(url)
191 # Make sure the novncproxy rejected the connection and closed it
192 data = self._websocket.receive_frame()
193 self.assertTrue(data is None or len(data) == 0,
194 "The novnc proxy actually sent us some data, but we "
195 "expected it to close the connection.")
196
197
198class _WebSocket(object):
199 def __init__(self, client_socket, url):
200 """Contructor for the WebSocket wrapper to the socket."""
201 self._socket = client_socket
202 # Upgrade the HTTP connection to a WebSocket
203 self._upgrade(url)
204
205 def receive_frame(self):
206 """Wrapper for receiving data to parse the WebSocket frame format"""
207 # We need to loop until we either get some bytes back in the frame
208 # or no data was received (meaning the socket was closed). This is
209 # done to handle the case where we get back some empty frames
210 while True:
211 header = self._socket.recv(2)
212 # If we didn't receive any data, just return None
213 if len(header) == 0:
214 return None
215 # We will make the assumption that we are only dealing with
216 # frames less than 125 bytes here (for the negotiation) and
217 # that only the 2nd byte contains the length, and since the
218 # server doesn't do masking, we can just read the data length
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500219 if ord_func(header[1]) & 127 > 0:
220 return self._socket.recv(ord_func(header[1]) & 127)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400221
222 def send_frame(self, data):
223 """Wrapper for sending data to add in the WebSocket frame format."""
224 frame_bytes = list()
225 # For the first byte, want to say we are sending binary data (130)
226 frame_bytes.append(130)
227 # Only sending negotiation data so don't need to worry about > 125
228 # We do need to add the bit that says we are masking the data
229 frame_bytes.append(len(data) | 128)
230 # We don't really care about providing a random mask for security
231 # So we will just hard-code a value since a test program
232 mask = [7, 2, 1, 9]
233 for i in range(len(mask)):
234 frame_bytes.append(mask[i])
235 # Mask each of the actual data bytes that we are going to send
236 for i in range(len(data)):
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500237 frame_bytes.append(ord_func(data[i]) ^ mask[i % 4])
Michelle Mandel1f87a562016-07-15 17:11:33 -0400238 # Convert our integer list to a binary array of bytes
239 frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
240 self._socket.sendall(frame_bytes)
241
242 def close(self):
243 """Helper method to close the connection."""
244 # Close down the real socket connection and exit the test program
245 if self._socket is not None:
246 self._socket.shutdown(1)
247 self._socket.close()
248 self._socket = None
249
250 def _upgrade(self, url):
251 """Upgrade the HTTP connection to a WebSocket and verify."""
252 # The real request goes to the /websockify URI always
253 reqdata = 'GET /websockify HTTP/1.1\r\n'
254 reqdata += 'Host: %s:%s\r\n' % (url.hostname, url.port)
255 # Tell the HTTP Server to Upgrade the connection to a WebSocket
256 reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
257 # The token=xxx is sent as a Cookie not in the URI
258 reqdata += 'Cookie: %s\r\n' % url.query
259 # Use a hard-coded WebSocket key since a test program
260 reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
261 reqdata += 'Sec-WebSocket-Version: 13\r\n'
262 # We are choosing to use binary even though browser may do Base64
263 reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
264 # Send the HTTP GET request and get the response back
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500265 self._socket.sendall(reqdata.encode('utf8'))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400266 self.response = data = self._socket.recv(4096)
267 # Loop through & concatenate all of the data in the response body
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500268 while len(data) > 0 and self.response.find(b'\r\n\r\n') < 0:
Michelle Mandel1f87a562016-07-15 17:11:33 -0400269 data = self._socket.recv(4096)
270 self.response += data