wiki:RedirectHttpsRequests
Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Making HTTP and HTTPS Redirects Work With a Reverse Proxy

This guide is designed for TurboGears version 0.9.

The Problem

One common setup is to have a reverse proxy (like Pound, Lighttpd, or Apache) sit in front of CherryPy and handle requests. If you want to handle both http and https protocols, you set up your reverse proxy to deal with the secure communications, and then pass types of both types of requests (secure and insecure) to CherryPy as a normal http request. CherryPy processes the requests, returns them to the proxy, and the proxy passes them on to the client (secure or insecure, depending on the original request).

This causes a problem if you do a HTTPRedirect to a url in your application. CherryPy thinks that this is an unencrypted request. So, the redirect url provided by CherryPy will begin with http, regardless of whether or not the original url was http or https.

One Solution

One way to work around this is to have the proxy give a hint to CherryPy as to the original protocol. Most proxies have some way to set a custom header to the request before passing it on. So, the general solution is to add a special header to https requests, and then have a CherryPy filter look for that header and modify the base url to something more appropriate.

Step One: Make the proxy set a custom header

I'm going to use  Pound as my example reverse proxy. I configure Pound to add a special header called 'X-Requested-Ssl'. The relevant part of my pound configuration file looks like:

ListenHTTPS
	Address 127.0.0.1
	Port 443
	Cert "/Users/plewis/Desktop/Pound-2.0.2/mycert.pem"
	AddHeader "X-Requested-Ssl: Yes"
	Service
		BackEnd
			Address 127.0.0.1
			Port 8080
		End
	End
End

Lighttpd users can set a custom header via  setenv.add-request-header. Apache users can use  mod_headers. You don't want to add this header on a global basis, but only for https requests.

Step Two: Create a new filter

Now, we create a special CherryPy filter to search for the "X-Requested-Ssl" header. Below is one file that will do the trick:

httpsfilter.py

from cherrypy.filters.basefilter import BaseFilter
import cherrypy
from turbogears import config

class HTTPSFilter(BaseFilter):
   
    def before_request_body(self):
        # if the filter isn't turned on, exit immediately
        if not config.get('https_filter.on', False):
            return
            
        request = cherrypy.request
        
        # Check for a special header 'X-Requested-Ssl'.  If we have it,
        # then we substitute the secure base url
        requested_ssl = request.headers.get('X-Requested-Ssl', 'NO').upper()
        if requested_ssl = 'YES':
            request.base = config.get('https_filter.secure_base_url', 'https://localhost')

When the filter finds the 'X-Requested-Ssl' header, it sets the base url of the request with the secure url.

Step Three: Set configuration file settings

In config.py, set the following configuration variables:

path('/')
https_filter.on = True
https_filter.secure_base_url = "https://secure.example.com"

Set the secure_base_url value to be the base site name of your reqest.

Step Four: Use the filter

Assuming you want to use this filter throughout your application, add the follwing to your root controller:

from httpsfilter import HTTPSFilter

class Root(turbogears.controllers.RootController):
    
    _cp_filters = [HTTPSFilter()]
    # Rest of controller code ...

This is the super-secret semi-undocumented way to add a custom filter to you controller.

Step Five: Test things out

You should be able to add a method to your controller like:

@turbogears.expose()
def test_redirect(self):
    turbogears.controllers.redirect("/") #redirect uses cherrypy.HTTPRedirect

Calling this url should work over both http and https, and will properly redirect to the base of your application.

Better Solutions

In the long run, it would be better if CherryPy were able to listen on two different ports for requests. If we could configure the ports to have different base urls, it would make processing redirects much easier. It looks like this may be possible to do in upcoming versions of TurboGears.

Solving the problem with Apache

The above solution will work for any protocol/url combination. For example, if you have a controller 'do_stuff' that redirects to 'done', the following will work:

  • http://example.com/do_stuff -> redirects to -> http://example.com/done
  • https://example.com/do_stuff -> redirects to -> https://example.com/done

However, you may not need to do this in all cases. If part of your site is only going to be accessed via https (e.g. https://example.com/secure), you may be able to have configre Apache to fix the redirected urls to this area.

Using Apache you configure it normally to listen on whatever port you'd like to have it. And code your TurboGears code normally, as if it was all local. Using any of cherrypy.!HTTPRedirect or turbogears.redirect work the same way.

To achieve that you just need to put in your server (if you're only serving one website in one port) or virtual host (if you're serving more websites, mixing technologies such as TurboGears and Zope, using both HTTP and HTTPS, etc.) the following lines:

ProxyPass /root_of_site http://localhost:8080/
ProxyPassReverse /root_of_site http://localhost:8080/

If your newly developed TurboGears site is the root of your HTTPS website, then those lines would become:

<VirtualHost hostname:443>
  # Several common Apache configurations for servername, certificate, etc.

  # Let the magic begin!
  ProxyPass / http://localhost:8080/
  ProxyPassReverse / http://localhost:8080/
</VirtualHost>

ProxyPass takes care of passing the request on the SSL-encrypted port to the local 8080 port where CherryPy is listening. Then, ProxyPassReverse takes care that everything coming back from CherryPy references the public website.

I've been using these without any new filter or change to headers and with a lot of turbogears.redirects inside the code. I've also found that using Apache makes the site more responsive than using Pound, I believe it is due cache.