Package backend :: Package server :: Module apacheRequest
[hide private]
[frames] | no frames]

Source Code for Module backend.server.apacheRequest

  1  # 
  2  # Copyright (c) 2008--2016 Red Hat, Inc. 
  3  # 
  4  # This software is licensed to you under the GNU General Public License, 
  5  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  6  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
  8  # along with this software; if not, see 
  9  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 10  # 
 11  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 12  # granted to use or replicate Red Hat trademarks that are incorporated 
 13  # in this software or its documentation. 
 14  # 
 15  # This module implements requests handlers for GET and POST methods. 
 16  # 
 17   
 18  # system modules 
 19  import sys 
 20  import base64 
 21  import string 
 22  try: 
 23      #  python 2 
 24      import xmlrpclib 
 25  except ImportError: 
 26      #  python3 
 27      import xmlrpc.client as xmlrpclib 
 28  from rhn.rpclib import transports 
 29   
 30  # common modules 
 31  from spacewalk.common.usix import raise_with_tb 
 32  from spacewalk.common import apache, rhnFlags 
 33  from spacewalk.common.rhnConfig import CFG 
 34  from spacewalk.common import byterange 
 35  from spacewalk.common.rhnLog import log_debug, log_error 
 36  from spacewalk.common.rhnException import rhnFault, rhnNotFound,\ 
 37      redirectException  # to catch redirect exception 
 38  from spacewalk.common.rhnTranslate import _ 
 39  from spacewalk.common.rhnLib import setHeaderValue 
 40  from spacewalk.common.rhnTB import Traceback 
 41   
 42  # local modules 
 43  import rhnRepository 
 44  import rhnImport 
 45  import rhnSQL 
 46  import rhnCapability 
 47  import apacheAuth 
 48   
 49  # Exceptions 
 50   
 51   
