Package proxy :: Module rhnShared
[hide private]
[frames] | no frames]

Source Code for Module proxy.rhnShared

  1  # Shared (Spacewalk Proxy/Redirect) handler code called by rhnApache. 
  2  # 
  3  # Copyright (c) 2008--2017 Red Hat, Inc. 
  4  # 
  5  # This software is licensed to you under the GNU General Public License, 
  6  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  7  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  8  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
  9  # along with this software; if not, see 
 10  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 11  # 
 12  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 13  # granted to use or replicate Red Hat trademarks that are incorporated 
 14  # in this software or its documentation. 
 15  # 
 16   
 17  # language imports 
 18  import urllib 
 19  import socket 
 20  import sys 
 21  from types import ListType, TupleType 
 22   
 23  # global imports 
 24  from rhn import connections 
 25  from rhn.SSL import TimeoutException 
 26  from rhn.SmartIO import SmartIO 
 27   
 28  # common imports 
 29  from rhn.UserDictCase import UserDictCase 
 30  from spacewalk.common.rhnTB import Traceback 
 31  from spacewalk.common.rhnConfig import CFG 
 32  from spacewalk.common.rhnException import rhnFault, rhnException 
 33  from spacewalk.common.rhnLog import log_debug, log_error 
 34  from spacewalk.common import rhnFlags, rhnLib, apache 
 35  from spacewalk.common.rhnTranslate import _ 
 36   
 37  # local imports 
 38  import rhnConstants 
 39  from responseContext import ResponseContext 
