Package rhn :: Module rpclib
[hide private]
[frames] | no frames]

Source Code for Module rhn.rpclib

  1  # 
  2  # This module contains all the RPC-related functions the RHN code uses 
  3  # 
  4  # Copyright (c) 2005--2020 Red Hat, Inc. 
  5  # 
  6  # This software is licensed to you under the GNU General Public License, 
  7  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  8  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  9  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
 10  # along with this software; if not, see 
 11  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 12  # 
 13  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 14  # granted to use or replicate Red Hat trademarks that are incorporated 
 15  # in this software or its documentation. 
 16  # 
 17   
 18  __version__ = "$Revision$" 
 19   
 20  import socket 
 21  import re 
 22  import sys 
 23   
 24  from rhn import transports 
 25  from rhn.i18n import sstr 
 26  from rhn.UserDictCase import UserDictCase 
 27   
 28  try: # python2 
 29      import xmlrpclib 
 30      from types import ListType, TupleType, StringType, UnicodeType, DictType, DictionaryType 
 31      from urllib import splittype, splithost 
 32  except ImportError: # python3 
 33      import xmlrpc.client as xmlrpclib 
 34      ListType = list 
 35      TupleType = tuple 
 36      StringType = bytes 
 37      UnicodeType = str 
 38      DictType = dict 
 39      DictionaryType = dict 
 40      from urllib.parse import splittype, splithost 
 41   
 42  # Redirection handling 
 43   
 44  MAX_REDIRECTIONS = 5 
 45   
