Package proxy :: Package redirect :: Module rhnRedirect
[hide private]
[frames] | no frames]

Source Code for Module proxy.redirect.rhnRedirect

  1  # Spacewalk Proxy Server SSL Redirect handler code. 
  2  # 
  3  # Copyright (c) 2008--2018 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 socket 
 19  import re 
 20  from urlparse import urlparse, urlunparse 
 21   
 22  # common module imports 
 23  from spacewalk.common.rhnConfig import CFG 
 24  from spacewalk.common.rhnLog import log_debug, log_error 
 25  from spacewalk.common.rhnTB import Traceback 
 26  from spacewalk.common import rhnFlags, rhnLib, apache 
 27   
 28  # rhnlib imports 
 29  from rhn import connections 
 30   
 31  # local module imports 
 32  from proxy.rhnShared import SharedHandler 
 33  from proxy import rhnConstants 
 34   
 35   
 36  # Main apache entry point for the proxy. 
 37   
 38   
39 -class RedirectHandler(SharedHandler):
40 41 """ Spacewalk Proxy SSL Redirect specific handler code called by rhnApache. 42 43 Workflow is: 44 Client -> Apache:Broker -> Squid -> Apache:Redirect -> Satellite 45 46 Redirect handler get all request for localhost:80 and they come 47 from Broker handler through Squid, which hadle caching. 48 Redirect module transform destination url to parent or http proxy. 49 Depend on what we have in CFG. 50 """ 51
52 - def __init__(self, req):
53 SharedHandler.__init__(self, req) 54 self.componentType = 'proxy.redirect' 55 self._initConnectionVariables(req) 56 self.rhnParentXMLRPC = None
57
58 - def _initConnectionVariables(self, _req):
59 """ set connection variables 60 NOTE: self.{caChain,rhnParent,httpProxy*} are initialized 61 in SharedHandler 62 """ 63 64 effectiveURI = self._getEffectiveURI() 65 effectiveURI_parts = urlparse(effectiveURI) 66 scheme = 'http' 67 if CFG.USE_SSL: 68 scheme = 'https' 69 else: 70 self.caChain = '' 71 self.rhnParentXMLRPC = urlunparse((scheme, self.rhnParent, '/XMLRPC', '', '', '')) 72 self.rhnParent = urlunparse((scheme, self.rhnParent) + effectiveURI_parts[2:]) 73 74 log_debug(3, 'remapped self.rhnParent: %s' % self.rhnParent) 75 log_debug(3, 'remapped self.rhnParentXMLRPC: %s' % self.rhnParentXMLRPC)
76
77 - def handler(self):
78 """ Main handler for all requests pumped through this server. """ 79 80 log_debug(4, 'In redirect handler') 81 self._prepHandler() 82 83 # Rebuild the X-Forwarded-For header so that it reflects the actual 84 # path of the request. We must do this because squid is unable to 85 # determine the "real" client, and will make each entry in the chain 86 # 127.0.0.1. 87 _oto = rhnFlags.get('outputTransportOptions') 88 _oto['X-Forwarded-For'] = _oto['X-RHN-IP-Path'] 89 90 self.rhnParent = self.rhnParent or '' # paranoid 91 92 log_debug(4, 'Connecting to parent...') 93 self._connectToParent() # part 1 94 95 log_debug(4, 'Initiating communication with server...') 96 status = self._serverCommo() # part 2 97 if (status != apache.OK) and (status != apache.HTTP_PARTIAL_CONTENT): 98 log_debug(3, "Leaving handler with status code %s" % status) 99 return status 100 101 log_debug(4, 'Initiating communication with client...') 102 # If we got this far, it has to be a good response 103 return self._clientCommo(status)
104
105 - def _handleServerResponse(self, status):
106 """ Here, we'll override the default behavior for handling server responses 107 so that we can adequately handle 302's. 108 109 We will follow redirects unless it is redirect to (re)login page. In which 110 case we change protocol to https and return redirect to user. 111 """ 112 113 # In case of a 302, redirect the original request to the location 114 # specified in the response. 115 116 if status == apache.HTTP_MOVED_TEMPORARILY or \ 117 status == apache.HTTP_MOVED_PERMANENTLY: 118 119 log_debug(1, "Received redirect response: ", status) 120 121 # if we redirected to ssl version of login page, send redirect directly to user 122 headers = self.responseContext.getHeaders() 123 if headers is not None: 124 for headerKey in headers.keys(): 125 if headerKey == 'location': 126 location = self._get_header(headerKey) 127 relogin = re.compile(r'https?://.*(/rhn/(Re)?Login.do\?.*)') 128 m = relogin.match(location[0]) 129 if m: 130 # pull server name out of "t:o:k:e:n:hostname1,t:o:k:e:n:hostname2,..." 131 proxy_auth = self.req.headers_in['X-RHN-Proxy-Auth'] 132 last_auth = proxy_auth.split(',')[-1] 133 server_name = last_auth.split(':')[-1] 134 log_debug(1, "Redirecting to SSL version of login page") 135 rhnLib.setHeaderValue(self.req.headers_out, 'Location', 136 "https://%s%s" % (server_name, m.group(1))) 137 return apache.HTTP_MOVED_PERMANENTLY 138 139 redirectStatus = self.__redirectToNextLocation() 140 141 # At this point, we've either: 142 # 143 # (a) successfully redirected to the 3rd party 144 # (b) been told to redirect somewhere else from the 3rd party 145 # (c) run out of retry attempts 146 # 147 # We'll keep redirecting until we've received HTTP_OK or an error. 148 149 while redirectStatus == apache.HTTP_MOVED_PERMANENTLY or \ 150 redirectStatus == apache.HTTP_MOVED_TEMPORARILY: 151 152 # We've been told to redirect again. We'll pass a special 153 # argument to ensure that if we end up back at the server, we 154 # won't be redirected again. 155 156 log_debug(1, "Redirected again! Code=", redirectStatus) 157 redirectStatus = self.__redirectToNextLocation(True) 158 159 if (redirectStatus != apache.HTTP_OK) and (redirectStatus != apache.HTTP_PARTIAL_CONTENT): 160 161 # We must have run out of retry attempts. Fail over to Hosted 162 # to perform the request. 163 164 log_debug(1, "Redirection failed; retries exhausted. " 165 "Failing over. Code=", 166 redirectStatus) 167 redirectStatus = self.__redirectFailover() 168 169 return SharedHandler._handleServerResponse(self, redirectStatus) 170 171 else: 172 # Otherwise, revert to default behavior. 173 return SharedHandler._handleServerResponse(self, status) 174 return None
175
176 - def __redirectToNextLocation(self, loopProtection=False):
177 """ This function will perform a redirection to the next location, as 178 specified in the last response's "Location" header. This function will 179 return an actual HTTP response status code. If successful, it will 180 return apache.HTTP_OK, not apache.OK. If unsuccessful, this function 181 will retry a configurable number of times, as defined in 182 CFG.NETWORK_RETRIES. The following codes define "success". 183 184 HTTP_OK 185 HTTP_PARTIAL_CONTENT 186 HTTP_MOVED_TEMPORARILY 187 HTTP_MOVED_PERMANENTLY 188 189 Upon successful completion of this function, the responseContext 190 should be populated with the response. 191 192 Arguments: 193 194 loopProtection - If True, this function will insert a special 195 header into the new request that tells the RHN 196 server not to issue another redirect to us, in case 197 that's where we end up being redirected. 198 199 Return: 200 201 This function may return any valid HTTP_* response code. See 202 __redirectToNextLocationNoRetry for more info. 203 """ 204 retriesLeft = CFG.NETWORK_RETRIES 205 206 # We'll now try to redirect to the 3rd party. We will keep 207 # retrying until we exhaust the number of allowed attempts. 208 # Valid response codes are: 209 # HTTP_OK 210 # HTTP_PARTIAL_CONTENT 211 # HTTP_MOVED_PERMANENTLY 212 # HTTP_MOVED_TEMPORARILY 213 214 redirectStatus = self.__redirectToNextLocationNoRetry(loopProtection) 215 while redirectStatus != apache.HTTP_OK and redirectStatus != apache.HTTP_PARTIAL_CONTENT and \ 216 redirectStatus != apache.HTTP_MOVED_PERMANENTLY and \ 217 redirectStatus != apache.HTTP_MOVED_TEMPORARILY and retriesLeft > 0: 218 219 retriesLeft = retriesLeft - 1 220 log_debug(1, "Redirection failed; trying again. " 221 "Retries left=", 222 retriesLeft, 223 "Code=", 224 redirectStatus) 225 226 # Pop the current response context and restore the state to 227 # the last successful response. The acts of remove the current 228 # context will cause all of its open connections to be closed. 229 self.responseContext.remove() 230 231 # XXX: Possibly sleep here for a second? 232 redirectStatus = \ 233 self.__redirectToNextLocationNoRetry(loopProtection) 234 235 return redirectStatus
236
237 - def __redirectToNextLocationNoRetry(self, loopProtection=False):
238 """ This function will perform a redirection to the next location, as 239 specified in the last response's "Location" header. This function will 240 return an actual HTTP response status code. If successful, it will 241 return apache.HTTP_OK, not apache.OK. If unsuccessful, this function 242 will simply return; no retries will be performed. The following error 243 codes can be returned: 244 245 HTTP_OK,HTTP_PARTIAL_CONTENT - Redirect successful. 246 HTTP_MOVED_TEMPORARILY - Redirect was redirected again by 3rd party. 247 HTTP_MOVED_PERMANENTLY - Redirect was redirected again by 3rd party. 248 HTTP_INTERNAL_SERVER_ERROR - Error extracting redirect information 249 HTTP_SERVICE_UNAVAILABLE - Could not connect to 3rd party server, 250 connection was reset, or a read error 251 occurred during communication. 252 HTTP_* - Any other HTTP status code may also be 253 returned. 254 255 Upon successful completion of this function, a new responseContext 256 will be created and pushed onto the stack. 257 """ 258 259 # Obtain the redirect location first before we replace the current 260 # response context. It's contained in the Location header of the 261 # previous response. 262 263 redirectLocation = self._get_header(rhnConstants.HEADER_LOCATION) 264 265 # We are about to redirect to a new location so now we'll push a new 266 # response context before we return any errors. 267 self.responseContext.add() 268 269 # There should always be a redirect URL passed back to us. If not, 270 # there's an error. 271 272 if not redirectLocation: 273 log_error(" No redirect location specified!") 274 Traceback(mail=0) 275 return apache.HTTP_INTERNAL_SERVER_ERROR 276 277 # The _get_header function returns the value as a list. There should 278 # always be exactly one location specified. 279 280 redirectLocation = redirectLocation[0] 281 log_debug(1, " Redirecting to: ", redirectLocation) 282 283 # Tear apart the redirect URL. We need the scheme, the host, the 284 # port (if not the default), and the URI. 285 286 _scheme, host, port, uri = self._parse_url(redirectLocation) 287 288 # Add any params onto the URI since _parse_url doesn't include them. 289 if redirectLocation.find('?') > -1: 290 uri += redirectLocation[redirectLocation.index('?'):] 291 292 # Now create a new connection. We'll use SSL if configured to do 293 # so. 294 295 params = { 296 'host': host, 297 'port': port, 298 } 299 if CFG.has_key('timeout'): 300 params['timeout'] = CFG.TIMEOUT 301 if CFG.USE_SSL: 302 log_debug(1, " Redirecting with SSL. Cert= ", self.caChain) 303 params['trusted_certs'] = [self.caChain] 304 connection = connections.HTTPSConnection(**params) 305 else: 306 log_debug(1, " Redirecting withOUT SSL.") 307 connection = connections.HTTPConnection(**params) 308 309 # Put the connection into the current response context. 310 self.responseContext.setConnection(connection) 311 312 # Now open the connection to the 3rd party server. 313 314 log_debug(4, "Attempting to connect to 3rd party server...") 315 try: 316 connection.connect() 317 except socket.error, e: 318 log_error("Error opening redirect connection", redirectLocation, e) 319 Traceback(mail=0) 320 return apache.HTTP_SERVICE_UNAVAILABLE 321 log_debug(4, "Connected to 3rd party server:", 322 connection.sock.getpeername()) #pylint: disable=no-member 323 324 # Put the request out on the wire. 325 326 response = None 327 try: 328 # We'll redirect to the URI made in the original request, but with 329 # the new server instead. 330 331 log_debug(4, "Making request: ", self.req.method, uri) 332 connection.putrequest(self.req.method, uri) 333 334 # Add some custom headers. 335 336 if loopProtection: 337 connection.putheader(rhnConstants.HEADER_RHN_REDIRECT, '0') 338 339 log_debug(4, " Adding original URL header: ", self.rhnParent) 340 connection.putheader(rhnConstants.HEADER_RHN_ORIG_LOC, 341 self.rhnParent) 342 343 # Add all the other headers in the original request in case we 344 # need to re-authenticate with Hosted. 345 346 for hdr in self.req.headers_in.keys(): 347 if hdr.lower().startswith("x-rhn"): 348 connection.putheader(hdr, self.req.headers_in[hdr]) 349 log_debug(4, "Passing request header: ", 350 hdr, 351 self.req.headers_in[hdr]) 352 353 connection.endheaders() 354 355 response = connection.getresponse() 356 except IOError, ioe: 357 # Raised by getresponse() if server closes connection on us. 358 log_error("Redirect connection reset by peer.", 359 redirectLocation, 360 ioe) 361 Traceback(mail=0) 362 363 # The connection is saved in the current response context, and 364 # will be closed when the caller pops the context. 365 return apache.HTTP_SERVICE_UNAVAILABLE 366 367 except socket.error, se: 368 # Some socket error occurred. Possibly a read error. 369 log_error("Redirect request failed.", redirectLocation, se) 370 Traceback(mail=0) 371 372 # The connection is saved in the current response context, and 373 # will be closed when the caller pops the context. 374 return apache.HTTP_SERVICE_UNAVAILABLE 375 376 # Save the response headers and body FD in the current communication 377 # context. 378 379 self.responseContext.setBodyFd(response) 380 self.responseContext.setHeaders(response.msg) 381 382 log_debug(4, "Response headers: ", 383 self.responseContext.getHeaders().items()) 384 log_debug(4, "Got redirect response. Status=", response.status) 385 386 # Return the HTTP status to the caller. 387 388 return response.status
389
390 - def __redirectFailover(self):
391 """ This routine resends the original request back to the satellite/hosted 392 system if a redirect to a 3rd party failed. To prevent redirection loops 393 from occurring, an "X-RHN-Redirect: 0" header is passed along with the 394 request. This function will return apache.HTTP_OK if everything 395 succeeded, otherwise it will return an appropriate HTTP error code. 396 """ 397 398 # Add a special header which will tell the server not to send us any 399 # more redirects. 400 401 headers = rhnFlags.get('outputTransportOptions') 402 headers[rhnConstants.HEADER_RHN_REDIRECT] = '0' 403 404 log_debug(4, "Added X-RHN-Redirect header to outputTransportOptions:", 405 headers) 406 407 # Reset the existing connection and reconnect to the RHN parent server. 408 409 self.responseContext.clear() 410 self._connectToParent() 411 412 # We'll just call serverCommo once more. The X-RHN-Redirect constant 413 # will prevent us from falling into an infinite loop. Only GETs are 414 # redirected, so we can safely pass an empty string in as the request 415 # body. 416 417 status = self._serverCommo() 418 419 # This little hack isn't pretty, but lets us normalize our result code. 420 421 if status == apache.OK: 422 status = apache.HTTP_OK 423 424 return status
425