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 Send HTTP/HTTPS requests to a WBEM server.
27
28 This module does not know anything about the fact that the data being
29 transferred in the HTTP request and response is CIM-XML. It is up to the
30 caller to provide CIM-XML formatted input data and interpret the result data
31 as CIM-XML.
32 '''
33 from __future__ import print_function
34 import re
35 import os
36 import sys
37 import errno
38 import socket
39 import getpass
40 from stat import S_ISSOCK
41 import platform
42 import base64
43 import threading
44 from datetime import datetime
45 import six
46 from six.moves import http_client as httplib
47 from six.moves import urllib
48
49 from .cim_obj import CIMClassName, CIMInstanceName, _ensure_unicode, \
50 _ensure_bytes
51
52 if six.PY2:
53 from M2Crypto import SSL
54 from M2Crypto.Err import SSLError
55 _HAVE_M2CRYPTO = True
56
57 SocketErrors = (socket.error, socket.sslerror)
58 else:
59 import ssl as SSL
60 from ssl import SSLError, CertificateError
61 _HAVE_M2CRYPTO = False
62
63 SocketErrors = (socket.error,)
64
65 __all__ = ['Error', 'ConnectionError', 'AuthError', 'TimeoutError']
66
68 """Exception base class for catching any HTTP transport related errors."""
69 pass
70
72 """This exception is raised when there is a problem with the connection
73 to the server. A retry may or may not succeed."""
74 pass
75
77 """This exception is raised when an authentication error (401) occurs."""
78 pass
79
81 """This exception is raised when the client times out."""
82 pass
83
84
85 DEFAULT_PORT_HTTP = 5988
86 DEFAULT_PORT_HTTPS = 5989
87
88
89 DEFAULT_CA_CERT_PATHS = \
90 ['/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt', \
91 '/etc/ssl/certs', '/etc/ssl/certificates']
92
94 """Return the list of default certificate paths defined for this
95 system environment. This is the list of directories that
96 should be searched to find a directory that contains
97 certificates possibly suitable for the ssl ca_certs parameter
98 in SSL connections.
99 """
100 return DEFAULT_CA_CERT_PATHS
101
102
104 """HTTP timeout class that is a context manager (for use by 'with'
105 statement).
106
107 Usage:
108 ::
109 with HTTPTimeout(timeout, http_conn):
110 ... operations using http_conn ...
111
112 If the timeout expires, the socket of the HTTP connection is shut down.
113 Once the http operations return as a result of that or for other reasons,
114 the exit handler of this class raises a `cim_http.Error` exception in the
115 thread that executed the ``with`` statement.
116 """
117
118 - def __init__(self, timeout, http_conn):
119 """Initialize the HTTPTimeout object.
120
121 :Parameters:
122
123 timeout : number
124 Timeout in seconds, ``None`` means no timeout.
125
126 http_conn : `httplib.HTTPBaseConnection` (or subclass)
127 The connection that is to be stopped when the timeout expires.
128 """
129
130 self._timeout = timeout
131 self._http_conn = http_conn
132 self._retrytime = 5
133
134
135
136 self._timer = None
137 self._ts1 = None
138 self._shutdown = None
139
140 return
141
143 if self._timeout != None:
144 self._timer = threading.Timer(self._timeout,
145 HTTPTimeout.timer_expired, [self])
146 self._timer.start()
147 self._ts1 = datetime.now()
148 self._shutdown = False
149 return
150
151 - def __exit__(self, exc_type, exc_value, traceback):
152 if self._timeout != None:
153 self._timer.cancel()
154 if self._shutdown:
155
156
157
158 ts2 = datetime.now()
159 duration = ts2 - self._ts1
160 duration_sec = float(duration.microseconds)/1000000 +\
161 duration.seconds + duration.days*24*3600
162 raise TimeoutError("The client timed out and closed the "\
163 "socket after %.0fs." % duration_sec)
164 return False
165
167 """
168 This method is invoked in context of the timer thread, so we cannot
169 directly throw exceptions (we can, but they would be in the wrong
170 thread), so instead we shut down the socket of the connection.
171 When the timeout happens in early phases of the connection setup,
172 there is no socket object on the HTTP connection yet, in that case
173 we retry after the retry duration, indefinitely.
174 So we do not guarantee in all cases that the overall operation times
175 out after the specified timeout.
176 """
177 if self._http_conn.sock != None:
178 self._shutdown = True
179 self._http_conn.sock.shutdown(socket.SHUT_RDWR)
180 else:
181
182 self._timer.cancel()
183 self._timer = threading.Timer(self._retrytime,
184 HTTPTimeout.timer_expired, [self])
185 self._timer.start()
186
188 """Return a tuple of ``(host, port, ssl)`` from the URL specified in the
189 ``url`` parameter.
190
191 The returned ``ssl`` item is a boolean indicating the use of SSL, and is
192 recognized from the URL scheme (http vs. https). If none of these schemes
193 is specified in the URL, the returned value defaults to False
194 (non-SSL/http).
195
196 The returned ``port`` item is the port number, as an integer. If there is
197 no port number specified in the URL, the returned value defaults to 5988
198 for non-SSL/http, and to 5989 for SSL/https.
199
200 The returned ``host`` item is the host portion of the URL, as a string.
201 The host portion may be specified in the URL as a short or long host name,
202 dotted IPv4 address, or bracketed IPv6 address with or without zone index
203 (aka scope ID). An IPv6 address is converted from the RFC6874 URI syntax
204 to the RFC4007 text representation syntax before being returned, by
205 removing the brackets and converting the zone index (if present) from
206 "-eth0" to "%eth0".
207
208 Examples for valid URLs can be found in the test program
209 `testsuite/test_cim_http.py`.
210 """
211
212 default_ssl = False
213
214
215 matches = re.match(r"^(https?)://(.*)$", url, re.I)
216 if matches:
217 _scheme = matches.group(1).lower()
218 hostport = matches.group(2)
219 ssl = (_scheme == 'https')
220 else:
221
222
223 ssl = default_ssl
224 hostport = url
225
226
227
228
229 result = hostport.find("/")
230 if result >= 0:
231 hostport = hostport[0:result]
232
233
234
235
236 matches = re.search(r":([0-9]+)$", hostport)
237 if matches:
238 host = hostport[0:matches.start(0)]
239 port = int(matches.group(1))
240 else:
241 host = hostport
242 port = DEFAULT_PORT_HTTPS if ssl else DEFAULT_PORT_HTTP
243
244
245
246
247
248
249
250
251 matches = re.match(r"^\[(.+?)(?:-(.+))?\]$", host)
252 if matches:
253
254 host = matches.group(1)
255 if matches.group(2) != None:
256
257 host += "%" + matches.group(2)
258
259 return host, port, ssl
260
262 """
263 Try to find out system path with ca certificates. This path is cached and
264 returned. If no path is found out, None is returned.
265 """
266 if not hasattr(get_default_ca_certs, '_path'):
267 for path in get_default_ca_cert_paths():
268 if os.path.exists(path):
269 get_default_ca_certs._path = path
270 break
271 else:
272 get_default_ca_certs._path = None
273 return get_default_ca_certs._path
274
275
276 -def wbem_request(url, data, creds, headers=[], debug=0, x509=None,
277 verify_callback=None, ca_certs=None,
278 no_verification=False, timeout=None):
279
280
281 """
282 Send an HTTP or HTTPS request to a WBEM server and return the response.
283
284 This function uses Python's built-in `httplib` module.
285
286 :Parameters:
287
288 url : Unicode string or UTF-8 encoded byte string
289 URL of the WBEM server (e.g. ``"https://10.11.12.13:6988"``).
290 For details, see the ``url`` parameter of
291 `WBEMConnection.__init__`.
292
293 data : Unicode string or UTF-8 encoded byte string
294 The CIM-XML formatted data to be sent as a request to the WBEM server.
295
296 creds
297 Credentials for authenticating with the WBEM server.
298 For details, see the ``creds`` parameter of
299 `WBEMConnection.__init__`.
300
301 headers : list of Unicode strings or UTF-8 encoded byte strings
302 List of HTTP header fields to be added to the request, in addition to
303 the standard header fields such as ``Content-type``,
304 ``Content-length``, and ``Authorization``.
305
306 debug : ``bool``
307 Boolean indicating whether to create debug information.
308
309 x509
310 Used for HTTPS with certificates.
311 For details, see the ``x509`` parameter of
312 `WBEMConnection.__init__`.
313
314 verify_callback
315 Used for HTTPS with certificates.
316 For details, see the ``verify_callback`` parameter of
317 `WBEMConnection.__init__`.
318
319 ca_certs
320 Used for HTTPS with certificates.
321 For details, see the ``ca_certs`` parameter of
322 `WBEMConnection.__init__`.
323
324 no_verification
325 Used for HTTPS with certificates.
326 For details, see the ``no_verification`` parameter of
327 `WBEMConnection.__init__`.
328
329 timeout : number
330 Timeout in seconds, for requests sent to the server. If the server did
331 not respond within the timeout duration, the socket for the connection
332 will be closed, causing a `TimeoutError` to be raised.
333 A value of ``None`` means there is no timeout.
334 A value of ``0`` means the timeout is very short, and does not really
335 make any sense.
336 Note that not all situations can be handled within this timeout, so
337 for some issues, this method may take longer to raise an exception.
338
339 :Returns:
340 The CIM-XML formatted response data from the WBEM server, as a `unicode`
341 object.
342
343 :Raises:
344 :raise AuthError:
345 :raise ConnectionError:
346 :raise TimeoutError:
347 """
348
349 class HTTPBaseConnection:
350 """ Common base for specific connection classes. Implements
351 the send method
352 """
353
354 def send(self, strng):
355 """
356 A copy of httplib.HTTPConnection.send(), with these fixes:
357
358 * We fix the problem that the connection gets closed upon error
359 32 (EPIPE), by not doing that (If the connection gets closed,
360 getresponse() fails). This problem was reported as Python issue
361 #5542, and the same fix we do here was integrated into Python
362 2.7 and 3.1 or 3.2, but not into Python 2.6 (so we still need
363 our fix here).
364
365 * Ensure that the data are bytes, not unicode.
366 TODO 2016-05 AM: Ensuring bytes at this level can only be a
367 quick fix. Figure out a better approach.
368 """
369 if self.sock is None:
370 if self.auto_open:
371 self.connect()
372 else:
373 raise httplib.NotConnected()
374 if self.debuglevel > 0:
375 print("send: %r" % strng)
376 blocksize = 8192
377 if hasattr(strng, 'read') and not isinstance(strng, array):
378 if self.debuglevel > 0:
379 print("sendIng a read()able")
380 data = strng.read(blocksize)
381 while data:
382 self.sock.sendall(_ensure_bytes(data))
383 data = strng.read(blocksize)
384 else:
385 self.sock.sendall(_ensure_bytes(strng))
386
387 class HTTPConnection(HTTPBaseConnection, httplib.HTTPConnection):
388 """ Execute client connection without ssl using httplib. """
389 def __init__(self, host, port=None, timeout=None):
390
391 httplib.HTTPConnection.__init__(self, host=host, port=port,
392 timeout=timeout)
393
394 class HTTPSConnection(HTTPBaseConnection, httplib.HTTPSConnection):
395 """ Execute client connection with ssl using httplib."""
396
397 def __init__(self, host, port=None, key_file=None, cert_file=None,
398 ca_certs=None, verify_callback=None, timeout=None):
399
400 httplib.HTTPSConnection.__init__(self, host=host, port=port,
401 key_file=key_file,
402 cert_file=cert_file,
403 timeout=timeout)
404 self.ca_certs = ca_certs
405 self.verify_callback = verify_callback
406
407 def connect(self):
408
409 """Connect to a host on a given (SSL) port."""
410
411
412 if _HAVE_M2CRYPTO:
413
414
415
416
417
418
419
420
421
422 if sys.version_info[0:2] >= (2, 7):
423
424 self.sock = socket.create_connection(
425 (self.host, self.port), None, self.source_address)
426 else:
427 self.sock = socket.create_connection(
428 (self.host, self.port), None)
429
430
431
432
433
434 ctx = SSL.Context('sslv23')
435
436 if self.cert_file:
437 ctx.load_cert(self.cert_file, keyfile=self.key_file)
438 if self.ca_certs:
439 ctx.set_verify(
440 SSL.verify_peer | SSL.verify_fail_if_no_peer_cert,
441 depth=9, callback=verify_callback)
442 if os.path.isdir(self.ca_certs):
443 ctx.load_verify_locations(capath=self.ca_certs)
444 else:
445 ctx.load_verify_locations(cafile=self.ca_certs)
446 try:
447 self.sock = SSL.Connection(ctx, self.sock)
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462 if False:
463
464
465 if self.timeout is not None:
466 self.sock.set_socket_read_timeout(
467 SSL.timeout(self.timeout))
468 self.sock.set_socket_write_timeout(
469 SSL.timeout(self.timeout))
470
471 self.sock.addr = (self.host, self.port)
472 self.sock.setup_ssl()
473 self.sock.set_connect_state()
474 ret = self.sock.connect_ssl()
475 if self.ca_certs:
476 check = getattr(self.sock, 'postConnectionCheck',
477 self.sock.clientPostConnectionCheck)
478 if check is not None:
479 if not check(self.sock.get_peer_cert(), self.host):
480 raise ConnectionError(
481 'SSL error: post connection check failed')
482 return ret
483
484
485
486
487
488
489
490 except SSLError as arg:
491 raise ConnectionError(
492 "SSL error %s: %s" % (arg.__class__, arg))
493
494
495 else:
496
497
498
499
500 ctx = SSL.SSLContext(SSL.PROTOCOL_SSLv23)
501
502
503 if self.cert_file:
504 ctx.load_cert(self.cert_file, keyfile=self.key_file)
505 if self.ca_certs:
506
507
508 ctx.verify_mode = SSL.CERT_REQUIRED
509 if os.path.isdir(self.ca_certs):
510 ctx.load_verify_locations(capath=self.ca_certs)
511 else:
512 ctx.load_verify_locations(cafile=self.ca_certs)
513 ctx.check_hostname = True
514 else:
515 ctx.check_hostname = False
516 ctx.verify_mode = SSL.CERT_NONE
517
518
519 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
520 sock.settimeout(self.timeout)
521
522 try:
523 self.sock = ctx.wrap_socket(sock,
524 server_hostname=self.host)
525 return self.sock.connect((self.host, self.port))
526
527 except SSLError as arg:
528 raise ConnectionError(
529 "SSL error %s: %s" % (arg.__class__, arg))
530 except CertificateError as arg:
531 raise ConnectionError(
532 "SSL certificate error %s: %s" % (arg.__class__, arg))
533
534 class FileHTTPConnection(HTTPBaseConnection, httplib.HTTPConnection):
535 """Execute client connection based on a unix domain socket. """
536
537 def __init__(self, uds_path):
538 httplib.HTTPConnection.__init__(self, host='localhost')
539 self.uds_path = uds_path
540
541 def connect(self):
542 try:
543 socket_af = socket.AF_UNIX
544 except AttributeError:
545 raise ConnectionError(
546 'file URLs not supported on %s platform due '\
547 'to missing AF_UNIX support' % platform.system())
548 self.sock = socket.socket(socket_af, socket.SOCK_STREAM)
549 self.sock.connect(self.uds_path)
550
551 host, port, use_ssl = parse_url(_ensure_unicode(url))
552
553 key_file = None
554 cert_file = None
555
556 if use_ssl and x509 is not None:
557 cert_file = x509.get('cert_file')
558 key_file = x509.get('key_file')
559
560 num_tries = 0
561 local_auth_header = None
562 try_limit = 5
563
564
565
566
567
568 data = _ensure_bytes(data)
569
570 data = b'<?xml version="1.0" encoding="utf-8" ?>\n' + data
571
572
573
574 if not no_verification and ca_certs is None:
575 ca_certs = get_default_ca_certs()
576 elif no_verification:
577 ca_certs = None
578
579 local = False
580 if use_ssl:
581 client = HTTPSConnection(host=host,
582 port=port,
583 key_file=key_file,
584 cert_file=cert_file,
585 ca_certs=ca_certs,
586 verify_callback=verify_callback,
587 timeout=timeout)
588 else:
589 if url.startswith('http'):
590 client = HTTPConnection(host=host,
591 port=port,
592 timeout=timeout)
593 else:
594 if url.startswith('file:'):
595 url_ = url[5:]
596 else:
597 url_ = url
598 try:
599 status = os.stat(url_)
600 if S_ISSOCK(status.st_mode):
601 client = FileHTTPConnection(url_)
602 local = True
603 else:
604 raise ConnectionError('File URL is not a socket: %s' % url)
605 except OSError as exc:
606 raise ConnectionError('Error with file URL %s: %s' % (url, exc))
607
608 locallogin = None
609 if host in ('localhost', 'localhost6', '127.0.0.1', '::1'):
610 local = True
611 if local:
612 try:
613 locallogin = getpass.getuser()
614 except (KeyError, ImportError):
615 locallogin = None
616
617 with HTTPTimeout(timeout, client):
618
619 while num_tries < try_limit:
620 num_tries = num_tries + 1
621
622 client.putrequest('POST', '/cimom')
623
624 client.putheader('Content-type',
625 'application/xml; charset="utf-8"')
626 client.putheader('Content-length', str(len(data)))
627 if local_auth_header is not None:
628
629
630
631
632 client.putheader(*local_auth_header)
633 elif creds is not None:
634 auth = '%s:%s' % (creds[0], creds[1])
635 auth64 = _ensure_unicode(base64.b64encode(
636 _ensure_bytes(auth))).replace('\n', '')
637 client.putheader('Authorization', 'Basic %s' % auth64)
638 elif locallogin is not None:
639 client.putheader('PegasusAuthorization',
640 'Local "%s"' % locallogin)
641
642 for hdr in headers:
643 hdr = _ensure_unicode(hdr)
644 hdr_pieces = [x.strip() for x in hdr.split(':', 1)]
645 client.putheader(urllib.parse.quote(hdr_pieces[0]),
646 urllib.parse.quote(hdr_pieces[1]))
647
648 try:
649
650
651
652
653
654
655
656
657
658
659
660
661
662 try:
663
664
665 client.endheaders()
666 client.send(data)
667 except SocketErrors as exc:
668 if exc.args[0] == errno.ECONNRESET:
669 if debug:
670 print("Debug: Ignoring socket error ECONNRESET " \
671 "(connection reset) returned by server.")
672 elif exc.args[0] == errno.EPIPE:
673 if debug:
674 print("Debug: Ignoring socket error EPIPE " \
675 "(broken pipe) returned by server.")
676 else:
677 raise ConnectionError("Socket error: %s" % exc)
678
679 response = client.getresponse()
680
681 if response.status != 200:
682 if response.status == 401:
683 if num_tries >= try_limit:
684 raise AuthError(response.reason)
685 if not local:
686 raise AuthError(response.reason)
687 auth_chal = response.getheader('WWW-Authenticate', '')
688 if 'openwbem' in response.getheader('Server', ''):
689 if 'OWLocal' not in auth_chal:
690 try:
691 uid = os.getuid()
692 except AttributeError:
693 raise ConnectionError(
694 "OWLocal authorization for OpenWbem "\
695 "server not supported on %s platform "\
696 "due to missing os.getuid()" % \
697 platform.system())
698 local_auth_header = ('Authorization',
699 'OWLocal uid="%d"' % uid)
700 continue
701 else:
702 try:
703 nonce_idx = auth_chal.index('nonce=')
704 nonce_begin = auth_chal.index('"',
705 nonce_idx)
706 nonce_end = auth_chal.index('"',
707 nonce_begin+1)
708 nonce = auth_chal[nonce_begin+1:nonce_end]
709 cookie_idx = auth_chal.index('cookiefile=')
710 cookie_begin = auth_chal.index('"',
711 cookie_idx)
712 cookie_end = auth_chal.index(
713 '"', cookie_begin+1)
714 cookie_file = auth_chal[
715 cookie_begin+1:cookie_end]
716 file_hndl = open(cookie_file, 'r')
717 cookie = file_hndl.read().strip()
718 file_hndl.close()
719 local_auth_header = (
720 'Authorization',
721 'OWLocal nonce="%s", cookie="%s"' % \
722 (nonce, cookie))
723 continue
724 except Exception as exc:
725 if debug:
726 print("Debug: Ignoring exception %s " \
727 "in OpenWBEM auth challenge " \
728 "processing." % exc)
729 local_auth_header = None
730 continue
731 elif 'Local' in auth_chal:
732 try:
733 beg = auth_chal.index('"') + 1
734 end = auth_chal.rindex('"')
735 if end > beg:
736 _file = auth_chal[beg:end]
737 file_hndl = open(_file, 'r')
738 cookie = file_hndl.read().strip()
739 file_hndl.close()
740 local_auth_header = (
741 'PegasusAuthorization',
742 'Local "%s:%s:%s"' % \
743 (locallogin, _file, cookie))
744 continue
745 except ValueError:
746 pass
747 raise AuthError(response.reason)
748
749 cimerror_hdr = response.getheader('CIMError', None)
750 if cimerror_hdr is not None:
751 exc_str = 'CIMError: %s' % cimerror_hdr
752 pgerrordetail_hdr = response.getheader('PGErrorDetail',
753 None)
754 if pgerrordetail_hdr is not None:
755
756 exc_str += ', PGErrorDetail: %s' %\
757 urllib.parse.unquote(pgerrordetail_hdr)
758 raise ConnectionError(exc_str)
759
760 raise ConnectionError('HTTP error: %s' % response.reason)
761
762 body = response.read()
763
764 except httplib.BadStatusLine as exc:
765
766
767
768
769
770
771 if exc.line is None or exc.line.strip().strip("'") in \
772 ('', 'None'):
773 raise ConnectionError("The server closed the "\
774 "connection without returning any data, or the "\
775 "client timed out")
776 else:
777 raise ConnectionError("The server returned a bad "\
778 "HTTP status line: %r" % exc.line)
779 except httplib.IncompleteRead as exc:
780 raise ConnectionError("HTTP incomplete read: %s" % exc)
781 except httplib.NotConnected as exc:
782 raise ConnectionError("HTTP not connected: %s" % exc)
783 except SocketErrors as exc:
784 raise ConnectionError("Socket error: %s" % exc)
785
786 break
787
788 return body
789
790
792 """Return the HTTP header required to make a CIM operation request
793 using the given object. Return None if the object does not need
794 to have a header."""
795
796
797
798 if isinstance(obj, six.string_types):
799 return 'CIMObject: %s' % obj
800
801
802
803 if isinstance(obj, CIMClassName):
804 return 'CIMObject: %s:%s' % (obj.namespace, obj.classname)
805
806
807
808 if isinstance(obj, CIMInstanceName) and obj.namespace is not None:
809 return 'CIMObject: %s' % obj
810
811 raise TypeError('Don\'t know how to generate HTTP headers for %s' % obj)
812