46 -def check_ipv6(n):
47 """ Returns true if n is IPv6 address, false otherwise. """ 48 try: 49 socket.inet_pton(socket.AF_INET6, n) 50 return True 51 except: 52 return False
53
54 -def split_host(hoststring):
55 """ Function used to split host information in an URL per RFC 2396 56 handle full hostname like user:passwd@host:port 57 """ 58 l = hoststring.split('@', 1) 59 host = None 60 port = None 61 user = None 62 passwd = None 63 64 if len(l) == 2: 65 hostport = l[1] 66 # userinfo present 67 userinfo = l[0].split(':', 1) 68 user = userinfo[0] 69 if len(userinfo) == 2: 70 passwd = userinfo[1] 71 else: 72 hostport = l[0] 73 74 # Now parse hostport 75 if hostport[0] == '[': 76 # IPv6 with port 77 host, port = re.split('(?<=\]):', hostport, 1) 78 host = host.lstrip('[').rstrip(']') 79 elif check_ipv6(hostport): 80 # just IPv6 81 host = hostport 82 else: 83 # IPv4 84 arr = hostport.split(':', 1) 85 host = arr[0] 86 if len(arr) == 2: 87 port = arr[1] 88 89 return (host, port, user, passwd)
90
91 -def get_proxy_info(proxy):
92 if proxy == None: 93 raise ValueError("Host string cannot be null") 94 95 arr = proxy.split('://', 1) 96 if len(arr) == 2: 97 # scheme found, strip it 98 proxy = arr[1] 99 100 return split_host(proxy)
101 102
103 -class MalformedURIError(IOError):
104 pass
105 106 107 # Originaly taken from xmlrpclib.ServerProxy, now changed most of the code
108 -class Server:
109 """uri [,options] -> a logical connection to an XML-RPC server 110 111 uri is the connection point on the server, given as 112 scheme://host/target. 113 114 The standard implementation always supports the "http" scheme. If 115 SSL socket support is available (Python 2.0), it also supports 116 "https". 117 118 If the target part and the slash preceding it are both omitted, 119 "/RPC2" is assumed. 120 121 The following options can be given as keyword arguments: 122 123 transport: a transport factory 124 encoding: the request encoding (default is UTF-8) 125 verbose: verbosity level 126 proxy: use an HTTP proxy 127 username: username for authenticated HTTP proxy 128 password: password for authenticated HTTP proxy 129 130 All 8-bit strings passed to the server proxy are assumed to use 131 the given encoding. 132 """ 133 134 # Default factories 135 _transport_class = transports.Transport 136 _transport_class_https = transports.SafeTransport 137 _transport_class_proxy = transports.ProxyTransport 138 _transport_class_https_proxy = transports.SafeProxyTransport
139 - def __init__(self, uri, transport=None, encoding=None, verbose=0, 140 proxy=None, username=None, password=None, refreshCallback=None, 141 progressCallback=None, timeout=None):
142 # establish a "logical" server connection 143 144 # 145 # First parse the proxy information if available 146 # 147 if proxy != None: 148 (ph, pp, pu, pw) = get_proxy_info(proxy) 149 150 if pp is not None: 151 proxy = "%s:%s" % (ph, pp) 152 else: 153 proxy = ph 154 155 # username and password will override whatever was passed in the 156 # URL 157 if pu is not None and username is None: 158 username = pu 159 160 if pw is not None and password is None: 161 password = pw 162 163 self._uri = sstr(uri) 164 self._refreshCallback = None 165 self._progressCallback = None 166 self._bufferSize = None 167 self._proxy = proxy 168 self._username = username 169 self._password = password 170 self._timeout = timeout 171 172 if len(__version__.split()) > 1: 173 self.rpc_version = __version__.split()[1] 174 else: 175 self.rpc_version = __version__ 176 177 self._reset_host_handler_and_type() 178 179 if transport is None: 180 self._allow_redirect = 1 181 transport = self.default_transport(self._type, proxy, username, 182 password, timeout) 183 else: 184 # 185 # dont allow redirect on unknow transports, that should be 186 # set up independantly 187 # 188 self._allow_redirect = 0 189 190 self._redirected = None 191 self.use_handler_path = 1 192 self._transport = transport 193 194 self._trusted_cert_files = [] 195 self._lang = None 196 197 self._encoding = encoding 198 self._verbose = verbose 199 200 self.set_refresh_callback(refreshCallback) 201 self.set_progress_callback(progressCallback) 202 203 # referer, which redirect us to new handler 204 self.send_handler=None 205 206 self._headers = UserDictCase()
207
208 - def default_transport(self, type, proxy=None, username=None, password=None, 209 timeout=None):
210 if proxy: 211 if type == 'https': 212 transport = self._transport_class_https_proxy(proxy, 213 proxyUsername=username, proxyPassword=password, timeout=timeout) 214 else: 215 transport = self._transport_class_proxy(proxy, 216 proxyUsername=username, proxyPassword=password, timeout=timeout) 217 else: 218 if type == 'https': 219 transport = self._transport_class_https(timeout=timeout) 220 else: 221 transport = self._transport_class(timeout=timeout) 222 return transport
223
224 - def allow_redirect(self, allow):
225 self._allow_redirect = allow
226
227 - def redirected(self):
228 if not self._allow_redirect: 229 return None 230 return self._redirected
231
232 - def set_refresh_callback(self, refreshCallback):
233 self._refreshCallback = refreshCallback 234 self._transport.set_refresh_callback(refreshCallback)
235
236 - def set_buffer_size(self, bufferSize):
237 self._bufferSize = bufferSize 238 self._transport.set_buffer_size(bufferSize)
239
240 - def set_progress_callback(self, progressCallback, bufferSize=16384):
241 self._progressCallback = progressCallback 242 self._transport.set_progress_callback(progressCallback, bufferSize)
243
244 - def _req_body(self, params, methodname):
245 return xmlrpclib.dumps(params, methodname, encoding=self._encoding)
246
247 - def get_response_headers(self):
248 if self._transport: 249 return self._transport.headers_in 250 return None
251
252 - def get_response_status(self):
253 if self._transport: 254 return self._transport.response_status 255 return None
256
257 - def get_response_reason(self):
258 if self._transport: 259 return self._transport.response_reason 260 return None
261
262 - def get_content_range(self):
263 """Returns a dictionary with three values: 264 length: the total length of the entity-body (can be None) 265 first_byte_pos: the position of the first byte (zero based) 266 last_byte_pos: the position of the last byte (zero based) 267 The range is inclusive; that is, a response 8-9/102 means two bytes 268 """ 269 headers = self.get_response_headers() 270 if not headers: 271 return None 272 content_range = headers.get('Content-Range') 273 if not content_range: 274 return None 275 arr = filter(None, content_range.split()) 276 assert arr[0] == "bytes" 277 assert len(arr) == 2 278 arr = arr[1].split('/') 279 assert len(arr) == 2 280 281 brange, total_len = arr 282 if total_len == '*': 283 # Per RFC, the server is allowed to use * if the length of the 284 # entity-body is unknown or difficult to determine 285 total_len = None 286 else: 287 total_len = int(total_len) 288 289 start, end = brange.split('-') 290 result = { 291 'length' : total_len, 292 'first_byte_pos' : int(start), 293 'last_byte_pos' : int(end), 294 } 295 return result
296
297 - def accept_ranges(self):
298 headers = self.get_response_headers() 299 if not headers: 300 return None 301 if 'Accept-Ranges' in headers: 302 return headers['Accept-Ranges'] 303 return None
304
306 """ Reset the attributes: 307 self._host, self._handler, self._type 308 according the value of self._uri. 309 """ 310 # get the url 311 type, uri = splittype(self._uri) 312 if type is None: 313 raise MalformedURIError("missing protocol in uri") 314 # with a real uri passed in, uri will now contain "//hostname..." so we 315 # need at least 3 chars for it to maybe be ok... 316 if len(uri) < 3 or uri[0:2] != "//": 317 raise MalformedURIError 318 self._type = type.lower() 319 if self._type not in ("http", "https"): 320 raise IOError("unsupported XML-RPC protocol") 321 self._host, self._handler = splithost(uri) 322 if not self._handler: 323 self._handler = "/RPC2"
324
325 - def _strip_characters(self, *args):
326 """ Strip characters, which are not allowed according: 327 http://www.w3.org/TR/2006/REC-xml-20060816/#charsets 328 From spec: 329 Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */ 330 """ 331 regexp = r'[\x00-\x09]|[\x0b-\x0c]|[\x0e-\x1f]' 332 result=[] 333 for item in args: 334 item_type = type(item) 335 if item_type == StringType or item_type == UnicodeType: 336 item = re.sub(regexp, '', sstr(item)) 337 elif item_type == TupleType: 338 item = tuple(self._strip_characters(i) for i in item) 339 elif item_type == ListType: 340 item = [self._strip_characters(i) for i in item] 341 elif item_type == DictType or item_type == DictionaryType: 342 item = dict([(self._strip_characters(name, val)) for name, val in item.items()]) 343 # else: some object - should take care of himself 344 # numbers - are safe 345 result.append(item) 346 if len(result) == 1: 347 return result[0] 348 else: 349 return tuple(result)
350
351 - def _request(self, methodname, params):
352 """ Call a method on the remote server 353 we can handle redirections. """ 354 # the loop is used to handle redirections 355 redirect_response = 0 356 retry = 0 357 358 self._reset_host_handler_and_type() 359 360 while 1: 361 if retry >= MAX_REDIRECTIONS: 362 raise InvalidRedirectionError( 363 "Unable to fetch requested Package") 364 365 # Clear the transport headers first 366 self._transport.clear_headers() 367 for k, v in self._headers.items(): 368 self._transport.set_header(k, v) 369 370 self._transport.add_header("X-Info", 371 'RPC Processor (C) Red Hat, Inc (version %s)' % 372 self.rpc_version) 373 # identify the capability set of this client to the server 374 self._transport.set_header("X-Client-Version", 1) 375 376 if self._allow_redirect: 377 # Advertise that we follow redirects 378 #changing the version from 1 to 2 to support backward compatibility 379 self._transport.add_header("X-RHN-Transport-Capability", 380 "follow-redirects=3") 381 382 if redirect_response: 383 self._transport.add_header('X-RHN-Redirect', '0') 384 if self.send_handler: 385 self._transport.add_header('X-RHN-Path', self.send_handler) 386 387 request = self._req_body(self._strip_characters(params), methodname) 388 389 try: 390 response = self._transport.request(self._host, \ 391 self._handler, request, verbose=self._verbose) 392 save_response = self._transport.response_status 393 except xmlrpclib.ProtocolError: 394 if self.use_handler_path: 395 raise 396 else: 397 save_response = sys.exc_info()[1].errcode 398 399 self._redirected = None 400 retry += 1 401 if save_response == 200: 402 # exit redirects loop and return response 403 break 404 elif save_response not in (301, 302): 405 # Retry pkg fetch 406 self.use_handler_path = 1 407 continue 408 # rest of loop is run only if we are redirected (301, 302) 409 self._redirected = self._transport.redirected() 410 self.use_handler_path = 0 411 redirect_response = 1 412 413 if not self._allow_redirect: 414 raise InvalidRedirectionError("Redirects not allowed") 415 416 if self._verbose: 417 print("%s redirected to %s" % (self._uri, self._redirected)) 418 419 typ, uri = splittype(self._redirected) 420 421 if typ != None: 422 typ = typ.lower() 423 if typ not in ("http", "https"): 424 raise InvalidRedirectionError( 425 "Redirected to unsupported protocol %s" % typ) 426 427 # 428 # We forbid HTTPS -> HTTP for security reasons 429 # Note that HTTP -> HTTPS -> HTTP is allowed (because we compare 430 # the protocol for the redirect with the original one) 431 # 432 if self._type == "https" and typ == "http": 433 raise InvalidRedirectionError( 434 "HTTPS redirected to HTTP is not supported") 435 436 self._host, self._handler = splithost(uri) 437 if not self._handler: 438 self._handler = "/RPC2" 439 440 # Create a new transport for the redirected service and 441 # set up the parameters on the new transport 442 del self._transport 443 self._transport = self.default_transport(typ, self._proxy, 444 self._username, self._password, self._timeout) 445 self.set_progress_callback(self._progressCallback) 446 self.set_refresh_callback(self._refreshCallback) 447 self.set_buffer_size(self._bufferSize) 448 self.setlang(self._lang) 449 450 if self._trusted_cert_files != [] and \ 451 hasattr(self._transport, "add_trusted_cert"): 452 for certfile in self._trusted_cert_files: 453 self._transport.add_trusted_cert(certfile) 454 # Then restart the loop to try the new entry point. 455 456 if isinstance(response, transports.File): 457 # Just return the file 458 return response 459 460 # an XML-RPC encoded data structure 461 if isinstance(response, TupleType) and len(response) == 1: 462 response = response[0] 463 464 return response
465
466 - def __repr__(self):
467 return ( 468 "<%s for %s%s>" % 469 (self.__class__.__name__, self._host, self._handler) 470 )
471 472 __str__ = __repr__ 473
474 - def __getattr__(self, name):
475 # magic method dispatcher 476 return _Method(self._request, name)
477 478 # note: to call a remote object with an non-standard name, use 479 # result getattr(server, "strange-python-name")(args) 480
481 - def set_transport_flags(self, transfer=0, encoding=0, **kwargs):
482 if not self._transport: 483 # Nothing to do 484 return 485 kwargs.update({ 486 'transfer' : transfer, 487 'encoding' : encoding, 488 }) 489 self._transport.set_transport_flags(**kwargs)
490
491 - def get_transport_flags(self):
492 if not self._transport: 493 # Nothing to do 494 return {} 495 return self._transport.get_transport_flags()
496
497 - def reset_transport_flags(self):
498 # Does nothing 499 pass
500 501 # Allow user-defined additional headers.
502 - def set_header(self, name, arg):
503 if type(arg) in [ type([]), type(()) ]: 504 # Multivalued header 505 self._headers[name] = [str(a) for a in arg] 506 else: 507 self._headers[name] = str(arg)
508
509 - def add_header(self, name, arg):
510 if name in self._headers: 511 vlist = self._headers[name] 512 if not isinstance(vlist, ListType): 513 vlist = [ vlist ] 514 else: 515 vlist = self._headers[name] = [] 516 vlist.append(str(arg))
517 518 # Sets the i18n options
519 - def setlang(self, lang):
520 self._lang = lang 521 if self._transport and hasattr(self._transport, "setlang"): 522 self._transport.setlang(lang)
523 524 # Sets the CA chain to be used
525 - def use_CA_chain(self, ca_chain = None):
526 raise NotImplementedError("This method is deprecated")
527
528 - def add_trusted_cert(self, certfile):
529 self._trusted_cert_files.append(certfile) 530 if self._transport and hasattr(self._transport, "add_trusted_cert"): 531 self._transport.add_trusted_cert(certfile)
532
533 - def close(self):
534 if self._transport: 535 self._transport.close() 536 self._transport = None
537 538 # RHN GET server
539 -class GETServer(Server):
540 - def __init__(self, uri, transport=None, proxy=None, username=None, 541 password=None, client_version=2, headers={}, refreshCallback=None, 542 progressCallback=None, timeout=None):
543 Server.__init__(self, uri, 544 proxy=proxy, 545 username=username, 546 password=password, 547 transport=transport, 548 refreshCallback=refreshCallback, 549 progressCallback=progressCallback, 550 timeout=timeout) 551 self._client_version = client_version 552 self._headers = headers 553 # Back up the original handler, since we mangle it 554 self._orig_handler = self._handler 555 # Download resumption 556 self.set_range(offset=None, amount=None)
557
558 - def _req_body(self, params, methodname):
559 if not params or len(params) < 1: 560 raise Exception("Required parameter channel not found") 561 # Strip the multiple / from the handler 562 h_comps = filter(lambda x: x != '', self._orig_handler.split('/')) 563 # Set the handler we are going to request 564 hndl = h_comps + ["$RHN", params[0], methodname] + list(params[1:]) 565 self._handler = '/' + '/'.join(hndl) 566 567 #save the constructed handler in case of redirect 568 self.send_handler = self._handler 569 570 # Add headers 571 #override the handler to replace /XMLRPC with pkg path 572 if self._redirected and not self.use_handler_path: 573 self._handler = self._new_req_body() 574 575 for h, v in self._headers.items(): 576 self._transport.set_header(h, v) 577 578 if self._offset is not None: 579 if self._offset >= 0: 580 brange = str(self._offset) + '-' 581 if self._amount is not None: 582 brange = brange + str(self._offset + self._amount - 1) 583 else: 584 # The last bytes 585 # amount is ignored in this case 586 brange = '-' + str(-self._offset) 587 588 self._transport.set_header('Range', "bytes=" + brange) 589 # Flag that we allow for partial content 590 self._transport.set_transport_flags(allow_partial_content=1) 591 # GET requests have empty body 592 return ""
593
594 - def _new_req_body(self):
595 type, tmpuri = splittype(self._redirected) 596 site, handler = splithost(tmpuri) 597 return handler
598
599 - def set_range(self, offset=None, amount=None):
600 if offset is not None: 601 try: 602 offset = int(offset) 603 except ValueError: 604 # Error 605 raise RangeError("Invalid value `%s' for offset" % offset, None, sys.exc_info()[2]) 606 607 if amount is not None: 608 try: 609 amount = int(amount) 610 except ValueError: 611 # Error 612 raise RangeError("Invalid value `%s' for amount" % amount, None, sys.exc_info()[2]) 613 614 if amount <= 0: 615 raise RangeError("Invalid value `%s' for amount" % amount) 616 617 self._amount = amount 618 self._offset = offset
619
620 - def reset_transport_flags(self):
621 self._transport.set_transport_flags(allow_partial_content=0)
622
623 - def __getattr__(self, name):
624 # magic method dispatcher 625 return SlicingMethod(self._request, name)
626
627 - def default_transport(self, type, proxy=None, username=None, password=None, 628 timeout=None):
629 ret = Server.default_transport(self, type, proxy=proxy, username=username, password=password, timeout=timeout) 630 ret.set_method("GET") 631 return ret
632
633 -class RangeError(Exception):
634 pass
635
636 -class InvalidRedirectionError(Exception):
637 pass
638
639 -def getHeaderValues(headers, name):
640 import mimetools 641 if not isinstance(headers, mimetools.Message): 642 if name in headers: 643 return [headers[name]] 644 return [] 645 646 return [x.split(':', 1)[1].strip() for x in 647 headers.getallmatchingheaders(name)]
648
649 -class _Method:
650 """ some magic to bind an XML-RPC method to an RPC server. 651 supports "nested" methods (e.g. examples.getStateName) 652 """
653 - def __init__(self, send, name):
654 self._send = send 655 self._name = name
656 - def __getattr__(self, name):
657 return _Method(self._send, "%s.%s" % (self._name, name))
658 - def __call__(self, *args):
659 return self._send(self._name, args)
660 - def __repr__(self):
661 return ( 662 "<%s %s (%s)>" % 663 (self.__class__.__name__, self._name, self._send) 664 )
665 __str__ = __repr__
666 667
668 -class SlicingMethod(_Method):
669 """ 670 A "slicing method" allows for byte range requests 671 """
672 - def __init__(self, send, name):
673 _Method.__init__(self, send, name) 674 self._offset = None
675 - def __getattr__(self, name):
676 return SlicingMethod(self._send, "%s.%s" % (self._name, name))
677 - def __call__(self, *args, **kwargs):
678 self._offset = kwargs.get('offset') 679 self._amount = kwargs.get('amount') 680 681 # im_self is a pointer to self, so we can modify the class underneath 682 try: 683 self._send.im_self.set_range(offset=self._offset, 684 amount=self._amount) 685 except AttributeError: 686 pass 687 688 result = self._send(self._name, args) 689 690 # Reset "sticky" transport flags 691 try: 692 self._send.im_self.reset_transport_flags() 693 except AttributeError: 694 pass 695 696 return result
697 698
699 -def reportError(headers):
700 """ Reports the error from the headers. """ 701 errcode = 0 702 errmsg = "" 703 s = "X-RHN-Fault-Code" 704 if s in headers: 705 errcode = int(headers[s]) 706 s = "X-RHN-Fault-String" 707 if s in headers: 708 _sList = getHeaderValues(headers, s) 709 if _sList: 710 _s = ''.join(_sList) 711 import base64 712 errmsg = "%s" % base64.decodestring(_s) 713 714 return errcode, errmsg
715