blob: d10f370c23dfb459d9f25002307c277416fd57fc [file] [log] [blame]
Michelle Mandel1f87a562016-07-15 17:11:33 -04001# Copyright 2016 OpenStack Foundation
2# 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
29
30class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
31
32 @classmethod
33 def skip_checks(cls):
34 super(NoVNCConsoleTestJSON, cls).skip_checks()
35 if not CONF.compute_feature_enabled.vnc_console:
36 raise cls.skipException('VNC Console feature is disabled.')
37
38 def setUp(self):
39 super(NoVNCConsoleTestJSON, self).setUp()
40 self._websocket = None
41
42 def tearDown(self):
43 self.server_check_teardown()
44 super(NoVNCConsoleTestJSON, self).tearDown()
45 if self._websocket is not None:
46 self._websocket.close()
47
48 @classmethod
49 def setup_clients(cls):
50 super(NoVNCConsoleTestJSON, cls).setup_clients()
51 cls.client = cls.servers_client
52
53 @classmethod
54 def resource_setup(cls):
55 super(NoVNCConsoleTestJSON, cls).resource_setup()
56 cls.server = cls.create_test_server(wait_until="ACTIVE")
57
58 def _validate_novnc_html(self, vnc_url):
59 """Verify we can connect to novnc and get back the javascript."""
60 resp = urllib3.PoolManager().request('GET', vnc_url)
61 # Make sure that the GET request was accepted by the novncproxy
62 self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
63 'initial call: ' + str(resp.status))
64 # Do some basic validation to make sure it is an expected HTML document
65 self.assertTrue('<html>' in resp.data and '</html>' in resp.data,
66 'Not a valid html document in the response.')
67 # Just try to make sure we got JavaScript back for noVNC, since we
68 # won't actually use it since not inside of a browser
69 self.assertTrue('noVNC' in resp.data and '<script' in resp.data,
70 'Not a valid noVNC javascript html document.')
71
72 def _validate_rfb_negotiation(self):
73 """Verify we can connect to novnc and do the websocket connection."""
74 # Turn the Socket into a WebSocket to do the communication
75 data = self._websocket.receive_frame()
76 self.assertFalse(data is None or len(data) == 0,
77 'Token must be invalid because the connection '
78 'closed.')
79 # Parse the RFB version from the data to make sure it is valid
80 # and greater than or equal to 3.3
81 version = float("%d.%d" % (int(data[4:7], base=10),
82 int(data[8:11], base=10)))
83 self.assertTrue(version >= 3.3, 'Bad RFB Version: ' + str(version))
84 # Send our RFB version to the server, which we will just go with 3.3
85 self._websocket.send_frame(str(data))
86 # Get the sever authentication type and make sure None is supported
87 data = self._websocket.receive_frame()
88 self.assertIsNotNone(data, 'Expected authentication type None.')
89 self.assertGreaterEqual(
90 len(data), 2, 'Expected authentication type None.')
91 self.assertIn(
92 1, [ord(data[i + 1]) for i in range(ord(data[0]))],
93 'Expected authentication type None.')
94 # Send to the server that we only support authentication type None
95 self._websocket.send_frame(six.int2byte(1))
96 # The server should send 4 bytes of 0's if security handshake succeeded
97 data = self._websocket.receive_frame()
98 self.assertEqual(
99 len(data), 4, 'Server did not think security was successful.')
100 self.assertEqual(
101 [ord(i) for i in data], [0, 0, 0, 0],
102 'Server did not think security was successful.')
103 # Say to leave the desktop as shared as part of client initialization
104 self._websocket.send_frame(six.int2byte(1))
105 # Get the server initialization packet back and make sure it is the
106 # right structure where bytes 20-24 is the name length and
107 # 24-N is the name
108 data = self._websocket.receive_frame()
109 data_length = len(data) if data is not None else 0
110 self.assertFalse(data_length <= 24 or
111 data_length != (struct.unpack(">L",
112 data[20:24])[0] + 24),
113 'Server initialization was not the right format.')
114 # Since the rest of the data on the screen is arbitrary, we will
115 # close the socket and end our validation of the data at this point
116 # Assert that the latest check was false, meaning that the server
117 # initialization was the right format
118 self.assertFalse(data_length <= 24 or
119 data_length != (struct.unpack(">L",
120 data[20:24])[0] + 24))
121
122 def _validate_websocket_upgrade(self):
123 self.assertTrue(
124 self._websocket.response.startswith('HTTP/1.1 101 Switching '
125 'Protocols\r\n'),
126 'Did not get the expected 101 on the websockify call: '
127 + str(len(self._websocket.response)))
128 self.assertTrue(
129 self._websocket.response.find('Server: WebSockify') > 0,
130 'Did not get the expected WebSocket HTTP Response.')
131
132 def _create_websocket(self, url):
133 url = urlparse.urlparse(url)
134 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
135 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
136 client_socket.connect((url.hostname, url.port))
137 # Turn the Socket into a WebSocket to do the communication
138 return _WebSocket(client_socket, url)
139
140 @test.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
141 def test_novnc(self):
142 body = self.client.get_vnc_console(self.server['id'],
143 type='novnc')['console']
144 self.assertEqual('novnc', body['type'])
145 # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
146 self._validate_novnc_html(body['url'])
147 # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
148 self._websocket = self._create_websocket(body['url'])
149 # Validate that we succesfully connected and upgraded to Web Sockets
150 self._validate_websocket_upgrade()
151 # Validate the RFB Negotiation to determine if a valid VNC session
152 self._validate_rfb_negotiation()
153
154 @test.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
155 def test_novnc_bad_token(self):
156 body = self.client.get_vnc_console(self.server['id'],
157 type='novnc')['console']
158 self.assertEqual('novnc', body['type'])
159 # Do the WebSockify HTTP Request to novncproxy with a bad token
160 url = body['url'].replace('token=', 'token=bad')
161 self._websocket = self._create_websocket(url)
162 # Make sure the novncproxy rejected the connection and closed it
163 data = self._websocket.receive_frame()
164 self.assertTrue(data is None or len(data) == 0,
165 "The novnc proxy actually sent us some data, but we "
166 "expected it to close the connection.")
167
168
169class _WebSocket(object):
170 def __init__(self, client_socket, url):
171 """Contructor for the WebSocket wrapper to the socket."""
172 self._socket = client_socket
173 # Upgrade the HTTP connection to a WebSocket
174 self._upgrade(url)
175
176 def receive_frame(self):
177 """Wrapper for receiving data to parse the WebSocket frame format"""
178 # We need to loop until we either get some bytes back in the frame
179 # or no data was received (meaning the socket was closed). This is
180 # done to handle the case where we get back some empty frames
181 while True:
182 header = self._socket.recv(2)
183 # If we didn't receive any data, just return None
184 if len(header) == 0:
185 return None
186 # We will make the assumption that we are only dealing with
187 # frames less than 125 bytes here (for the negotiation) and
188 # that only the 2nd byte contains the length, and since the
189 # server doesn't do masking, we can just read the data length
190 if ord(header[1]) & 127 > 0:
191 return self._socket.recv(ord(header[1]) & 127)
192
193 def send_frame(self, data):
194 """Wrapper for sending data to add in the WebSocket frame format."""
195 frame_bytes = list()
196 # For the first byte, want to say we are sending binary data (130)
197 frame_bytes.append(130)
198 # Only sending negotiation data so don't need to worry about > 125
199 # We do need to add the bit that says we are masking the data
200 frame_bytes.append(len(data) | 128)
201 # We don't really care about providing a random mask for security
202 # So we will just hard-code a value since a test program
203 mask = [7, 2, 1, 9]
204 for i in range(len(mask)):
205 frame_bytes.append(mask[i])
206 # Mask each of the actual data bytes that we are going to send
207 for i in range(len(data)):
208 frame_bytes.append(ord(data[i]) ^ mask[i % 4])
209 # Convert our integer list to a binary array of bytes
210 frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
211 self._socket.sendall(frame_bytes)
212
213 def close(self):
214 """Helper method to close the connection."""
215 # Close down the real socket connection and exit the test program
216 if self._socket is not None:
217 self._socket.shutdown(1)
218 self._socket.close()
219 self._socket = None
220
221 def _upgrade(self, url):
222 """Upgrade the HTTP connection to a WebSocket and verify."""
223 # The real request goes to the /websockify URI always
224 reqdata = 'GET /websockify HTTP/1.1\r\n'
225 reqdata += 'Host: %s:%s\r\n' % (url.hostname, url.port)
226 # Tell the HTTP Server to Upgrade the connection to a WebSocket
227 reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
228 # The token=xxx is sent as a Cookie not in the URI
229 reqdata += 'Cookie: %s\r\n' % url.query
230 # Use a hard-coded WebSocket key since a test program
231 reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
232 reqdata += 'Sec-WebSocket-Version: 13\r\n'
233 # We are choosing to use binary even though browser may do Base64
234 reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
235 # Send the HTTP GET request and get the response back
236 self._socket.sendall(reqdata)
237 self.response = data = self._socket.recv(4096)
238 # Loop through & concatenate all of the data in the response body
239 while len(data) > 0 and self.response.find('\r\n\r\n') < 0:
240 data = self._socket.recv(4096)
241 self.response += data