blob: 73361d79d22e1ebe4af6786a1d4fe37136dffb8d [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
25from tempest import test
26
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
90 # and greater than or equal to 3.3
91 version = float("%d.%d" % (int(data[4:7], base=10),
92 int(data[8:11], base=10)))
93 self.assertTrue(version >= 3.3, 'Bad RFB Version: ' + str(version))
94 # Send our RFB version to the server, which we will just go with 3.3
Matthew Treinish90e2a6d2017-02-06 19:56:43 -050095 self._websocket.send_frame(data)
Michelle Mandel1f87a562016-07-15 17:11:33 -040096 # Get the sever authentication type and make sure None is supported
97 data = self._websocket.receive_frame()
98 self.assertIsNotNone(data, 'Expected authentication type None.')
99 self.assertGreaterEqual(
100 len(data), 2, 'Expected authentication type None.')
101 self.assertIn(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500102 1, [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
Michelle Mandel1f87a562016-07-15 17:11:33 -0400103 'Expected authentication type None.')
104 # Send to the server that we only support authentication type None
105 self._websocket.send_frame(six.int2byte(1))
106 # The server should send 4 bytes of 0's if security handshake succeeded
107 data = self._websocket.receive_frame()
108 self.assertEqual(
109 len(data), 4, 'Server did not think security was successful.')
110 self.assertEqual(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500111 [ord_func(i) for i in data], [0, 0, 0, 0],
Michelle Mandel1f87a562016-07-15 17:11:33 -0400112 'Server did not think security was successful.')
113 # Say to leave the desktop as shared as part of client initialization
114 self._websocket.send_frame(six.int2byte(1))
115 # Get the server initialization packet back and make sure it is the
116 # right structure where bytes 20-24 is the name length and
117 # 24-N is the name
118 data = self._websocket.receive_frame()
119 data_length = len(data) if data is not None else 0
120 self.assertFalse(data_length <= 24 or
121 data_length != (struct.unpack(">L",
122 data[20:24])[0] + 24),
123 'Server initialization was not the right format.')
124 # Since the rest of the data on the screen is arbitrary, we will
125 # close the socket and end our validation of the data at this point
126 # Assert that the latest check was false, meaning that the server
127 # initialization was the right format
128 self.assertFalse(data_length <= 24 or
129 data_length != (struct.unpack(">L",
130 data[20:24])[0] + 24))
131
132 def _validate_websocket_upgrade(self):
133 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500134 self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
135 b'Protocols\r\n'),
Michelle Mandel1f87a562016-07-15 17:11:33 -0400136 'Did not get the expected 101 on the websockify call: '
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500137 + six.text_type(self._websocket.response))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400138 self.assertTrue(
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500139 self._websocket.response.find(b'Server: WebSockify') > 0,
Michelle Mandel1f87a562016-07-15 17:11:33 -0400140 'Did not get the expected WebSocket HTTP Response.')
141
142 def _create_websocket(self, url):
143 url = urlparse.urlparse(url)
144 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
145 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
146 client_socket.connect((url.hostname, url.port))
147 # Turn the Socket into a WebSocket to do the communication
148 return _WebSocket(client_socket, url)
149
150 @test.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
151 def test_novnc(self):
152 body = self.client.get_vnc_console(self.server['id'],
153 type='novnc')['console']
154 self.assertEqual('novnc', body['type'])
155 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
156 self._validate_novnc_html(body['url'])
157 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
158 self._websocket = self._create_websocket(body['url'])
159 # Validate that we succesfully connected and upgraded to Web Sockets
160 self._validate_websocket_upgrade()
161 # Validate the RFB Negotiation to determine if a valid VNC session
162 self._validate_rfb_negotiation()
163
164 @test.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
165 def test_novnc_bad_token(self):
166 body = self.client.get_vnc_console(self.server['id'],
167 type='novnc')['console']
168 self.assertEqual('novnc', body['type'])
169 # Do the WebSockify HTTP Request to novncproxy with a bad token
170 url = body['url'].replace('token=', 'token=bad')
171 self._websocket = self._create_websocket(url)
172 # Make sure the novncproxy rejected the connection and closed it
173 data = self._websocket.receive_frame()
174 self.assertTrue(data is None or len(data) == 0,
175 "The novnc proxy actually sent us some data, but we "
176 "expected it to close the connection.")
177
178
179class _WebSocket(object):
180 def __init__(self, client_socket, url):
181 """Contructor for the WebSocket wrapper to the socket."""
182 self._socket = client_socket
183 # Upgrade the HTTP connection to a WebSocket
184 self._upgrade(url)
185
186 def receive_frame(self):
187 """Wrapper for receiving data to parse the WebSocket frame format"""
188 # We need to loop until we either get some bytes back in the frame
189 # or no data was received (meaning the socket was closed). This is
190 # done to handle the case where we get back some empty frames
191 while True:
192 header = self._socket.recv(2)
193 # If we didn't receive any data, just return None
194 if len(header) == 0:
195 return None
196 # We will make the assumption that we are only dealing with
197 # frames less than 125 bytes here (for the negotiation) and
198 # that only the 2nd byte contains the length, and since the
199 # server doesn't do masking, we can just read the data length
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500200 if ord_func(header[1]) & 127 > 0:
201 return self._socket.recv(ord_func(header[1]) & 127)
Michelle Mandel1f87a562016-07-15 17:11:33 -0400202
203 def send_frame(self, data):
204 """Wrapper for sending data to add in the WebSocket frame format."""
205 frame_bytes = list()
206 # For the first byte, want to say we are sending binary data (130)
207 frame_bytes.append(130)
208 # Only sending negotiation data so don't need to worry about > 125
209 # We do need to add the bit that says we are masking the data
210 frame_bytes.append(len(data) | 128)
211 # We don't really care about providing a random mask for security
212 # So we will just hard-code a value since a test program
213 mask = [7, 2, 1, 9]
214 for i in range(len(mask)):
215 frame_bytes.append(mask[i])
216 # Mask each of the actual data bytes that we are going to send
217 for i in range(len(data)):
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500218 frame_bytes.append(ord_func(data[i]) ^ mask[i % 4])
Michelle Mandel1f87a562016-07-15 17:11:33 -0400219 # Convert our integer list to a binary array of bytes
220 frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
221 self._socket.sendall(frame_bytes)
222
223 def close(self):
224 """Helper method to close the connection."""
225 # Close down the real socket connection and exit the test program
226 if self._socket is not None:
227 self._socket.shutdown(1)
228 self._socket.close()
229 self._socket = None
230
231 def _upgrade(self, url):
232 """Upgrade the HTTP connection to a WebSocket and verify."""
233 # The real request goes to the /websockify URI always
234 reqdata = 'GET /websockify HTTP/1.1\r\n'
235 reqdata += 'Host: %s:%s\r\n' % (url.hostname, url.port)
236 # Tell the HTTP Server to Upgrade the connection to a WebSocket
237 reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
238 # The token=xxx is sent as a Cookie not in the URI
239 reqdata += 'Cookie: %s\r\n' % url.query
240 # Use a hard-coded WebSocket key since a test program
241 reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
242 reqdata += 'Sec-WebSocket-Version: 13\r\n'
243 # We are choosing to use binary even though browser may do Base64
244 reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
245 # Send the HTTP GET request and get the response back
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500246 self._socket.sendall(reqdata.encode('utf8'))
Michelle Mandel1f87a562016-07-15 17:11:33 -0400247 self.response = data = self._socket.recv(4096)
248 # Loop through & concatenate all of the data in the response body
Matthew Treinish90e2a6d2017-02-06 19:56:43 -0500249 while len(data) > 0 and self.response.find(b'\r\n\r\n') < 0:
Michelle Mandel1f87a562016-07-15 17:11:33 -0400250 data = self._socket.recv(4096)
251 self.response += data