40 41 42 -class SharedHandler:
43 44 """ Shared handler class (between rhnBroker and rhnRedirect. 45 *** only inherited *** 46 """ 47 48 # pylint: disable=R0902,R0903
49 - def __init__(self, req):
50 """ init with http request object """ 51 52 # FIXME: should rename some things: 53 # self.bodyFd --> self.body or self.data or ? 54 # self.caChain --> self.caCert 55 56 self.req = req 57 # turn wsgi.input object into a SmartIO instance so it can be read 58 # more than once 59 if 'wsgi.input' in self.req.headers_in: 60 smartFd = SmartIO(max_mem_size=CFG.MAX_MEM_FILE_SIZE) 61 smartFd.write(self.req.headers_in['wsgi.input'].read()) 62 self.req.headers_in['wsgi.input'] = smartFd 63 64 self.responseContext = ResponseContext() 65 self.uri = None # '' 66 67 # Common settings for both the proxy and the redirect 68 # broker and redirect immediately alter these for their own purposes 69 self.caChain = CFG.CA_CHAIN 70 self.httpProxy = CFG.HTTP_PROXY 71 self.httpProxyUsername = CFG.HTTP_PROXY_USERNAME 72 self.httpProxyPassword = CFG.HTTP_PROXY_PASSWORD 73 if not self.httpProxyUsername: 74 self.httpProxyPassword = '' 75 self.rhnParent = CFG.RHN_PARENT or '' 76 self.rhnParent = rhnLib.parseUrl(self.rhnParent)[1].split(':')[0] 77 CFG.set('RHN_PARENT', self.rhnParent)
78 79 # can we resolve self.rhnParent? 80 # BUG 148961: not necessary, and dumb if the proxy is behind a firewall 81 # try: 82 # socket.gethostbyname(self.rhnParent) 83 # except socket.error, e: 84 # msg = "SOCKET ERROR: hostname: %s - %s" % (self.rhnParent, str(e)) 85 # log_error(msg) 86 # log_debug(0, msg) 87 # raise 88 89 # --- HANDLER SPECIFIC CODE --- 90
91 - def _prepHandler(self):
92 """ Handler part 0 """ 93 94 # Just to be on the safe side 95 if self.req.main: 96 # A subrequest 97 return apache.DECLINED 98 log_debug(4, rhnFlags.all()) 99 100 if not self.rhnParent: 101 raise rhnException("Oops, no proxy parent! Exiting") 102 103 # Copy the headers. 104 rhnFlags.get('outputTransportOptions').clear() 105 rhnFlags.get('outputTransportOptions').update(self._getHeaders(self.req)) 106 107 return apache.OK
108
109 - def _connectToParent(self):
110 """ Handler part 1 111 Should not return an error code -- simply connects. 112 """ 113 114 scheme, host, port, self.uri = self._parse_url(self.rhnParent) 115 self.responseContext.setConnection(self._create_connection()) 116 117 if not self.uri: 118 self.uri = '/' 119 120 log_debug(3, 'Scheme:', scheme) 121 log_debug(3, 'Host:', host) 122 log_debug(3, 'Port:', port) 123 log_debug(3, 'URI:', self.uri) 124 log_debug(3, 'HTTP proxy:', self.httpProxy) 125 log_debug(3, 'HTTP proxy username:', self.httpProxyUsername) 126 log_debug(3, 'HTTP proxy password:', "<password>") 127 log_debug(3, 'CA cert:', self.caChain) 128 129 try: 130 self.responseContext.getConnection().connect() 131 except socket.error, e: 132 log_error("Error opening connection", self.rhnParent, e) 133 Traceback(mail=0) 134 raise rhnFault(1000, 135 _("Spacewalk Proxy could not successfully connect its RHN parent. " 136 "Please contact your system administrator.")), None, sys.exc_info()[2] 137 138 # At this point the server should be okay 139 log_debug(3, "Connected to parent: %s " % self.rhnParent) 140 if self.httpProxy: 141 if self.httpProxyUsername: 142 log_debug(3, "HTTP proxy info: %s %s/<password>" % ( 143 self.httpProxy, self.httpProxyUsername)) 144 else: 145 log_debug(3, "HTTP proxy info: %s" % self.httpProxy) 146 else: 147 log_debug(3, "HTTP proxy info: not using an HTTP proxy") 148 peer = self.responseContext.getConnection().sock.getpeername() 149 log_debug(4, "Other connection info: %s:%s%s" % 150 (peer[0], peer[1], self.uri))
151
152 - def _create_connection(self):
153 """ Returns a Connection object """ 154 scheme, host, port, _uri = self._parse_url(self.rhnParent) 155 # Build the list of params 156 params = { 157 'host': host, 158 'port': port, 159 } 160 if CFG.has_key('timeout'): 161 params['timeout'] = CFG.TIMEOUT 162 if self.httpProxy: 163 params['proxy'] = self.httpProxy 164 params['username'] = self.httpProxyUsername 165 params['password'] = self.httpProxyPassword 166 if scheme == 'https' and self.caChain: 167 params['trusted_certs'] = [self.caChain, ] 168 169 # Now select the right class 170 if self.httpProxy: 171 if scheme == 'https': 172 conn_class = connections.HTTPSProxyConnection 173 else: 174 conn_class = connections.HTTPProxyConnection 175 else: 176 if scheme == 'https': 177 conn_class = connections.HTTPSConnection 178 else: 179 conn_class = connections.HTTPConnection 180 181 log_debug(5, "Using connection class", conn_class, 'Params:', params) 182 return conn_class(**params)
183 184 @staticmethod
185 - def _parse_url(url):
186 """ Returns scheme, host, port, path. """ 187 scheme, netloc, path, _params, _query, _frag = rhnLib.parseUrl(url) 188 host, port = urllib.splitnport(netloc) 189 if (port <= 0): 190 port = None 191 return scheme, host, port, path
192
193 - def _serverCommo(self):
194 """ Handler part 2 195 196 Server (or next proxy) communication. 197 """ 198 199 log_debug(1) 200 201 # Copy the method from the original request, and use the 202 # handler for this server 203 # We add path_info to the put (GET, CONNECT, HEAD, PUT, POST) request. 204 log_debug(2, self.req.method, self.uri) 205 self.responseContext.getConnection().putrequest(self.req.method, 206 self.uri) 207 208 # Send the headers, the body and expect a response 209 try: 210 status, headers, bodyFd = self._proxy2server() 211 self.responseContext.setHeaders(headers) 212 self.responseContext.setBodyFd(bodyFd) 213 except IOError: 214 # Raised by HTTP*Connection.getresponse 215 # Server closed connection on us, no need to mail out 216 # XXX: why are we not mailing this out??? 217 Traceback("SharedHandler._serverCommo", self.req, mail=0) 218 raise rhnFault(1000, _( 219 "Spacewalk Proxy error: connection with the Spacewalk server failed")), None, sys.exc_info()[2] 220 except socket.error: 221 # maybe self.req.read() failed? 222 Traceback("SharedHandler._serverCommo", self.req) 223 raise rhnFault(1000, _( 224 "Spacewalk Proxy error: connection with the Spacewalk server failed")), None, sys.exc_info()[2] 225 226 log_debug(2, "HTTP status code (200 means all is well): %s" % status) 227 228 # Now we need to decide how to deal with the server's response. We'll 229 # defer to subclass-specific implementation here. The handler will 230 # return apache.OK if the request was a success. 231 232 return self._handleServerResponse(status)
233
234 - def _handleServerResponse(self, status):
235 """ This method can be overridden by subclasses who want to handle server 236 responses in their own way. By default, we will wrap all the headers up 237 and send them back to the client with an error status. This method 238 should return apache.OK if everything went according to plan. 239 """ 240 if (status != apache.HTTP_OK) and (status != apache.HTTP_PARTIAL_CONTENT): 241 # Non 200 response; have to treat it differently 242 log_debug(2, "Forwarding status %s" % status) 243 # Copy the incoming headers to headers_out 244 headers = self.responseContext.getHeaders() 245 if headers is not None: 246 for k in headers.keys(): 247 rhnLib.setHeaderValue(self.req.headers_out, k, 248 self._get_header(k)) 249 else: 250 log_error('WARNING? - no incoming headers found!') 251 # And that's that 252 return status 253 254 if status == apache.HTTP_PARTIAL_CONTENT: 255 return apache.HTTP_PARTIAL_CONTENT 256 257 # apache.HTTP_OK becomes apache.OK. 258 return apache.OK
259
260 - def _get_header(self, k, headerObj=None):
261 if headerObj is None: 262 headerObj = self.responseContext.getHeaders() 263 264 if hasattr(headerObj, 'getheaders'): 265 return headerObj.getheaders(k) 266 # The pain of python 1.5.2 267 headers = headerObj.getallmatchingheaders(k) 268 hname = str(k).lower() + ':' 269 hlen = len(hname) 270 ret = [] 271 for header in headers: 272 hn = header[:hlen].lower() 273 if hn != hname: 274 log_debug(1, "Invalid header", header) 275 continue 276 ret.append(header[hlen:].strip()) 277 return ret
278
279 - def _clientCommo(self, status=apache.OK):
280 """ Handler part 3 281 Forward server's response to the client. 282 """ 283 log_debug(1) 284 285 try: 286 self._forwardServer2Client() 287 except IOError: 288 # Raised by HTTP*connection.getresponse 289 # Client closed connection on us, no need to mail out a traceback 290 Traceback("SharedHandler._clientCommo", self.req, mail=0) 291 return apache.HTTP_SERVICE_UNAVAILABLE 292 293 # Close all open response contexts. 294 self.responseContext.clear() 295 296 return status
297 298 # --- PROTECTED METHODS --- 299 300 @staticmethod
301 - def _getHeaders(req):
302 """ Copy the incoming headers. """ 303 304 hdrs = UserDictCase() 305 for k in req.headers_in.keys(): 306 # XXX misa: is this enough? Shouldn't we care about multivalued 307 # headers? 308 hdrs[k] = req.headers_in[k] 309 return hdrs
310
311 - def _forwardServer2Client(self):
312 """ Forward headers, and bodyfd from server to the calling client. 313 For most XMLRPC code, this function is called. 314 """ 315 316 log_debug(1) 317 318 # Okay, nothing interesting from the server; 319 # we'll just forward what we got 320 321 bodyFd = self.responseContext.getBodyFd() 322 323 self._forwardHTTPHeaders(bodyFd, self.req) 324 325 # Set the content type 326 327 headers = self.responseContext.getHeaders() 328 self.req.content_type = headers.gettype() 329 self.req.send_http_header() 330 331 # Forward the response body back to the client. 332 333 self._forwardHTTPBody(bodyFd, self.req)
334
335 - def _proxy2server(self):
336 hdrs = rhnFlags.get('outputTransportOptions') 337 log_debug(3, hdrs) 338 size = -1 339 340 # Put the headers into the output connection object 341 http_connection = self.responseContext.getConnection() 342 for (k, vals) in hdrs.items(): 343 if k.lower() in ['content_length', 'content-length']: 344 try: 345 size = int(vals) 346 except ValueError: 347 pass 348 if k.lower() in ['content_length', 'content_type']: 349 # mod_wsgi modifies incoming headers so we have to transform them back 350 k = k.replace('_', '-') 351 if not (k.lower()[:2] == 'x-' or 352 k.lower() in [ # all but 'host', and 'via' 353 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 354 'accept-ranges', 'age', 'allow', 'authorization', 'cache-control', 355 'connection', 'content-encoding', 'content-language', 'content-length', 356 'content-location', 'content-md5', 'content-range', 'content-type', 357 'date', 'etag', 'expect', 'expires', 'from', 'if-match', 358 'if-modified-since', 'if-none-match', 'if-range', 'if-unmodified-since', 359 'last-modified', 'location', 'max-forwards', 'pragma', 'proxy-authenticate', 360 'proxy-authorization', 'range', 'referer', 'retry-after', 'server', 361 'te', 'trailer', 'transfer-encoding', 'upgrade', 'user-agent', 'vary', 362 'warning', 'www-authenticate']): 363 # filter out header we don't want to send 364 continue 365 if not isinstance(vals, (ListType, TupleType)): 366 vals = [vals] 367 for v in vals: 368 log_debug(5, "Outgoing header", k, v) 369 http_connection.putheader(k, v) 370 http_connection.endheaders() 371 372 # Send the body too if there is a body 373 if size > 0: 374 # reset file to beginning so it can be read again 375 self.req.headers_in['wsgi.input'].seek(0, 0) 376 if sys.version_info < (2, 6): 377 data = self.req.headers_in['wsgi.input'].read(size) 378 else: 379 data = self.req.headers_in['wsgi.input'] 380 http_connection.send(data) 381 382 # At this point everything is sent to the server 383 # We now wait for the response 384 try: 385 response = http_connection.getresponse() 386 except TimeoutException: 387 log_error("Connection timed out") 388 return apache.HTTP_GATEWAY_TIME_OUT, None, None 389 headers = response.msg 390 status = response.status 391 # Get the body of the request too - well, just a fd actually 392 # in this case, the response object itself. 393 bodyFd = response 394 return status, headers, bodyFd
395
396 - def _getEffectiveURI(self):
397 if self.req.headers_in.has_key(rhnConstants.HEADER_EFFECTIVE_URI): 398 return self.req.headers_in[rhnConstants.HEADER_EFFECTIVE_URI] 399 400 return self.req.uri
401 402 @staticmethod
403 - def _determineHTTPBodySize(headers):
404 """ This routine attempts to determine the size of an HTTP body by searching 405 the headers for a "Content-Length" field. The size is returned, if 406 found, otherwise -1 is returned. 407 """ 408 409 # Get the size of the body 410 size = 0 411 if headers.has_key(rhnConstants.HEADER_CONTENT_LENGTH): 412 try: 413 size = int(headers[rhnConstants.HEADER_CONTENT_LENGTH]) 414 except ValueError: 415 size = -1 416 else: 417 size = -1 418 419 return size
420
421 - def _forwardHTTPHeaders(self, fromResponse, toRequest):
422 """ This routine will transfer the header contents of an HTTP response to 423 the output headers of an HTTP request for reply to the original 424 requesting client. This function does NOT call the request's 425 send_http_header routine; that is the responsibility of the caller. 426 """ 427 428 if fromResponse is None or toRequest is None: 429 return 430 431 # Iterate over each header in the response and place it in the request 432 # output area. 433 434 for k in fromResponse.msg.keys(): 435 # Get the value 436 v = self._get_header(k, fromResponse.msg) 437 438 if (k == 'transfer-encoding') and ('chunked' in v): 439 log_debug(5, "Filtering header", k, v) 440 continue 441 442 # Set the field in the response 443 444 rhnLib.setHeaderValue(toRequest.headers_out, k, v)
445
446 - def _forwardHTTPBody(self, fromResponse, toRequest):
447 """ This routine will transfer the body of an HTTP response to the output 448 area of an HTTP request for response to the original requesting client. 449 The request's send_http_header function must be called before this 450 function is called. 451 """ 452 if fromResponse is None or toRequest is None: 453 return 454 455 # Get the size of the body 456 457 size = self._determineHTTPBodySize(fromResponse.msg) 458 log_debug(4, "Response body size: ", size) 459 460 # Now fill in the bytes if need be. 461 462 # read content if there is some or the size is unknown 463 if (size > 0 or size == -1) and (toRequest.method != 'HEAD'): 464 tfile = SmartIO(max_mem_size=CFG.MAX_MEM_FILE_SIZE) 465 buf = fromResponse.read(CFG.BUFFER_SIZE) 466 while buf: 467 try: 468 tfile.write(buf) 469 buf = fromResponse.read(CFG.BUFFER_SIZE) 470 except IOError: 471 buf = 0 472 tfile.seek(0) 473 if 'wsgi.file_wrapper' in toRequest.headers_in: 474 toRequest.output = toRequest.headers_in['wsgi.file_wrapper'](tfile, CFG.BUFFER_SIZE) 475 else: 476 toRequest.output = iter(lambda: tfile.read(CFG.BUFFER_SIZE), '')
477