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 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
56 """Exception base class for catching any HTTP transport related errors."""
57 pass
58
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
65 """This exception is raised when an authentication error (401) occurs."""
66 pass
67
69 """This exception is raised when the client times out."""
70 pass
71
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
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
102
103
104
105 self._timer = None
106 self._ts1 = None
107 self._shutdown = None
108
109 return
110
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
125
126
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
134
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
151 self._timer.cancel()
152 self._timer = threading.Timer(self._retrytime,
153 HTTPTimeout.timer_expired, [self])
154 self._timer.start()
155
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
182 default_port_https = 5989
183 default_ssl = False
184
185
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:
193 ssl = False
194 else:
195
196
197 ssl = default_ssl
198 hostport = url
199
200
201
202
203 m = hostport.find("/")
204 if m >= 0:
205 hostport = hostport[0:m]
206
207
208
209
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
219
220
221
222
223
224
225 m = re.match(r"^\[(.+?)(?:-(.+))?\]$", host)
226 if m:
227
228 host = m.group(1)
229 if m.group(2) != None:
230
231 host += "%" + m.group(2)
232
233 return host, port, ssl
234
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
357
358
359
360
361
362 if sys.version_info[0:2] >= (2, 7):
363
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
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
388
389
390
391
392
393
394
395
396
397 if False:
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
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
451
452
453
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
527
528
529
530
531
532
533
534
535
536
537
538
539 try:
540
541
542 h.endheaders()
543 h.send(data)
544 except socket.error as exc:
545
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
626
627
628
629
630
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
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
659
660 if isinstance(obj, StringTypes):
661 return 'CIMObject: %s' % obj
662
663
664
665 if isinstance(obj, cim_obj.CIMClassName):
666 return 'CIMObject: %s:%s' % (obj.namespace, obj.classname)
667
668
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