Package pywbem :: Module cim_http
[frames] | no frames]

Source Code for Module pywbem.cim_http

  1  # 
  2  # (C) Copyright 2003-2005 Hewlett-Packard Development Company, L.P. 
  3  # (C) Copyright 2006-2007 Novell, Inc. 
  4  # 
  5  # This library is free software; you can redistribute it and/or 
  6  # modify it under the terms of the GNU Lesser General Public 
  7  # License as published by the Free Software Foundation; either 
  8  # version 2.1 of the License, or (at your option) any later version. 
  9  # 
 10  # This program is distributed in the hope that it will be useful, but 
 11  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 13  # Lesser General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU Lesser General Public 
 16  # License along with this program; if not, write to the Free Software 
 17  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 18  # 
 19  # Author: Tim Potter <tpot@hp.com> 
 20  # Author: Martin Pool <mbp@hp.com> 
 21  # Author: Bart Whiteley <bwhiteley@suse.de> 
 22  # 
 23   
 24  ''' 
 25  Send HTTP/HTTPS requests to a WBEM server. 
 26   
 27  This module does not know anything about the fact that the data being 
 28  transferred in the HTTP request and response is CIM-XML.  It is up to the 
 29  caller to provide CIM-XML formatted input data and interpret the result data 
 30  as CIM-XML. 
 31  ''' 
 32   
 33  import string 
 34  import re 
 35  import os 
 36  import sys 
 37  import socket 
 38  import getpass 
 39  from stat import S_ISSOCK 
 40  from types import StringTypes 
 41  import platform 
 42  import httplib 
 43  import base64 
 44  import urllib 
 45  import threading 
 46  from datetime import timedelta, datetime 
 47   
 48  from M2Crypto import SSL, Err 
 49   
 50  from pywbem import cim_obj 
 51   
 52  __all__ = ['Error', 'ConnectionError', 'AuthError', 'TimeoutError', 
 53             'HTTPTimeout', 'wbem_request', 'get_object_header'] 
 54   
