blob: a8aafe93ae476b42fe2be4c7ac33b7998f7f592d [file] [log] [blame]
Joseph Lanouxb3e1f872015-01-30 11:13:07 +00001# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain 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,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
Clark Boylan844180e2017-03-15 15:24:58 -070016import base64
Markus Zoellerae36ce82017-03-20 16:27:26 +010017import socket
Martin Kopecdc9a93b2022-03-25 17:34:42 +010018from ssl import SSLContext as sslc
Markus Zoellerae36ce82017-03-20 16:27:26 +010019import struct
Clark Boylan844180e2017-03-15 15:24:58 -070020import textwrap
songwenping99d6e002021-01-05 03:07:46 +000021from urllib import parse as urlparse
Markus Zoellerae36ce82017-03-20 16:27:26 +010022
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000023from oslo_log import log as logging
24from oslo_utils import excutils
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000025
Lee Yarwood0b4bc3d2021-11-11 17:45:25 +000026from tempest.common.utils.linux import remote_client
Ken'ichi Ohmichi0eb153c2015-07-13 02:18:25 +000027from tempest.common import waiters
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000028from tempest import config
Lee Yarwood20556df2021-11-12 09:38:18 +000029from tempest import exceptions
Matthew Treinishb19c55d2017-07-17 12:38:35 -040030from tempest.lib.common import fixed_network
Ken'ichi Ohmichi54030522016-03-02 11:01:34 -080031from tempest.lib.common import rest_client
Ken'ichi Ohmichi757833a2017-03-10 10:30:30 -080032from tempest.lib.common.utils import data_utils
Lee Yarwood20556df2021-11-12 09:38:18 +000033from tempest.lib import exceptions as lib_exc
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000034
35CONF = config.CONF
36
37LOG = logging.getLogger(__name__)
38
39
Andrea Frittoli88eb6772017-08-07 21:06:27 +010040def is_scheduler_filter_enabled(filter_name):
41 """Check the list of enabled compute scheduler filters from config.
42
Artom Lifshitz595ae162018-05-23 10:19:18 -040043 This function checks whether the given compute scheduler filter is enabled
44 in the nova config file. If the scheduler_enabled_filters option is set to
45 'all' in tempest.conf then, this function returns True with assumption that
46 requested filter 'filter_name' is one of the enabled filters in nova
47 ("nova.scheduler.filters.all_filters").
Andrea Frittoli88eb6772017-08-07 21:06:27 +010048 """
49
Artom Lifshitz595ae162018-05-23 10:19:18 -040050 filters = CONF.compute_feature_enabled.scheduler_enabled_filters
Andrea Frittoli88eb6772017-08-07 21:06:27 +010051 if not filters:
52 return False
53 if 'all' in filters:
54 return True
55 if filter_name in filters:
56 return True
57 return False
58
59
Lee Yarwood20556df2021-11-12 09:38:18 +000060def get_server_ip(server, validation_resources=None):
61 """Get the server fixed or floating IP.
62
63 Based on the configuration we're in, return a correct ip
64 address for validating that a guest is up.
65
66 :param server: The server dict as returned by the API
67 :param validation_resources: The dict of validation resources
68 provisioned for the server.
69 """
70 if CONF.validation.connect_method == 'floating':
71 if validation_resources:
72 return validation_resources['floating_ip']['ip']
73 else:
74 msg = ('When validation.connect_method equals floating, '
75 'validation_resources cannot be None')
76 raise lib_exc.InvalidParam(invalid_param=msg)
77 elif CONF.validation.connect_method == 'fixed':
78 addresses = server['addresses'][CONF.validation.network_for_ssh]
79 for address in addresses:
80 if address['version'] == CONF.validation.ip_version_for_ssh:
81 return address['addr']
82 raise exceptions.ServerUnreachable(server_id=server['id'])
83 else:
84 raise lib_exc.InvalidConfiguration()
85
86
Ghanshyam Mann8c6d0cf2022-03-17 13:14:49 -050087def _setup_validation_fip(
88 server, clients, tenant_network, validation_resources):
89 if CONF.service_available.neutron:
90 ifaces = clients.interfaces_client.list_interfaces(server['id'])
91 validation_port = None
92 for iface in ifaces['interfaceAttachments']:
93 if iface['net_id'] == tenant_network['id']:
94 validation_port = iface['port_id']
95 break
96 if not validation_port:
97 # NOTE(artom) This will get caught by the catch-all clause in
98 # the wait_until loop below
99 raise ValueError('Unable to setup floating IP for validation: '
100 'port not found on tenant network')
101 clients.floating_ips_client.update_floatingip(
102 validation_resources['floating_ip']['id'],
103 port_id=validation_port)
104 else:
105 fip_client = clients.compute_floating_ips_client
106 fip_client.associate_floating_ip_to_server(
107 floating_ip=validation_resources['floating_ip']['ip'],
108 server_id=server['id'])
109
110
111def wait_for_ssh_or_ping(server, clients, tenant_network,
112 validatable, validation_resources, wait_until,
113 set_floatingip):
114 """Wait for the server for SSH or Ping as requested.
115
116 :param server: The server dict as returned by the API
117 :param clients: Client manager which provides OpenStack Tempest clients.
118 :param tenant_network: Tenant network to be used for creating a server.
119 :param validatable: Whether the server will be pingable or sshable.
120 :param validation_resources: Resources created for the connection to the
121 server. Include a keypair, a security group and an IP.
122 :param wait_until: Server status to wait for the server to reach.
123 It can be PINGABLE and SSHABLE states when the server is both
124 validatable and has the required validation_resources provided.
125 :param set_floatingip: If FIP needs to be associated to server
126 """
127 if set_floatingip and CONF.validation.connect_method == 'floating':
128 _setup_validation_fip(
129 server, clients, tenant_network, validation_resources)
130
131 server_ip = get_server_ip(
132 server, validation_resources=validation_resources)
133 if wait_until == 'PINGABLE':
134 waiters.wait_for_ping(
135 server_ip,
136 clients.servers_client.build_timeout,
137 clients.servers_client.build_interval
138 )
139 if wait_until == 'SSHABLE':
140 pkey = validation_resources['keypair']['private_key']
141 ssh_client = remote_client.RemoteClient(
142 server_ip,
143 CONF.validation.image_ssh_user,
144 pkey=pkey,
145 server=server,
146 servers_client=clients.servers_client
147 )
148 waiters.wait_for_ssh(
149 ssh_client,
150 clients.servers_client.build_timeout
151 )
152
153
Andrea Frittoli (andreaf)476e9192015-08-14 23:59:58 +0100154def create_test_server(clients, validatable=False, validation_resources=None,
Joe Gordon8843f0f2015-03-17 15:07:34 -0700155 tenant_network=None, wait_until=None,
Anusha Ramineni9aaef8b2016-01-19 10:56:40 +0530156 volume_backed=False, name=None, flavor=None,
Lee Yarwood1a76c2c2021-11-11 15:28:53 +0000157 image_id=None, **kwargs):
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000158 """Common wrapper utility returning a test server.
159
160 This method is a common wrapper returning a test server that can be
161 pingable or sshable.
162
Takashi NATSUME6d5a2b42015-09-08 11:27:49 +0900163 :param clients: Client manager which provides OpenStack Tempest clients.
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000164 :param validatable: Whether the server will be pingable or sshable.
165 :param validation_resources: Resources created for the connection to the
Andrea Frittoli (andreaf)9df3a522016-07-06 14:09:48 +0100166 server. Include a keypair, a security group and an IP.
Ken'ichi Ohmichid5bc31a2015-09-02 01:45:28 +0000167 :param tenant_network: Tenant network to be used for creating a server.
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000168 :param wait_until: Server status to wait for the server to reach after
Lee Yarwood0b4bc3d2021-11-11 17:45:25 +0000169 its creation. Additionally PINGABLE and SSHABLE states are also
170 accepted when the server is both validatable and has the required
171 validation_resources provided.
ghanshyam61db96e2016-12-16 12:49:25 +0900172 :param volume_backed: Whether the server is volume backed or not.
Sergey Vilgelmeac094a2018-11-21 18:27:51 -0600173 If this is true, a volume will be created and create server will be
174 requested with 'block_device_mapping_v2' populated with below values:
175
176 .. code-block:: python
177
178 bd_map_v2 = [{
179 'uuid': volume['volume']['id'],
180 'source_type': 'volume',
181 'destination_type': 'volume',
182 'boot_index': 0,
183 'delete_on_termination': True}]
184 kwargs['block_device_mapping_v2'] = bd_map_v2
185
186 If server needs to be booted from volume with other combination of bdm
187 inputs than mentioned above, then pass the bdm inputs explicitly as
188 kwargs and image_id as empty string ('').
Andrea Frittoli (andreaf)9df3a522016-07-06 14:09:48 +0100189 :param name: Name of the server to be provisioned. If not defined a random
190 string ending with '-instance' will be generated.
191 :param flavor: Flavor of the server to be provisioned. If not defined,
192 CONF.compute.flavor_ref will be used instead.
193 :param image_id: ID of the image to be used to provision the server. If not
194 defined, CONF.compute.image_ref will be used instead.
lei zhangdd552b22015-11-25 20:41:48 +0800195 :returns: a tuple
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000196 """
197
Anusha Ramineni9aaef8b2016-01-19 10:56:40 +0530198 if name is None:
Martin Kopec213d0a42023-11-30 10:28:14 +0100199 name = data_utils.rand_name(
200 prefix=CONF.resource_name_prefix, name=__name__ + "-instance")
Anusha Ramineni9aaef8b2016-01-19 10:56:40 +0530201 if flavor is None:
202 flavor = CONF.compute.flavor_ref
203 if image_id is None:
204 image_id = CONF.compute.image_ref
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000205
206 kwargs = fixed_network.set_networks_kwarg(
207 tenant_network, kwargs) or {}
208
Ghanshyam4de44ae2015-12-25 10:34:00 +0900209 multiple_create_request = (max(kwargs.get('min_count', 0),
210 kwargs.get('max_count', 0)) > 1)
211
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000212 if CONF.validation.run_validation and validatable:
Andrea Frittoli9f416dd2017-08-10 15:38:00 +0100213 LOG.debug("Provisioning test server with validation resources %s",
214 validation_resources)
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000215 if 'security_groups' in kwargs:
216 kwargs['security_groups'].append(
217 {'name': validation_resources['security_group']['name']})
218 else:
219 try:
220 kwargs['security_groups'] = [
221 {'name': validation_resources['security_group']['name']}]
222 except KeyError:
223 LOG.debug("No security group provided.")
224
225 if 'key_name' not in kwargs:
226 try:
227 kwargs['key_name'] = validation_resources['keypair']['name']
228 except KeyError:
229 LOG.debug("No key provided.")
230
231 if CONF.validation.connect_method == 'floating':
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000232 if wait_until is None:
233 wait_until = 'ACTIVE'
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000234
Clark Boylan844180e2017-03-15 15:24:58 -0700235 if 'user_data' not in kwargs:
236 # If nothing overrides the default user data script then run
237 # a simple script on the host to print networking info. This is
238 # to aid in debugging ssh failures.
239 script = '''
240 #!/bin/sh
241 echo "Printing {user} user authorized keys"
242 cat ~{user}/.ssh/authorized_keys || true
243 '''.format(user=CONF.validation.image_ssh_user)
244 script_clean = textwrap.dedent(script).lstrip().encode('utf8')
245 script_b64 = base64.b64encode(script_clean)
246 kwargs['user_data'] = script_b64
247
Joe Gordon8843f0f2015-03-17 15:07:34 -0700248 if volume_backed:
Martin Kopec213d0a42023-11-30 10:28:14 +0100249 volume_name = data_utils.rand_name(
250 prefix=CONF.resource_name_prefix, name=__name__ + '-volume')
ghanshyam6c682ff2018-08-06 09:54:45 +0000251 volumes_client = clients.volumes_client_latest
ghanshyam3bd0d2b2017-03-23 01:57:28 +0000252 params = {'name': volume_name,
ghanshyam61db96e2016-12-16 12:49:25 +0900253 'imageRef': image_id,
254 'size': CONF.volume.volume_size}
Martin Kopec00e6d6c2019-06-05 14:30:06 +0000255 if CONF.compute.compute_volume_common_az:
256 params.setdefault('availability_zone',
257 CONF.compute.compute_volume_common_az)
ghanshyam61db96e2016-12-16 12:49:25 +0900258 volume = volumes_client.create_volume(**params)
mccasland, trevor (tm2086)0fedb412019-01-21 13:37:58 -0600259 try:
260 waiters.wait_for_volume_resource_status(volumes_client,
261 volume['volume']['id'],
262 'available')
263 except Exception:
264 with excutils.save_and_reraise_exception():
265 try:
266 volumes_client.delete_volume(volume['volume']['id'])
267 volumes_client.wait_for_resource_deletion(
268 volume['volume']['id'])
269 except Exception as exc:
270 LOG.exception("Deleting volume %s failed, exception %s",
271 volume['volume']['id'], exc)
Joe Gordon8843f0f2015-03-17 15:07:34 -0700272 bd_map_v2 = [{
273 'uuid': volume['volume']['id'],
274 'source_type': 'volume',
275 'destination_type': 'volume',
276 'boot_index': 0,
ghanshyam61db96e2016-12-16 12:49:25 +0900277 'delete_on_termination': True}]
Joe Gordon8843f0f2015-03-17 15:07:34 -0700278 kwargs['block_device_mapping_v2'] = bd_map_v2
279
280 # Since this is boot from volume an image does not need
281 # to be specified.
282 image_id = ''
283
Martin Kopec00e6d6c2019-06-05 14:30:06 +0000284 if CONF.compute.compute_volume_common_az:
285 kwargs.setdefault('availability_zone',
286 CONF.compute.compute_volume_common_az)
Ken'ichi Ohmichif2d436e2015-09-03 01:13:16 +0000287 body = clients.servers_client.create_server(name=name, imageRef=image_id,
288 flavorRef=flavor,
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000289 **kwargs)
Artom Lifshitzda48e4e2021-11-22 15:59:15 -0500290 request_id = body.response['x-openstack-request-id']
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000291
292 # handle the case of multiple servers
Ghanshyam4de44ae2015-12-25 10:34:00 +0900293 if multiple_create_request:
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000294 # Get servers created which name match with name param.
295 body_servers = clients.servers_client.list_servers()
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200296 created_servers = \
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000297 [s for s in body_servers['servers'] if s['name'].startswith(name)]
ghanshyam0f825252015-08-25 16:02:50 +0900298 else:
Ken'ichi Ohmichi54030522016-03-02 11:01:34 -0800299 body = rest_client.ResponseBody(body.response, body['server'])
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200300 created_servers = [body]
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000301
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000302 if wait_until:
Lee Yarwood0b4bc3d2021-11-11 17:45:25 +0000303
304 # NOTE(lyarwood): PINGABLE and SSHABLE both require the instance to
305 # go ACTIVE initially before we can setup the fip(s) etc so stash
306 # this additional wait state for later use.
307 wait_until_extra = None
308 if wait_until in ['PINGABLE', 'SSHABLE']:
Dan Smithe5da6752023-05-02 14:56:58 -0700309 if not validatable and validation_resources is None:
310 raise RuntimeError(
311 'SSHABLE/PINGABLE requires validatable=True '
312 'and validation_resources to be passed')
Lee Yarwood0b4bc3d2021-11-11 17:45:25 +0000313 wait_until_extra = wait_until
314 wait_until = 'ACTIVE'
315
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200316 servers = []
317 try:
318 # Wait for server to be in active state and populate servers list
319 # with those full server response so that we will have addresses
320 # field present in server which is needed to be used for wait for
321 # ssh
322 for server in created_servers:
323 server = waiters.wait_for_server_status(
Artom Lifshitzda48e4e2021-11-22 15:59:15 -0500324 clients.servers_client, server['id'], wait_until,
325 request_id=request_id)
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200326 servers.append(server)
327
328 for server in servers:
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000329 if CONF.validation.run_validation and validatable:
330 if CONF.validation.connect_method == 'floating':
Ghanshyam Mann8c6d0cf2022-03-17 13:14:49 -0500331 _setup_validation_fip(
332 server, clients, tenant_network,
333 validation_resources)
334 if wait_until_extra:
335 wait_for_ssh_or_ping(
336 server, clients, tenant_network,
337 validatable, validation_resources,
338 wait_until_extra, False)
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200339 except Exception:
340 with excutils.save_and_reraise_exception():
341 for server in created_servers:
342 try:
343 clients.servers_client.delete_server(
344 server['id'])
345 except Exception:
346 LOG.exception('Deleting server %s failed',
347 server['id'])
348 for server in created_servers:
349 # NOTE(artom) If the servers were booted with volumes
350 # and with delete_on_termination=False we need to wait
351 # for the servers to go away before proceeding with
352 # cleanup, otherwise we'll attempt to delete the
353 # volumes while they're still attached to servers that
354 # are in the process of being deleted.
355 try:
356 waiters.wait_for_server_termination(
357 clients.servers_client, server['id'])
358 except Exception:
359 LOG.exception('Server %s failed to delete in time',
360 server['id'])
Ilya Popov2c6da1d2022-11-13 23:30:57 +0300361 if servers and not multiple_create_request:
362 body = rest_client.ResponseBody(body.response, servers[0])
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200363 return body, servers
Lee Yarwood0b4bc3d2021-11-11 17:45:25 +0000364
Marian Krcmarikca5ddb42022-08-29 17:01:01 +0200365 return body, created_servers
ghanshyam017b5fe2016-04-15 18:49:26 +0900366
367
ghanshyam4c1391c2016-12-01 13:13:06 +0900368def shelve_server(servers_client, server_id, force_shelve_offload=False):
ghanshyam017b5fe2016-04-15 18:49:26 +0900369 """Common wrapper utility to shelve server.
370
371 This method is a common wrapper to make server in 'SHELVED'
372 or 'SHELVED_OFFLOADED' state.
373
ghanshyam4c1391c2016-12-01 13:13:06 +0900374 :param servers_clients: Compute servers client instance.
ghanshyam017b5fe2016-04-15 18:49:26 +0900375 :param server_id: Server to make in shelve state
376 :param force_shelve_offload: Forcefully offload shelve server if it
377 is configured not to offload server
378 automatically after offload time.
379 """
Artom Lifshitz3f233b22023-11-15 14:15:45 -0500380 body = servers_client.shelve_server(server_id)
381 request_id = body.response['x-openstack-request-id']
ghanshyam017b5fe2016-04-15 18:49:26 +0900382
383 offload_time = CONF.compute.shelved_offload_time
384 if offload_time >= 0:
ghanshyam4c1391c2016-12-01 13:13:06 +0900385 waiters.wait_for_server_status(servers_client, server_id,
ghanshyam017b5fe2016-04-15 18:49:26 +0900386 'SHELVED_OFFLOADED',
Artom Lifshitz3f233b22023-11-15 14:15:45 -0500387 extra_timeout=offload_time,
388 request_id=request_id)
ghanshyam017b5fe2016-04-15 18:49:26 +0900389 else:
ghanshyam4c1391c2016-12-01 13:13:06 +0900390 waiters.wait_for_server_status(servers_client, server_id, 'SHELVED')
ghanshyam017b5fe2016-04-15 18:49:26 +0900391 if force_shelve_offload:
ghanshyam4c1391c2016-12-01 13:13:06 +0900392 servers_client.shelve_offload_server(server_id)
393 waiters.wait_for_server_status(servers_client, server_id,
Artom Lifshitz3f233b22023-11-15 14:15:45 -0500394 'SHELVED_OFFLOADED',
395 request_id=request_id)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100396
397
398def create_websocket(url):
399 url = urlparse.urlparse(url)
Mohammed Naseraa5dd9a2017-12-29 18:52:01 -0500400
401 # NOTE(mnaser): It is possible that there is no port specified, so fall
402 # back to the default port based on the scheme.
403 port = url.port or (443 if url.scheme == 'https' else 80)
404
405 for res in socket.getaddrinfo(url.hostname, port,
Jens Harbott6bc422d2017-09-27 10:29:34 +0000406 socket.AF_UNSPEC, socket.SOCK_STREAM):
407 af, socktype, proto, _, sa = res
408 client_socket = socket.socket(af, socktype, proto)
409 if url.scheme == 'https':
Phil Sphicas8e08a772022-03-24 10:09:49 -0700410 client_socket = sslc().wrap_socket(client_socket,
411 server_hostname=url.hostname)
Jens Harbott6bc422d2017-09-27 10:29:34 +0000412 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
413 try:
414 client_socket.connect(sa)
415 except socket.error:
416 client_socket.close()
417 continue
418 break
xxj8eb90982017-04-10 21:18:39 +0800419 else:
Jens Harbott6bc422d2017-09-27 10:29:34 +0000420 raise socket.error('WebSocket creation failed')
Markus Zoellerae36ce82017-03-20 16:27:26 +0100421 # Turn the Socket into a WebSocket to do the communication
422 return _WebSocket(client_socket, url)
423
424
425class _WebSocket(object):
426 def __init__(self, client_socket, url):
427 """Contructor for the WebSocket wrapper to the socket."""
428 self._socket = client_socket
jianghua wangd22514a2017-05-08 08:05:04 +0100429 # cached stream for early frames.
430 self.cached_stream = b''
Markus Zoellerae36ce82017-03-20 16:27:26 +0100431 # Upgrade the HTTP connection to a WebSocket
432 self._upgrade(url)
433
jianghua wangd22514a2017-05-08 08:05:04 +0100434 def _recv(self, recv_size):
435 """Wrapper to receive data from the cached stream or socket."""
436 if recv_size <= 0:
437 return None
438
439 data_from_cached = b''
440 data_from_socket = b''
441 if len(self.cached_stream) > 0:
442 read_from_cached = min(len(self.cached_stream), recv_size)
443 data_from_cached += self.cached_stream[:read_from_cached]
444 self.cached_stream = self.cached_stream[read_from_cached:]
445 recv_size -= read_from_cached
446 if recv_size > 0:
447 data_from_socket = self._socket.recv(recv_size)
448 return data_from_cached + data_from_socket
449
Markus Zoellerae36ce82017-03-20 16:27:26 +0100450 def receive_frame(self):
451 """Wrapper for receiving data to parse the WebSocket frame format"""
452 # We need to loop until we either get some bytes back in the frame
453 # or no data was received (meaning the socket was closed). This is
454 # done to handle the case where we get back some empty frames
455 while True:
jianghua wangd22514a2017-05-08 08:05:04 +0100456 header = self._recv(2)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100457 # If we didn't receive any data, just return None
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900458 if not header:
Markus Zoellerae36ce82017-03-20 16:27:26 +0100459 return None
460 # We will make the assumption that we are only dealing with
461 # frames less than 125 bytes here (for the negotiation) and
462 # that only the 2nd byte contains the length, and since the
463 # server doesn't do masking, we can just read the data length
likui7d91c872020-09-22 12:29:16 +0800464 if int(header[1]) & 127 > 0:
465 return self._recv(int(header[1]) & 127)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100466
467 def send_frame(self, data):
468 """Wrapper for sending data to add in the WebSocket frame format."""
469 frame_bytes = list()
470 # For the first byte, want to say we are sending binary data (130)
471 frame_bytes.append(130)
472 # Only sending negotiation data so don't need to worry about > 125
473 # We do need to add the bit that says we are masking the data
474 frame_bytes.append(len(data) | 128)
475 # We don't really care about providing a random mask for security
476 # So we will just hard-code a value since a test program
477 mask = [7, 2, 1, 9]
478 for i in range(len(mask)):
479 frame_bytes.append(mask[i])
480 # Mask each of the actual data bytes that we are going to send
481 for i in range(len(data)):
likui7d91c872020-09-22 12:29:16 +0800482 frame_bytes.append(int(data[i]) ^ mask[i % 4])
Markus Zoellerae36ce82017-03-20 16:27:26 +0100483 # Convert our integer list to a binary array of bytes
484 frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
485 self._socket.sendall(frame_bytes)
486
487 def close(self):
488 """Helper method to close the connection."""
489 # Close down the real socket connection and exit the test program
490 if self._socket is not None:
491 self._socket.shutdown(1)
492 self._socket.close()
493 self._socket = None
494
495 def _upgrade(self, url):
496 """Upgrade the HTTP connection to a WebSocket and verify."""
melanie witt27ba9332019-04-26 02:33:20 +0000497 # It is possible to pass the path as a query parameter in the request,
498 # so use it if present
Jason Lica0fad02020-04-06 10:56:43 -0500499 # Given noVNC format
500 # https://x.com/vnc_auto.html?path=%3Ftoken%3Dxxx,
501 # url format is
502 # ParseResult(scheme='https', netloc='x.com',
503 # path='/vnc_auto.html', params='',
504 # query='path=%3Ftoken%3Dxxx', fragment='').
505 # qparams format is {'path': ['?token=xxx']}
melanie witt27ba9332019-04-26 02:33:20 +0000506 qparams = urlparse.parse_qs(url.query)
Jason Lica0fad02020-04-06 10:56:43 -0500507 # according to references
508 # https://docs.python.org/3/library/urllib.parse.html
509 # https://tools.ietf.org/html/rfc3986#section-3.4
510 # qparams['path'][0] format is '?token=xxx' without / prefix
511 # remove / in /websockify to comply to references.
512 path = qparams['path'][0] if 'path' in qparams else 'websockify'
513 # Fix websocket request format by adding / prefix.
514 # Updated request format: GET /?token=xxx HTTP/1.1
515 # or GET /websockify HTTP/1.1
516 reqdata = 'GET /%s HTTP/1.1\r\n' % path
Mohammed Naseraa5dd9a2017-12-29 18:52:01 -0500517 reqdata += 'Host: %s' % url.hostname
518 # Add port only if we have one specified
519 if url.port:
520 reqdata += ':%s' % url.port
521 # Line-ending for Host header
522 reqdata += '\r\n'
Markus Zoellerae36ce82017-03-20 16:27:26 +0100523 # Tell the HTTP Server to Upgrade the connection to a WebSocket
524 reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
melanie witt27ba9332019-04-26 02:33:20 +0000525 # The token=xxx is sent as a Cookie not in the URI for noVNC < v1.1.0
Markus Zoellerae36ce82017-03-20 16:27:26 +0100526 reqdata += 'Cookie: %s\r\n' % url.query
527 # Use a hard-coded WebSocket key since a test program
528 reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
529 reqdata += 'Sec-WebSocket-Version: 13\r\n'
530 # We are choosing to use binary even though browser may do Base64
531 reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
532 # Send the HTTP GET request and get the response back
533 self._socket.sendall(reqdata.encode('utf8'))
534 self.response = data = self._socket.recv(4096)
535 # Loop through & concatenate all of the data in the response body
jianghua wangd22514a2017-05-08 08:05:04 +0100536 end_loc = self.response.find(b'\r\n\r\n')
537 while data and end_loc < 0:
Markus Zoellerae36ce82017-03-20 16:27:26 +0100538 data = self._socket.recv(4096)
539 self.response += data
jianghua wangd22514a2017-05-08 08:05:04 +0100540 end_loc = self.response.find(b'\r\n\r\n')
541
542 if len(self.response) > end_loc + 4:
543 # In case some frames (e.g. the first RFP negotiation) have
544 # arrived, cache it for next reading.
545 self.cached_stream = self.response[end_loc + 4:]
546 # ensure response ends with '\r\n\r\n'.
547 self.response = self.response[:end_loc + 4]