52 -class UnknownXML(Exception):
53
54 - def __init__(self, value):
55 Exception.__init__(self) 56 self.__value = value
57
58 - def __repr__(self):
59 return _("Invalid request received (%s).") % self.__value
60 __str__ = __repr__
61 62
63 -class HandlerNotFoundError(Exception):
64 pass
65 66 # base class for requests 67 68
69 -class apacheRequest:
70
71 - def __init__(self, client_version, req):
72 self.client = client_version 73 self.req = req 74 # grab an Input object 75 self.input = transports.Input(req.headers_in) 76 # make sure we have a parser and a decoder available 77 self.parser, self.decoder = xmlrpclib.getparser() 78 # Make sure the decoder doesn't assume UTF-8 data, that would break if 79 # non-UTF-8 chars are sent (bug 139370) 80 self.decoder._encoding = None 81 82 # extract the server we're talking to and the root directory 83 # from the request configuration options 84 req_config = req.get_options() 85 # XXX: attempt to catch these KeyErrors sometime when there is 86 # time to play nicely 87 self.server = req_config["SERVER"] 88 # Load the server classes 89 # XXX: some day we're going to trust the timestamp stuff... 90 self.servers = None 91 self._setup_servers()
92
93 - def _setup_servers(self):
94 self.servers = rhnImport.load("server/handlers", 95 interface_signature='rpcClasses')
96 97 # return a reference to a method name. The method in the base
98 - def method_ref(self, method):
99 raise UnknownXML("Could not find reference definition" 100 "for method '%s'" % method)
101 102 # call a function with parameters
103 - def call_function(self, method, params):
104 # short-circuit everything if sending a system-wide message. 105 if CFG.SEND_MESSAGE_TO_ALL: 106 # Make sure the applet doesn't see the message 107 if method == 'applet.poll_status': 108 return self.response({ 109 'checkin_interval': 3600, 110 'server_status': 'normal' 111 }) 112 if method == 'applet.poll_packages': 113 return self.response({'use_cached_copy': 1}) 114 115 # Fetch global message being sent to clients if applicable. 116 msg = open(CFG.MESSAGE_TO_ALL).read() 117 log_debug(3, "Sending message to all clients: %s" % msg) 118 # Send the message as a fault. 119 response = xmlrpclib.Fault( 120 -1, _("IMPORTANT MESSAGE FOLLOWS:\n%s") % msg) 121 # and now send everything back 122 ret = self.response(response) 123 log_debug(4, "Leave with return value", ret) 124 return ret 125 126 # req: where the response is sent to 127 log_debug(2, method) 128 129 # Now we have the reference, call away 130 force_rollback = 1 131 try: 132 rhnSQL.clear_log_id() 133 # now get the function reference and call it 134 func = self.method_ref(method) 135 response = func(*params) 136 except (TypeError, ValueError, KeyError, IndexError, UnknownXML): 137 # report exception back to server 138 fault = 1 139 140 if sys.version_info[0] == 3: 141 exctype = sys.exc_info()[0] 142 else: 143 exctype = sys.exc_type 144 145 if exctype == UnknownXML: 146 fault = -1 147 e_type, e_value = sys.exc_info()[:2] 148 response = xmlrpclib.Fault(fault, _( 149 "While running '%s': caught\n%s : %s\n") % ( 150 method, e_type, e_value)) 151 Traceback(method, self.req, 152 extra="Response sent back to the caller:\n%s\n" % ( 153 response.faultString,), 154 severity="notification") 155 except rhnNotFound: 156 e = sys.exc_info()[1] 157 return apache.HTTP_NOT_FOUND 158 # pkilambi:catch exception if redirect 159 except redirectException: 160 re = sys.exc_info()[1] 161 log_debug(3, "redirect exception caught", re.path) 162 response = re.path 163 164 except rhnFault: 165 f = sys.exc_info()[1] 166 response = f.getxml() 167 except rhnSQL.SQLSchemaError: 168 e = sys.exc_info()[1] 169 f = None 170 if e.errno == 20200: 171 log_debug(2, "User Group Membership EXCEEDED") 172 f = rhnFault(43, e.errmsg) 173 if not f: 174 log_error("rhnSQL.SQLSchemaError caught", e) 175 rhnSQL.rollback() 176 # generate the traceback report 177 Traceback(method, self.req, 178 extra="SQL Error generated: %s" % e, 179 severity="schema") 180 return apache.HTTP_INTERNAL_SERVER_ERROR 181 response = f.getxml() 182 except rhnSQL.SQLError: 183 e = sys.exc_info()[1] 184 log_error("rhnSQL.SQLError caught", e) 185 rhnSQL.rollback() 186 Traceback(method, self.req, 187 extra="SQL Error generated: %s" % e, 188 severity="schema") 189 return apache.HTTP_INTERNAL_SERVER_ERROR 190 except Exception: 191 e = sys.exc_info()[1] 192 log_error("Unhandled exception", e) 193 rhnSQL.rollback() 194 # otherwise we do a full stop 195 Traceback(method, self.req, severity="unhandled") 196 return apache.HTTP_INTERNAL_SERVER_ERROR 197 else: 198 # if no exception, we don't need to rollback 199 force_rollback = 0 200 if force_rollback: 201 rhnSQL.rollback() 202 rhnSQL.clear_log_id() 203 # and now send everything back 204 ret = self.response(response) 205 log_debug(4, "Leave with return value", ret) 206 return ret
207 208 # process the request
209 - def process(self):
210 # this is just a stub we'd better override 211 return apache.HTTP_NOT_IMPLEMENTED
212 213 # convert a response to the right type for passing back to 214 # rpclib.xmlrpclib.dumps
215 - def normalize(self, response):
216 if isinstance(response, xmlrpclib.Fault): 217 return response 218 return (response,)
219 220 # send a file out
221 - def response_file(self, response):
222 log_debug(3, response.name) 223 # We may set the content type remotely 224 if rhnFlags.test("Content-Type"): 225 self.req.content_type = rhnFlags.get("Content-Type") 226 else: 227 # Safe default 228 self.req.content_type = "application/octet-stream" 229 230 # find out the size of the file 231 if response.length == 0: 232 response.file_obj.seek(0, 2) 233 file_size = response.file_obj.tell() 234 response.file_obj.seek(0, 0) 235 else: 236 file_size = response.length 237 238 success_response = apache.OK 239 response_size = file_size 240 241 # Respond to if-modified-since requests 242 if ("If-Modified-Since" in self.req.headers_in and 243 "Last-Modified" in rhnFlags.get("outputTransportOptions") and 244 rhnFlags.get("outputTransportOptions")['Last-Modified'] == self.req.headers_in['If-Modified-Since']): 245 return apache.HTTP_NOT_MODIFIED 246 247 # Serve up the requested byte range 248 if "Range" in self.req.headers_in: 249 try: 250 range_start, range_end = \ 251 byterange.parse_byteranges(self.req.headers_in["Range"], 252 file_size) 253 response_size = range_end - range_start 254 self.req.headers_out["Content-Range"] = \ 255 byterange.get_content_range(range_start, range_end, file_size) 256 self.req.headers_out["Accept-Ranges"] = "bytes" 257 258 response.file_obj.seek(range_start) 259 260 # We'll want to send back a partial content rather than ok 261 # if this works 262 self.req.status = apache.HTTP_PARTIAL_CONTENT 263 success_response = apache.HTTP_PARTIAL_CONTENT 264 265 # For now we will just return the file file on the following exceptions 266 except byterange.InvalidByteRangeException: 267 pass 268 except byterange.UnsatisfyableByteRangeException: 269 pass 270 271 self.req.headers_out["Content-Length"] = str(response_size) 272 273 # if we loaded this from a real fd, set it as the X-Replace-Content 274 # check for "name" since sometimes we get xmlrpclib.transports.File's that have 275 # a stringIO as the file_obj, and they dont have a .name (ie, 276 # fileLists...) 277 if response.name: 278 self.req.headers_out["X-Package-FileName"] = response.name 279 280 xrepcon = "X-Replace-Content-Active" in self.req.headers_in \ 281 and rhnFlags.test("Download-Accelerator-Path") 282 if xrepcon: 283 fpath = rhnFlags.get("Download-Accelerator-Path") 284 log_debug(1, "Serving file %s" % fpath) 285 self.req.headers_out["X-Replace-Content"] = fpath 286 # Only set a byte rate if xrepcon is active 287 byte_rate = rhnFlags.get("QOS-Max-Bandwidth") 288 if byte_rate: 289 self.req.headers_out["X-Replace-Content-Throttle"] = str(byte_rate) 290 291 # send the headers 292 self.req.send_http_header() 293 294 if "Range" in self.req.headers_in: 295 # and the file 296 read = 0 297 while read < response_size: 298 # We check the size here in case we're not asked for the entire file. 299 if (read + CFG.BUFFER_SIZE > response_size): 300 to_read = read + CFG.BUFFER_SIZE - response_size 301 else: 302 to_read = CFG.BUFFER_SIZE 303 buf = response.read(CFG.BUFFER_SIZE) 304 if not buf: 305 break 306 try: 307 self.req.write(buf) 308 read = read + CFG.BUFFER_SIZE 309 except IOError: 310 if xrepcon: 311 # We're talking to a proxy, so don't bother to report 312 # a SIGPIPE 313 break 314 return apache.HTTP_BAD_REQUEST 315 response.close() 316 else: 317 if 'wsgi.file_wrapper' in self.req.headers_in: 318 self.req.output = self.req.headers_in['wsgi.file_wrapper'](response, CFG.BUFFER_SIZE) 319 else: 320 self.req.output = iter(lambda: response.read(CFG.BUFFER_SIZE), '') 321 322 return success_response
323 324 # send the response (common code)
325 - def response(self, response):
326 # Send the xml-rpc response back 327 log_debug(3, type(response)) 328 needs_xmlrpc_encoding = not rhnFlags.test("XMLRPC-Encoded-Response") 329 compress_response = rhnFlags.test("compress_response") 330 # Init an output object; we'll use it for sending data in various 331 # formats 332 if isinstance(response, transports.File): 333 if not hasattr(response.file_obj, 'fileno') and compress_response: 334 # This is a StringIO that has to be compressed, so read it in 335 # memory; mark that we don't have to do any xmlrpc encoding 336 response = response.file_obj.read() 337 needs_xmlrpc_encoding = 0 338 else: 339 # Just treat is as a file 340 return self.response_file(response) 341 342 output = transports.Output() 343 344 # First, use the same encoding/transfer that the client used 345 output.set_transport_flags( 346 transfer=transports.lookupTransfer(self.input.transfer), 347 encoding=transports.lookupEncoding(self.input.encoding)) 348 349 if isinstance(response, xmlrpclib.Fault): 350 log_debug(4, "Return FAULT", 351 response.faultCode, response.faultString) 352 # No compression for faults because we'd like them to pop 353 # up in clear text on the other side just in case 354 output.set_transport_flags(output.TRANSFER_NONE, output.ENCODE_NONE) 355 elif compress_response: 356 # check if we have to compress this result 357 log_debug(4, "Compression on for client version", self.client) 358 if self.client > 0: 359 output.set_transport_flags(output.TRANSFER_BINARY, 360 output.ENCODE_ZLIB) 361 else: # original clients had the binary transport support broken 362 output.set_transport_flags(output.TRANSFER_BASE64, 363 output.ENCODE_ZLIB) 364 365 # We simply add the transport options to the output headers 366 output.headers.update(rhnFlags.get('outputTransportOptions').dict()) 367 368 if needs_xmlrpc_encoding: 369 # Normalize the response 370 response = self.normalize(response) 371 try: 372 response = xmlrpclib.dumps(response, methodresponse=1) 373 except TypeError: 374 e = sys.exc_info()[1] 375 log_debug(4, "Error \"%s\" encoding response = %s" % (e, response)) 376 Traceback("apacheHandler.response", self.req, 377 extra="Error \"%s\" encoding response = %s" % (e, response), 378 severity="notification") 379 return apache.HTTP_INTERNAL_SERVER_ERROR 380 except: 381 # Uncaught exception; signal the error 382 Traceback("apacheHandler.response", self.req, 383 severity="unhandled") 384 return apache.HTTP_INTERNAL_SERVER_ERROR 385 386 # we're about done here, patch up the headers 387 output.process(response) 388 # Copy the rest of the fields 389 for k, v in output.headers.items(): 390 if string.lower(k) == 'content-type': 391 # Content-type 392 self.req.content_type = v 393 else: 394 setHeaderValue(self.req.headers_out, k, v) 395 396 if 5 <= CFG.DEBUG < 10: 397 log_debug(5, "The response: %s[...SNIP (for sanity) SNIP...]%s" % (response[:100], response[-100:])) 398 elif CFG.DEBUG >= 10: 399 # if you absolutely must have that whole response in the log file 400 log_debug(10, "The response: %s" % response) 401 402 # send the headers 403 self.req.send_http_header() 404 try: 405 # XXX: in case data is really large maybe we should split 406 # it in smaller chunks instead of blasting everything at 407 # once. Not yet a problem... 408 self.req.write(output.data) 409 except IOError: 410 # send_http_header is already sent, so it doesn't make a lot of 411 # sense to return a non-200 error; but there is no better solution 412 return apache.HTTP_BAD_REQUEST 413 del output 414 return apache.OK
415
416 - def auth_client(self):
417 return apacheAuth.auth_client()
418
419 - def auth_proxy(self):
420 return apacheAuth.auth_proxy()
421 422 # handles the POST requests 423 424
425 -class apachePOST(apacheRequest):
426 # Decode the request. Returns a tuple of (params, methodName). 427
428 - def decode(self, data):
429 try: 430 self.parser.feed(data) 431 except IndexError: 432 # malformed XML data 433 raise_with_tb(xmlrpclib.ResponseError, sys.exc_info()[2]) 434 435 self.parser.close() 436 # extract the method and arguments; we pass the exceptions through 437 params = self.decoder.close() 438 method = self.decoder.getmethodname() 439 return params, method
440 441 # get the function reference for the POST request
442 - def method_ref(self, method):
443 # Execute the right function (from xml-rpc request) in the right class. 444 # NOTE: All functions should do their own logging 445 log_debug(3, self.server, method) 446 if method[-8:] == '.__str__': 447 # Ignore these, they are just some code trying to stringify an 448 # XML-RPC function 449 log_error("Ignoring call for method", method) 450 raise rhnFault(-1, "Ignoring call for a __str__ method", explain=0) 451 if self.server is None: 452 raise UnknownXML("Method `%s' is not bound to a server " 453 "(server = %s)" % (method, self.server)) 454 classes = self.servers[self.server] 455 if classes is None: 456 raise UnknownXML("Server %s is not a valid XML-RPC receiver" % 457 (self.server,)) 458 459 try: 460 classname, funcname = string.split(method, '.', 1) 461 except: 462 raise_with_tb(UnknownXML("method '%s' doesn't have a class and function" % 463 (method,)), sys.exc_info()[2]) 464 if not classname or not funcname: 465 raise UnknownXML(method) 466 467 log_debug(4, "Class name: %s; function name: %s" % (classname, 468 funcname)) 469 c = classes.get(classname) 470 if c is None: 471 raise UnknownXML("class %s.%s is not defined (function = %s)" % ( 472 self.server, classname, funcname)) 473 474 # Initialize the handlers object 475 serverHandlers = c() 476 # we need this for sat handler 477 serverHandlers.remote_hostname = self.req.get_remote_host(apache.REMOTE_DOUBLE_REV) 478 f = serverHandlers.get_function(funcname) 479 if f is None: 480 raise UnknownXML("function: %s invalid" % (method,)) 481 # Send the client this server's capabilities 482 rhnCapability.set_server_capabilities() 483 return f
484 485 # handle the POST requests
486 - def process(self):
487 log_debug(3) 488 # nice thing that req has a read() method, so it makes it look just 489 # like an fd 490 try: 491 fd = self.input.decode(self.req) 492 except IOError: # client timed out 493 return apache.HTTP_BAD_REQUEST 494 495 # Read the data from the request 496 _body = fd.read() 497 fd.close() 498 499 # In this case, we talk to a client (maybe through a proxy) 500 # make sure we have something to decode 501 if _body is None or len(_body) == 0: 502 return apache.HTTP_BAD_REQUEST 503 504 # Decode the request; avoid logging crappy responses 505 try: 506 params, method = self.decode(_body) 507 except xmlrpclib.ResponseError: 508 log_error("Got bad XML-RPC blob of len = %d" % len(_body)) 509 return apache.HTTP_BAD_REQUEST 510 else: 511 if params is None: 512 params = () 513 # make the actual function call and return the result 514 return self.call_function(method, params)
515 516
517 -class apacheGET:
518
519 - def __init__(self, client_version, req):
520 # extract the server we're talking to and the root directory 521 # from the request configuration options 522 req_config = req.get_options() 523 self.server = req_config["SERVER"] 524 # XXX: some day we're going to trust the timestamp stuff... 525 self.handler_classes = rhnImport.load("server/handlers", 526 interface_signature='getHandler') 527 log_debug(3, "Handler classes", self.handler_classes) 528 529 self.handler = None 530 if self.server not in self.handler_classes: 531 raise HandlerNotFoundError(self.server) 532 533 handler_class = self.handler_classes[self.server] 534 if handler_class is None: 535 # Was set just so that we make the logs quiet 536 raise HandlerNotFoundError(self.server) 537 log_debug(3, "Handler class", handler_class, type(handler_class)) 538 self.handler = handler_class(client_version, req)
539
540 - def __getattr__(self, name):
541 return getattr(self.handler, name)
542 543
544 -class GetHandler(apacheRequest):
545 # we require our own init since we depend on a channel 546
547 - def __init__(self, client_version, req):
548 apacheRequest.__init__(self, client_version, req) 549 self.channel = None
550
551 - def _setup_servers(self):
552 # Nothing to do here 553 pass
554 555 # get a function reference for the GET request
556 - def method_ref(self, method):
557 log_debug(3, self.server, method) 558 559 # Init the repository 560 server_id = rhnFlags.get("AUTH_SESSION_TOKEN")['X-RHN-Server-Id'] 561 username = rhnFlags.get("AUTH_SESSION_TOKEN")['X-RHN-Auth-User-Id'] 562 repository = rhnRepository.Repository(self.channel, server_id, 563 username) 564 repository.set_qos() 565 meth = method.replace('.', '_') 566 f = repository.get_function(meth) 567 if f is None: 568 raise UnknownXML("function '%s' invalid; path_info is %s" % ( 569 method, self.req.path_info)) 570 return f
571 572 # handle the GET requests
573 - def process(self):
574 log_debug(3) 575 # Query repository; only after a clients signature has been 576 # authenticated. 577 578 try: 579 method, params = self._get_method_params() 580 except rhnFault: 581 f = sys.exc_info()[1] 582 log_debug(2, "Fault caught") 583 response = f.getxml() 584 self.response(response) 585 return apache.HTTP_NOT_FOUND 586 except Exception: 587 e = sys.exc_info()[1] 588 rhnSQL.rollback() 589 # otherwise we do a full stop 590 Traceback(method, self.req, severity="unhandled") 591 return apache.HTTP_INTERNAL_SERVER_ERROR 592 # make the actual function call and return the result 593 return self.call_function(method, params)
594
595 - def _get_method_params(self):
596 # Returns the method name and params for this call 597 598 # Split the request into parts 599 array = string.split(self.req.path_info, '/') 600 if len(array) < 4: 601 log_error("Invalid URI for GET request", self.req.path_info) 602 raise rhnFault(21, _("Invalid URI %s" % self.req.path_info)) 603 604 self.channel, method = (array[2], array[3]) 605 params = tuple(array[4:]) 606 return method, params
607 608 # send the response out for the GET requests
609 - def response(self, response):
610 log_debug(3) 611 # pkilambi:if redirectException caught returns path(<str>) 612 if isinstance(response, str): 613 method, params = self._get_method_params() 614 if method == "getPackage": 615 return self.redirect(self.req, response) 616 617 # GET requests resulting in a Fault receive special treatment 618 # since we have to stick the error message in the HTTP header, 619 # and to return an Apache error code 620 621 if isinstance(response, xmlrpclib.Fault): 622 log_debug(4, "Return FAULT", 623 response.faultCode, response.faultString) 624 retcode = apache.HTTP_NOT_FOUND 625 if abs(response.faultCode) in (33, 34, 35, 37, 39, 41): 626 retcode = apache.HTTP_UNAUTHORIZED 627 628 self.req.headers_out["X-RHN-Fault-Code"] = \ 629 str(response.faultCode) 630 faultString = string.strip(base64.encodestring( 631 response.faultString)) 632 # Split the faultString into multiple lines 633 for line in string.split(faultString, '\n'): 634 self.req.headers_out.add("X-RHN-Fault-String", 635 string.strip(line)) 636 # And then send all the other things 637 for k, v in rhnFlags.get('outputTransportOptions').items(): 638 setHeaderValue(self.req.headers_out, k, v) 639 return retcode 640 # Otherwise we're pretty much fine with the standard response 641 # handler 642 643 # Copy the fields from the transport options, if necessary 644 for k, v in rhnFlags.get('outputTransportOptions').items(): 645 setHeaderValue(self.req.headers_out, k, v) 646 # and jump into the base handler 647 return apacheRequest.response(self, response)
648 649 # pkilambi: redirect request back to client with edge network url
650 - def redirect(self, req, url, temporary=1):
651 log_debug(3, "url input to redirect is ", url) 652 if req.sent_bodyct: 653 raise IOError("Cannot redirect after headers have already been sent.") 654 655 # akamize the url with the new tokengen before sending the redirect response 656 import tokengen.Generator 657 arl = tokengen.Generator.generate_auth_url(url) 658 req.headers_out["Location"] = arl 659 log_debug(3, "Akamized url to redirect is ", arl) 660 if temporary: 661 req.status = apache.HTTP_MOVED_TEMPORARILY 662 else: 663 req.status = apache.HTTP_MOVED_PERMANENTLY 664 return req.status
665