-
Notifications
You must be signed in to change notification settings - Fork 26
/
_cim_http.py
469 lines (380 loc) · 16.7 KB
/
_cim_http.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
#
# (C) Copyright 2003-2005 Hewlett-Packard Development Company, L.P.
# (C) Copyright 2006-2007 Novell, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Author: Tim Potter <tpot@hp.com>
# Author: Martin Pool <mbp@hp.com>
# Author: Bart Whiteley <bwhiteley@suse.de>
# Author: Ross Peoples <ross.peoples@gmail.com>
#
'''
Send HTTP/HTTPS requests to a WBEM server.
This module does not know anything about the fact that the data being
transferred in the HTTP request and response is CIM-XML. It is up to the
caller to provide CIM-XML formatted input data and interpret the result data
as CIM-XML.
'''
from __future__ import print_function, absolute_import
import re
import os
import base64
import ssl
import six
from six.moves import urllib
import requests
from requests.packages import urllib3
from ._cim_obj import CIMClassName, CIMInstanceName
from ._cim_constants import DEFAULT_URL_SCHEME, DEFAULT_URL_PORT_HTTP, \
DEFAULT_URL_PORT_HTTPS
from ._exceptions import ConnectionError, AuthError, TimeoutError, HTTPError, \
HeaderParseError # pylint: disable=redefined-builtin
from ._utils import _ensure_unicode, _ensure_bytes, _format
__all__ = []
_ON_RTD = os.environ.get('READTHEDOCS', None) == 'True'
HTTP_CONNECT_TIMEOUT = 10 # HTTP connect timeout in seconds
# Regexp pattern for an entire URL, with parsing items:
# (1) scheme (optional)
# (2) host (required) - may contain brackets and colons for IPv6 addresses
# (3) port (optional)
# (4) trailing path segments (optional)
URL_PATTERN = re.compile(
r'^(?:(.*?)://)?([^/]+?)(?::([^:\[\]\-/]+?))?(/.*)?$')
# Regexp pattern for the host of a URL in (bracketed) RFC6874 URI format,
# with parsing items:
# (1) host (required)
# (2) zone ID (optional)
URL_IPV6_URI_PATTERN = re.compile(r'^\[(.+?)(?:[\-%](.+))?\]$')
# Regexp pattern for the host of a URL in (unbracketed) RFC4007 text format,
# with parsing items:
# (1):(2) host (required)
# (3) zone ID (optional)
URL_IPV6_TEXT_PATTERN = re.compile(r'^([^\[\]]*?):([^\[\]]*?)(?:%([^\[\]]+))?$')
def parse_url(url, allow_defaults=True):
"""
Return a tuple (`scheme`, `hostport`, `url`) from the URL specified in the
input URL.
In the input URL, the host portion may be specified as a short or long
host name, dotted IPv4 address, bracketed IPv6 address in the RFC6874 URI
syntax, or unbracketed IPv6 address in the RFC4007 text syntax. In either
format, IPv6 addresses may optionally have a zone index (aka scope ID),
delimited with '-' or '%'.
The returned `scheme` item is the normalized scheme portion of the input
URL, as a unicode string. It is always in lower case. If not specified, it
defaults to DEFAULT_URL_SCHEME. Only 'http' and 'https' are supported
schemes, and ValueError is raised for invalid schemes.
The returned `hostport` item is the normalized host and port number of the
input URL in the format '{host}:{port}', as a unicode string.
IPv6 addresses are always represented in (and converted to) RFC6874 URI
syntax. If there is no port number specified in the input URL, the port
in the returned `hostport` item defaults to DEFAULT_URL_PORT_HTTP or
DEFAULT_URL_PORT_HTTP, dependent on the scheme.
The returned `hostport` item should be used as the 'host' portion of CIM
namespace paths.
The returned `url` item is the normalized input URL, constructed from
the returned `scheme` and `hostport` items.
Defaults are only applied if allow_defaults=True. Otherwise, missing
components cause ValueError to be raised.
ValueError is raised in addition for invalid URLs or portions thereof.
Examples for valid URLs can be found in the test program
`tests/unittest/pywbem/test_cim_http.py`.
Parameters:
url (string): Input URL.
allow_defaults (bool): If `True` allow defaults for scheme and
port. If `False`, raise ValueError for missing scheme or port.
Returns:
tuple of (`scheme`, `hostport`, `url`)
Raises:
ValueError: Component missing (when allow_defaults = False)
ValueError: Invalid URL
"""
url = _ensure_unicode(url)
m = URL_PATTERN.match(url)
if not m:
raise ValueError(
_format("Invalid URL {0!A}", url))
scheme = m.group(1)
if not scheme:
if not allow_defaults:
raise ValueError(
_format("Scheme component missing in URL {0!A}", url))
scheme = DEFAULT_URL_SCHEME
scheme = scheme.lower()
if scheme not in ('http', 'https'):
raise ValueError(
_format("Unsupported scheme {0!A} in URL {1!A}", scheme, url))
port = m.group(3)
if not port:
if not allow_defaults:
raise ValueError(
_format("Port component missing in URL {0!A}", url))
if scheme == 'http':
port = DEFAULT_URL_PORT_HTTP
else:
assert scheme == 'https'
port = DEFAULT_URL_PORT_HTTPS
try:
port = int(port)
except ValueError:
raise ValueError(
_format("Invalid port number {0!A} in URL {1!A}", port, url))
host = m.group(2)
assert host is not None # This is guaranteed by the URL_PATTERN
# Normalize the host for IPv6 addresses.
m = URL_IPV6_URI_PATTERN.match(host)
if m:
# It is an IPv6 address in RFC6874 URI syntax
_host = m.group(1)
_zone_index = m.group(2)
if _zone_index is not None:
host = '[{0}-{1}]'.format(_host, _zone_index)
else:
host = '[{0}]'.format(_host)
else:
m = URL_IPV6_TEXT_PATTERN.match(host)
if m:
# It is an IPv6 address in RFC4007 text syntax
_host = '{0}:{1}'.format(m.group(1), m.group(2))
_zone_index = m.group(3)
if _zone_index is not None:
host = '[{0}-{1}]'.format(_host, _zone_index)
else:
host = '[{0}]'.format(_host)
hostport = u'{0}:{1}'.format(host, port)
url = u'{0}://{1}'.format(scheme, hostport)
return scheme, hostport, url
def request_exc_message(exc, conn):
"""
Return a reasonable exception message from a requests exception.
The approach is to dig deep to the original reason, if the original
exception is present, skipping irrelevant exceptions such as
`urllib3.exceptions.MaxRetryError`, and eliminating useless object
representations such as the connection pool object in
`urllib3.exceptions.NewConnectionError`.
Parameters:
exc (requests.exceptions.RequestException): Exception
conn (WBEMConnection): Connection that was used.
Returns:
string: A reasonable exception message from the specified exception.
"""
if exc.args:
if isinstance(exc.args[0], Exception):
org_exc = exc.args[0]
if isinstance(org_exc, urllib3.exceptions.MaxRetryError):
reason_exc = org_exc.reason
message = str(reason_exc)
else:
message = str(org_exc.args[0])
else:
message = str(exc.args[0])
# Eliminate useless object repr at begin of the message
m = re.match(r'^(\(<[^>]+>, \'(.*)\'\)|<[^>]+>: (.*))$', message)
if m:
message = m.group(2) or m.group(3)
else:
message = ""
if conn.scheme == 'https':
message = message + \
"; OpenSSL version used: {}".format(ssl.OPENSSL_VERSION)
return message
def wbem_request(conn, req_data, cimxml_headers):
"""
Send an HTTP or HTTPS request to a WBEM server and return the response.
Parameters:
conn (:class:`~pywbem.WBEMConnection`):
WBEM connection to be used.
req_data (:term:`string`):
The CIM-XML formatted data to be sent as a request to the WBEM server.
cimxml_headers (:term:`py:iterable` of tuple(string,string)):
Where each tuple contains: header name, header value.
CIM-XML extension header fields for the request. The header value
is a string (unicode or binary) that is not encoded, and the two-step
encoding required by DSP0200 is performed inside of this function.
A value of `None` is treated like an empty iterable.
Returns:
Tuple containing:
The CIM-XML formatted response data from the WBEM server, as a
:term:`byte string` object.
The server response time in seconds as floating point number if
this data was received from the server. If no data returned
from server `None` is returned.
Raises:
:exc:`~pywbem.AuthError`
:exc:`~pywbem.ConnectionError`
:exc:`~pywbem.TimeoutError`
:exc:`~pywbem.HTTPError`
"""
if cimxml_headers is None:
cimxml_headers = []
target = '/cimom'
target_url = '{}{}'.format(conn.url, target)
# Make sure the data parameter is converted to a UTF-8 encoded byte string.
# This is important because according to RFC2616, the Content-Length HTTP
# header must be measured in Bytes (and the Content-Type header will
# indicate UTF-8).
req_body = _ensure_bytes(req_data)
req_body = b'<?xml version="1.0" encoding="utf-8" ?>\n' + req_body
req_headers = {
'Content-type': 'application/xml; charset="utf-8"',
'Content-length': '{}'.format(len(req_body)),
}
req_headers.update(dict(cimxml_headers))
if conn.creds is not None:
auth = '{0}:{1}'.format(conn.creds[0], conn.creds[1])
auth64 = _ensure_unicode(base64.b64encode(
_ensure_bytes(auth))).replace('\n', '')
req_headers['Authorization'] = 'Basic {0}'.format(auth64)
if conn.operation_recorders:
for recorder in conn.operation_recorders:
recorder.stage_http_request(
conn.conn_id, 11, conn.url, target, 'POST',
dict(cimxml_headers), req_body)
# We want clean response data when an exception is raised before
# the HTTP response comes in:
recorder.stage_http_response1(conn.conn_id, None, None, None, None)
recorder.stage_http_response2(None)
try:
resp = conn.session.post(
target_url, data=req_body, headers=req_headers,
timeout=(HTTP_CONNECT_TIMEOUT, conn.timeout))
except requests.exceptions.SSLError as exc:
msg = request_exc_message(exc, conn)
raise ConnectionError(msg, conn_id=conn.conn_id)
except requests.exceptions.ConnectionError as exc:
msg = request_exc_message(exc, conn)
raise ConnectionError(msg, conn_id=conn.conn_id)
except (requests.exceptions.ReadTimeout, requests.exceptions.RetryError) \
as exc:
msg = request_exc_message(exc, conn)
raise TimeoutError(msg, conn_id=conn.conn_id)
except requests.exceptions.RequestException as exc:
msg = request_exc_message(exc, conn)
raise ConnectionError(msg, conn_id=conn.conn_id)
# Get the optional response time header
svr_resp_time = resp.headers.get('WBEMServerResponseTime', None)
if svr_resp_time is not None:
try:
# convert to float and map from microsec to sec.
svr_resp_time = float(svr_resp_time) / 1000000
except ValueError:
pass
if conn.operation_recorders:
for recorder in conn.operation_recorders:
recorder.stage_http_response1(
conn.conn_id,
resp.raw.version,
resp.status_code,
resp.reason,
resp.headers)
if resp.status_code != 200:
if resp.status_code == 401:
msg = "WBEM server returned HTTP status {0} ({1}).". \
format(resp.status_code, resp.reason)
# According to RFC2016/2017, the server must include a
# WWW-Authenticate header in the response when returning 401.
# Pywbem treats this header as optional.
# According to RFC2016/2017, the value of that header must be a
# comma-separated list of auth schemes, where the Basic
# auth scheme typically looks like:
# Basic realm="somerealm"
# Some data points of Basic auth schemes that have been observed:
# Basic "hostname:port"
# Pywbem attempts to accomodate all of that.
server_auths = resp.headers.get('WWW-Authenticate', None)
if server_auths:
server_auths = server_auths.split(',')
else:
server_auths = []
server_auths = [sa.split(' ')[0] for sa in server_auths]
if 'Basic' not in server_auths:
# If client and server do not have a commonly supported
# HTTP auth scheme, that is a more severe issue before it
# even gets to things like invalid credentials or permissions.
msg += _format(
" Server does not support HTTP authentication scheme "
"'Basic' supported by pywbem, but only {0!A}.",
', '.join(server_auths))
else:
msg += _format(
" This is most likely an issue with userid/password, but "
"for servers that implement resource access control it "
"might also be an issue with the permissions of the "
"userid.")
raise AuthError(msg, conn_id=conn.conn_id)
cimerror_hdr = resp.headers.get('CIMError', None)
cimdetails = {}
if cimerror_hdr is not None:
pgdetails_hdr = resp.headers.get('PGErrorDetail', None)
if pgdetails_hdr is not None:
cimdetails['PGErrorDetail'] = \
urllib.parse.unquote(pgdetails_hdr)
raise HTTPError(
resp.status_code, resp.reason, cimerror_hdr, cimdetails,
conn_id=conn.conn_id, request_data=req_body)
# status code 200
resp_content_type = resp.headers.get('Content-type', None)
if resp_content_type is not None and \
not resp_content_type.startswith('application/xml') and \
not resp_content_type.startswith('text/xml'):
raise HeaderParseError(
"WBEM server returned invalid Content-type header: {!r}".
format(resp_content_type),
conn_id=conn.conn_id, request_data=req_body,
response_data=resp.text)
resp_body = resp.content
if conn.operation_recorders:
for recorder in conn.operation_recorders:
recorder.stage_http_response2(resp_body)
return resp_body, svr_resp_time
def max_repr(text, max_len=1000):
"""
Return the input text as a Python string representation (i.e. using repr())
that is limited to a maximum length.
"""
if text is None:
text_repr = 'None'
elif len(text) > max_len:
text_repr = repr(text[0:max_len]) + '...'
else:
text_repr = repr(text)
return text_repr
def get_cimobject_header(obj):
"""
Return the value for the CIM-XML extension header field 'CIMObject', using
the given object.
This function implements the rules defined in DSP0200 section 6.3.7
"CIMObject". The format of the CIMObject value is similar but not identical
to a local WBEM URI (one without namespace type and authority), as defined
in DSP0207.
One difference is that DSP0207 requires a leading slash for a local WBEM
URI, e.g. '/root/cimv2:CIM_Class.k=1', while the CIMObject value has no
leading slash, e.g. 'root/cimv2:CIM_Class.k=1'.
Another difference is that the CIMObject value for instance paths has
provisions for an instance path without keys, while WBEM URIs do not have
that. Pywbem does not support that.
"""
# Local namespace path
if isinstance(obj, six.string_types):
return obj
# Local class path
if isinstance(obj, CIMClassName):
return obj.to_wbem_uri(format='cimobject')
# Local instance path
if isinstance(obj, CIMInstanceName):
return obj.to_wbem_uri(format='cimobject')
raise TypeError(
_format("Invalid object type {0} to generate CIMObject header value "
"from", type(obj)))