55 -class Error(Exception):
56 """Exception base class for catching any HTTP transport related errors.""" 57 pass
58
59 -class ConnectionError(Error):
60 """This exception is raised when there is a problem with the connection 61 to the server. A retry may or may not succeed.""" 62 pass
63
64 -class AuthError(Error):
65 """This exception is raised when an authentication error (401) occurs.""" 66 pass
67
68 -class TimeoutError(Error):
69 """This exception is raised when the client times out.""" 70 pass
71
72 -class HTTPTimeout (object):
73 """HTTP timeout class that is a context manager (for use by 'with' 74 statement). 75 76 Usage: 77 :: 78 with HTTPTimeout(timeout, http_conn): 79 ... operations using http_conn ... 80 81 If the timeout expires, the socket of the HTTP connection is shut down. 82 Once the http operations return as a result of that or for other reasons, 83 the exit handler of this class raises a `cim_http.Error` exception in the 84 thread that executed the ``with`` statement. 85 """ 86
87 - def __init__(self, timeout, http_conn):
88 """Initialize the HTTPTimeout object. 89 90 :Parameters: 91 92 timeout : number 93 Timeout in seconds, ``None`` means no timeout. 94 95 http_conn : `httplib.HTTPBaseConnection` (or subclass) 96 The connection that is to be stopped when the timeout expires. 97 """ 98 99 self._timeout = timeout 100 self._http_conn = http_conn 101 self._retrytime = 5 # time in seconds after which a retry of the 102 # socket shutdown is scheduled if the socket 103 # is not yet on the connection when the 104 # timeout expires initially. 105 self._timer = None # the timer object 106 self._ts1 = None # timestamp when timer was started 107 self._shutdown = None # flag indicating that the timer handler has 108 # shut down the socket 109 return
110
111 - def __enter__(self):
112 if self._timeout != None: 113 self._timer = threading.Timer(self._timeout, 114 HTTPTimeout.timer_expired, [self]) 115 self._timer.start() 116 self._ts1 = datetime.now() 117 self._shutdown = False 118 return
119
120 - def __exit__(self, exc_type, exc_value, traceback):
121 if self._timeout != None: 122 self._timer.cancel() 123 if self._shutdown: 124 # If the timer handler has shut down the socket, we 125 # want to make that known, and override any other 126 # exceptions that may be pending. 127 ts2 = datetime.now() 128 duration = ts2 - self._ts1 129 duration_sec = float(duration.microseconds)/1000000 +\ 130 duration.seconds + duration.days*24*3600 131 raise TimeoutError("The client timed out and closed the "\ 132 "socket after %.0fs." % duration_sec) 133 return False # re-raise any other exceptions
134
135 - def timer_expired(self):
136 """ 137 This method is invoked in context of the timer thread, so we cannot 138 directly throw exceptions (we can, but they would be in the wrong 139 thread), so instead we shut down the socket of the connection. 140 When the timeout happens in early phases of the connection setup, 141 there is no socket object on the HTTP connection yet, in that case 142 we retry after the retry duration, indefinitely. 143 So we do not guarantee in all cases that the overall operation times 144 out after the specified timeout. 145 """ 146 if self._http_conn.sock != None: 147 self._shutdown = True 148 self._http_conn.sock.shutdown(socket.SHUT_RDWR) 149 else: 150 # Retry after the retry duration 151 self._timer.cancel() 152 self._timer = threading.Timer(self._retrytime, 153 HTTPTimeout.timer_expired, [self]) 154 self._timer.start()
155
156 -def parse_url(url):
157 """Return a tuple of ``(host, port, ssl)`` from the URL specified in the 158 ``url`` parameter. 159 160 The returned ``ssl`` item is a boolean indicating the use of SSL, and is 161 recognized from the URL scheme (http vs. https). If none of these schemes 162 is specified in the URL, the returned value defaults to False 163 (non-SSL/http). 164 165 The returned ``port`` item is the port number, as an integer. If there is 166 no port number specified in the URL, the returned value defaults to 5988 167 for non-SSL/http, and to 5989 for SSL/https. 168 169 The returned ``host`` item is the host portion of the URL, as a string. 170 The host portion may be specified in the URL as a short or long host name, 171 dotted IPv4 address, or bracketed IPv6 address with or without zone index 172 (aka scope ID). An IPv6 address is converted from the RFC6874 URI syntax 173 to the RFC4007 text representation syntax before being returned, by 174 removing the brackets and converting the zone index (if present) from 175 "-eth0" to "%eth0". 176 177 Examples for valid URLs can be found in the test program 178 `testsuite/test_cim_http.py`. 179 """ 180 181 default_port_http = 5988 # default port for http 182 default_port_https = 5989 # default port for https 183 default_ssl = False # default SSL use (for no or unknown scheme) 184 185 # Look for scheme. 186 m = re.match(r"^(https?)://(.*)$", url, re.I) 187 if m: 188 _scheme = m.group(1).lower() 189 hostport = m.group(2) 190 if _scheme == 'https': 191 ssl = True 192 else: # will be 'http' 193 ssl = False 194 else: 195 # The URL specified no scheme (or a scheme other than the expected 196 # schemes, but we don't check) 197 ssl = default_ssl 198 hostport = url 199 200 # Remove trailing path segments, if any. 201 # Having URL components other than just slashes (e.g. '#' or '?') is not 202 # allowed (but we don't check). 203 m = hostport.find("/") 204 if m >= 0: 205 hostport = hostport[0:m] 206 207 # Look for port. 208 # This regexp also works for (colon-separated) IPv6 addresses, because they 209 # must be bracketed in a URL. 210 m = re.search(r":([0-9]+)$", hostport) 211 if m: 212 host = hostport[0:m.start(0)] 213 port = int(m.group(1)) 214 else: 215 host = hostport 216 port = default_port_https if ssl else default_port_http 217 218 # Reformat IPv6 addresses from RFC6874 URI syntax to RFC4007 text 219 # representation syntax: 220 # - Remove the brackets. 221 # - Convert the zone index (aka scope ID) from "-eth0" to "%eth0". 222 # Note on the regexp below: The first group needs the '?' after '.+' to 223 # become non-greedy; in greedy mode, the optional second group would never 224 # be matched. 225 m = re.match(r"^\[(.+?)(?:-(.+))?\]$", host) 226 if m: 227 # It is an IPv6 address 228 host = m.group(1) 229 if m.group(2) != None: 230 # The zone index is present 231 host += "%" + m.group(2) 232 233 return host, port, ssl
234
235 -def get_default_ca_certs():
236 """ 237 Try to find out system path with ca certificates. This path is cached and 238 returned. If no path is found out, None is returned. 239 """ 240 if not hasattr(get_default_ca_certs, '_path'): 241 for path in ( 242 '/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt', 243 '/etc/ssl/certs', 244 '/etc/ssl/certificates'): 245 if os.path.exists(path): 246 get_default_ca_certs._path = path 247 break 248 else: 249 get_default_ca_certs._path = None 250 return get_default_ca_certs._path
251
252 -def wbem_request(url, data, creds, headers=[], debug=0, x509=None, 253 verify_callback=None, ca_certs=None, 254 no_verification=False, timeout=None):
255 """ 256 Send an HTTP or HTTPS request to a WBEM server and return the response. 257 258 This function uses Python's built-in `httplib` module. 259 260 :Parameters: 261 262 url : `unicode` or UTF-8 encoded `str` 263 URL of the WBEM server (e.g. ``"https://10.11.12.13:6988"``). 264 For details, see the ``url`` parameter of 265 `WBEMConnection.__init__`. 266 267 data : `unicode` or UTF-8 encoded `str` 268 The CIM-XML formatted data to be sent as a request to the WBEM server. 269 270 creds 271 Credentials for authenticating with the WBEM server. 272 For details, see the ``creds`` parameter of 273 `WBEMConnection.__init__`. 274 275 headers : list of `unicode` or UTF-8 encoded `str` 276 List of HTTP header fields to be added to the request, in addition to 277 the standard header fields such as ``Content-type``, 278 ``Content-length``, and ``Authorization``. 279 280 debug : ``bool`` 281 Boolean indicating whether to create debug information. 282 Not currently used. 283 284 x509 285 Used for HTTPS with certificates. 286 For details, see the ``x509`` parameter of 287 `WBEMConnection.__init__`. 288 289 verify_callback 290 Used for HTTPS with certificates. 291 For details, see the ``verify_callback`` parameter of 292 `WBEMConnection.__init__`. 293 294 ca_certs 295 Used for HTTPS with certificates. 296 For details, see the ``ca_certs`` parameter of 297 `WBEMConnection.__init__`. 298 299 no_verification 300 Used for HTTPS with certificates. 301 For details, see the ``no_verification`` parameter of 302 `WBEMConnection.__init__`. 303 304 timeout : number 305 Timeout in seconds, for requests sent to the server. If the server did 306 not respond within the timeout duration, the socket for the connection 307 will be closed, causing a `TimeoutError` to be raised. 308 A value of ``None`` means there is no timeout. 309 A value of ``0`` means the timeout is very short, and does not really 310 make any sense. 311 Note that not all situations can be handled within this timeout, so 312 for some issues, this method may take longer to raise an exception. 313 314 :Returns: 315 The CIM-XML formatted response data from the WBEM server, as a `unicode` 316 object. 317 318 :Raises: 319 :raise AuthError: 320 :raise ConnectionError: 321 :raise TimeoutError: 322 """ 323 324 class HTTPBaseConnection: 325 def send(self, str): 326 """ Same as httplib.HTTPConnection.send(), except we don't 327 check for sigpipe and close the connection. If the connection 328 gets closed, getresponse() fails. 329 """ 330 331 if self.sock is None: 332 if self.auto_open: 333 self.connect() 334 else: 335 raise httplib.NotConnected() 336 if self.debuglevel > 0: 337 print "send:", repr(str) 338 self.sock.sendall(str)
339 340 class HTTPConnection(HTTPBaseConnection, httplib.HTTPConnection): 341 def __init__(self, host, port=None, strict=None, timeout=None): 342 httplib.HTTPConnection.__init__(self, host, port, strict, timeout) 343 344 class HTTPSConnection(HTTPBaseConnection, httplib.HTTPSConnection): 345 def __init__(self, host, port=None, key_file=None, cert_file=None, 346 strict=None, ca_certs=None, verify_callback=None, 347 timeout=None): 348 httplib.HTTPSConnection.__init__(self, host, port, key_file, 349 cert_file, strict, timeout) 350 self.ca_certs = ca_certs 351 self.verify_callback = verify_callback 352 353 def connect(self): 354 "Connect to a host on a given (SSL) port." 355 356 # Calling httplib.HTTPSConnection.connect(self) does not work 357 # because of its ssl.wrap_socket() call. So we copy the code of 358 # that connect() method modulo the ssl.wrap_socket() call. 359 # 360 # Another change is that we do not pass the timeout value 361 # on to the socket call, because that does not work with M2Crypto. 362 if sys.version_info[0:2] >= (2, 7): 363 # the source_address argument was added in 2.7 364 self.sock = socket.create_connection( 365 (self.host, self.port), None, self.source_address) 366 else: 367 self.sock = socket.create_connection( 368 (self.host, self.port), None) 369 370 if self._tunnel_host: 371 self._tunnel() 372 # End of code from httplib.HTTPSConnection.connect(self). 373 374 ctx = SSL.Context('sslv23') 375 if self.cert_file: 376 ctx.load_cert(self.cert_file, keyfile=self.key_file) 377 if self.ca_certs: 378 ctx.set_verify( 379 SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, 380 depth=9, callback=verify_callback) 381 if os.path.isdir(self.ca_certs): 382 ctx.load_verify_locations(capath=self.ca_certs) 383 else: 384 ctx.load_verify_locations(cafile=self.ca_certs) 385 try: 386 self.sock = SSL.Connection(ctx, self.sock) 387 # Below is a body of SSL.Connection.connect() method 388 # except for the first line (socket connection). We want to 389 # preserve tunneling ability. 390 391 # Setting the timeout on the input socket does not work 392 # with M2Crypto, with such a timeout set it calls a different 393 # low level function (nbio instead of bio) that does not work. 394 # the symptom is that reading the response returns None. 395 # Therefore, we set the timeout at the level of the outer 396 # M2Crypto socket object. 397 if False: # Currently disabled 398 if self.timeout is not None: 399 self.sock.set_socket_read_timeout( 400 SSL.timeout(self.timeout)) 401 self.sock.set_socket_write_timeout( 402 SSL.timeout(self.timeout)) 403 404 self.sock.addr = (self.host, self.port) 405 self.sock.setup_ssl() 406 self.sock.set_connect_state() 407 ret = self.sock.connect_ssl() 408 if self.ca_certs: 409 check = getattr(self.sock, 'postConnectionCheck', 410 self.sock.clientPostConnectionCheck) 411 if check is not None: 412 if not check(self.sock.get_peer_cert(), self.host): 413 raise ConnectionError( 414 'SSL error: post connection check failed') 415 return ret 416 except (Err.SSLError, SSL.SSLError, SSL.Checker.WrongHost), arg: 417 # This will include SSLTimeoutError (it subclasses SSLError) 418 raise ConnectionError( 419 "SSL error %s: %s" % (str(arg.__class__), arg)) 420 421 class FileHTTPConnection(HTTPBaseConnection, httplib.HTTPConnection): 422 423 def __init__(self, uds_path): 424 httplib.HTTPConnection.__init__(self, 'localhost') 425 self.uds_path = uds_path 426 427 def connect(self): 428 try: 429 socket_af = socket.AF_UNIX 430 except AttributeError: 431 raise ConnectionError( 432 'file URLs not supported on %s platform due '\ 433 'to missing AF_UNIX support' % platform.system()) 434 self.sock = socket.socket(socket_af, socket.SOCK_STREAM) 435 self.sock.connect(self.uds_path) 436 437 host, port, use_ssl = parse_url(url) 438 439 key_file = None 440 cert_file = None 441 442 if use_ssl and x509 is not None: 443 cert_file = x509.get('cert_file') 444 key_file = x509.get('key_file') 445 446 numTries = 0 447 localAuthHeader = None 448 tryLimit = 5 449 450 # Make sure the data argument is converted to a UTF-8 encoded str object. 451 # This is important because according to RFC2616, the Content-Length HTTP 452 # header must be measured in Bytes (and the Content-Type header will 453 # indicate UTF-8). 454 if isinstance(data, unicode): 455 data = data.encode('utf-8') 456 457 data = '<?xml version="1.0" encoding="utf-8" ?>\n' + data 458 459 if not no_verification and ca_certs is None: 460 ca_certs = get_default_ca_certs() 461 elif no_verification: 462 ca_certs = None 463 464 local = False 465 if use_ssl: 466 h = HTTPSConnection(host, 467 port=port, 468 key_file=key_file, 469 cert_file=cert_file, 470 ca_certs=ca_certs, 471 verify_callback=verify_callback, 472 timeout=timeout) 473 else: 474 if url.startswith('http'): 475 h = HTTPConnection(host, 476 port=port, 477 timeout=timeout) 478 else: 479 if url.startswith('file:'): 480 url_ = url[5:] 481 try: 482 s = os.stat(url_) 483 if S_ISSOCK(s.st_mode): 484 h = FileHTTPConnection(url_) 485 local = True 486 else: 487 raise ConnectionError('File URL is not a socket: %s' % url) 488 except OSError as exc: 489 raise ConnectionError('Error with file URL %s: %s' % (url, exc)) 490 491 locallogin = None 492 if host in ('localhost', 'localhost6', '127.0.0.1', '::1'): 493 local = True 494 if local: 495 try: 496 locallogin = getpass.getuser() 497 except (KeyError, ImportError): 498 locallogin = None 499 500 with HTTPTimeout(timeout, h): 501 502 while numTries < tryLimit: 503 numTries = numTries + 1 504 505 h.putrequest('POST', '/cimom') 506 507 h.putheader('Content-type', 'application/xml; charset="utf-8"') 508 h.putheader('Content-length', str(len(data))) 509 if localAuthHeader is not None: 510 h.putheader(*localAuthHeader) 511 elif creds is not None: 512 h.putheader('Authorization', 'Basic %s' % 513 base64.encodestring( 514 '%s:%s' % 515 (creds[0], creds[1])).replace('\n', '')) 516 elif locallogin is not None: 517 h.putheader('PegasusAuthorization', 'Local "%s"' % locallogin) 518 519 for hdr in headers: 520 if isinstance(hdr, unicode): 521 hdr = hdr.encode('utf-8') 522 s = map(lambda x: string.strip(x), string.split(hdr, ":", 1)) 523 h.putheader(urllib.quote(s[0]), urllib.quote(s[1])) 524 525 try: 526 # See RFC 2616 section 8.2.2 527 # An http server is allowed to send back an error (presumably 528 # a 401), and close the connection without reading the entire 529 # request. A server may do this to protect itself from a DoS 530 # attack. 531 # 532 # If the server closes the connection during our h.send(), we 533 # will either get a socket exception 104 (TCP RESET), or a 534 # socket exception 32 (broken pipe). In either case, thanks 535 # to our fixed HTTPConnection classes, we'll still be able to 536 # retrieve the response so that we can read and respond to the 537 # authentication challenge. 538 539 try: 540 # endheaders() is the first method in this sequence that 541 # actually sends something to the server. 542 h.endheaders() 543 h.send(data) 544 except socket.error as exc: 545 # TODO: Verify these errno numbers on Windows vs. Linux 546 if exc[0] != 104 and exc[0] != 32: 547 raise ConnectionError("Socket error: %s" % exc) 548 549 response = h.getresponse() 550 551 if response.status != 200: 552 if response.status == 401: 553 if numTries >= tryLimit: 554 raise AuthError(response.reason) 555 if not local: 556 raise AuthError(response.reason) 557 authChal = response.getheader('WWW-Authenticate', '') 558 if 'openwbem' in response.getheader('Server', ''): 559 if 'OWLocal' not in authChal: 560 try: 561 uid = os.getuid() 562 except AttributeError: 563 raise ConnectionError( 564 "OWLocal authorization for OpenWbem "\ 565 "server not supported on %s platform "\ 566 "due to missing os.getuid()" %\ 567 platform.system()) 568 localAuthHeader = ('Authorization', 569 'OWLocal uid="%d"' % uid) 570 continue 571 else: 572 try: 573 nonceIdx = authChal.index('nonce=') 574 nonceBegin = authChal.index('"', nonceIdx) 575 nonceEnd = authChal.index('"', nonceBegin+1) 576 nonce = authChal[nonceBegin+1:nonceEnd] 577 cookieIdx = authChal.index('cookiefile=') 578 cookieBegin = authChal.index('"', cookieIdx) 579 cookieEnd = authChal.index('"', cookieBegin+1) 580 cookieFile = authChal[cookieBegin+1:cookieEnd] 581 f = open(cookieFile, 'r') 582 cookie = f.read().strip() 583 f.close() 584 localAuthHeader = ( 585 'Authorization', 586 'OWLocal nonce="%s", cookie="%s"' % \ 587 (nonce, cookie)) 588 continue 589 except: 590 localAuthHeader = None 591 continue 592 elif 'Local' in authChal: 593 try: 594 beg = authChal.index('"') + 1 595 end = authChal.rindex('"') 596 if end > beg: 597 file = authChal[beg:end] 598 fo = open(file, 'r') 599 cookie = fo.read().strip() 600 fo.close() 601 localAuthHeader = ( 602 'PegasusAuthorization', 603 'Local "%s:%s:%s"' % \ 604 (locallogin, file, cookie)) 605 continue 606 except ValueError: 607 pass 608 raise AuthError(response.reason) 609 610 cimerror_hdr = response.getheader('CIMError', None) 611 if cimerror_hdr is not None: 612 exc_str = 'CIMError: %s' % cimerror_hdr 613 pgerrordetail_hdr = response.getheader('PGErrorDetail', 614 None) 615 if pgerrordetail_hdr is not None: 616 exc_str += ', PGErrorDetail: %s' %\ 617 urllib.unquote(pgerrordetail_hdr) 618 raise ConnectionError(exc_str) 619 620 raise ConnectionError('HTTP error: %s' % response.reason) 621 622 body = response.read() 623 624 except httplib.BadStatusLine as exc: 625 # Background: BadStatusLine is documented to be raised only 626 # when strict=True is used (that is not the case here). 627 # However, httplib currently raises BadStatusLine also 628 # independent of strict when a keep-alive connection times out 629 # (e.g. because the server went down). 630 # See http://bugs.python.org/issue8450. 631 if exc.line is None or exc.line.strip().strip("'") in \ 632 ('', 'None'): 633 raise ConnectionError("The server closed the "\ 634 "connection without returning any data, or the "\ 635 "client timed out") 636 else: 637 raise ConnectionError("The server returned a bad "\ 638 "HTTP status line: %r" % exc.line) 639 except httplib.IncompleteRead as exc: 640 raise ConnectionError("HTTP incomplete read: %s" % exc) 641 except httplib.NotConnected as exc: 642 raise ConnectionError("HTTP not connected: %s" % exc) 643 except socket.error as exc: 644 raise ConnectionError("Socket error: %s" % exc) 645 except socket.sslerror as exc: 646 raise ConnectionError("SSL error: %s" % exc) 647 648 break 649 650 return body 651 652
653 -def get_object_header(obj):
654 """Return the HTTP header required to make a CIM operation request 655 using the given object. Return None if the object does not need 656 to have a header.""" 657 658 # Local namespacepath 659 660 if isinstance(obj, StringTypes): 661 return 'CIMObject: %s' % obj 662 663 # CIMLocalClassPath 664 665 if isinstance(obj, cim_obj.CIMClassName): 666 return 'CIMObject: %s:%s' % (obj.namespace, obj.classname) 667 668 # CIMInstanceName with namespace 669 670 if isinstance(obj, cim_obj.CIMInstanceName) and obj.namespace is not None: 671 return 'CIMObject: %s' % obj 672 673 raise TypeError('Don\'t know how to generate HTTP headers for %s' % obj)
674