WFront - A WSGI front-door dispatcher.

WFront is a simple top-level request dispatcher, directing requests based on "Virtual Host". WFront can be used to host multiple WSGI-powered domains in a single process, emulate a mod_proxy, SCGI/FastCGI/AJP or mod_python WSGI setup for development, or any other composition where operating at the host-level is desired. Mapping HTTP/1.0 requests to HTTP/1.1 Host:-style requests is supported and is very flexible.

Download

From the Cheese Shop.

Changelog

Version 0.3.1- initial public release.

Features

WFront was developed to unify 'development mode' WSGI servers and Apache-fronted production WSGI servers connected via Apache mod_proxy, FastCGI, AJP, etc. The examples discuss Apache, but WFront is not Apache-specific.

Some example routing situations WFront can assist with:

WFront provides 3 hooks into the request routing process:

  1. Host: determination (pluggable)

    Analyze the environ and determine which VirtualHost and URI is being requested. The default implementation handles direct and proxied requests automatically.

  2. Routing

    Matches the incoming request information to a mapping of WSGI applications.

  3. Cleanup (pluggable)

    Optional processing of environ and start_response before the WSGI application is invoked. The default implementation allows you to perform useful environment manipulation in a dict-based interface.

Usage

map = [('www.example.com', myapp, None),
       ('www.example.com:3000', otherapp, {'environ.key':'value'}),]

front = wfront.route(map)
return front(environ, start_response)

map = [('www.example.com:http', myapp, None),
       ('www.example.com:https', secureapp, None) ]

front = wfront.by_scheme(map, schemes={'http':(80,), 'https':(443,3000)'})
return front(environ, start_response)

WFront is driven by a routing map. The mapping is a sequence of 3-tuples:

path-spec, wsgi callable or string, pre-request cleanup

Path specs are compared to the requested hostname, port and path. Any or all of these components can be matched against, either literally or wildcarded. Separate each component with a colon. Empty or missing components match anything.

www.example.com
www.example.com:80
www.example.com:80:/some/path
*.example.com::/some/path
:80
::/some/path

Two path spec extensions are provided to generalize configuration.

wfront.by_scheme() maps ports to request schemes, allowing you to route without tracking exactly which port(s) are currently bound:

www.example.com:http
secure.example.com:https

MacroMap adds simple macro expansion to path specs:

www.{domain}
secure.{domain}

The WSGI application to dispatch to may be supplied either as a callable or a string. Strings are passed to either resolver or Paste for evaluation and are expected to return a callable. See either package's documentation for details, but the general idea is something like:

'module:my_callable'

You must have either resolver and/or the Paste package installed to use this feature.

Pre-request cleanup is an optional filter that operates on the WSGI call arguments: environ and start_response. As a convenience, you may provide a dict of key/value pairs instead of a callable and they will be merged into the environ dict on each request. You may also specify simple key transformations, for example removing a prefix from SCRIPT_NAME, adding to existing values, etc.

Like the WSGI callable, a cleanup callable may also be supplied as a string to be resolved at runtime.

Built-In Cleanup

The built-in cleanup function takes a dict d and runs environ.update(d). This behavior can be augmented with magic wfront.* keys in your dictionary. For example:

{ 'HTTPS': 'on',
  'wfront.remove.REMOTE_USER': 'true',
  'wfront.copy.REMOTE_HOST': 'our.remote_host.key',
  'wfront.subtract.HTTP_ACCEPT_ENCODING': 'deflate' }

The wfront.* directives operate on the existing environ values and take the general form:

'wfront.<action>.<environ key>' = '<new value>'
{ 'wfront.sprintf.SCRIPT_NAME': '/prepended/%s',
  'wfront.sprintf.PATH_INFO': '%s/appended',
  'wfront.sprintf.sandwich': 'in the %s middle' }

A less verbose syntax is available by supplying cleanup_syntax='shortcut' to the WFront constructor. This is not enabled by default.

Cookbook

Dispatch proxied VirtualHost requests from Apache with confidence:

httpd.conf:
# Two secure VirtualHosts, each on a separate IP.
<VirtualHost 127.0.0.1:443>
   ServerName www.example.com
   #...
   RewriteRule ^(/myapp.*)  http://localhost:8001/example.com/$1 [P] [L]
</VirtualHost>
<VirtualHost 127.0.0.2:443>
   ServerName www.example.org
   #...
   RewriteRule ^(/myapp.*)  http://localhost:8001/example.org/$1 [P] [L]
</VirtualHost>

our-app.py:
# Link VirtualHosts to 'exampleapp', some WSGI callable
map = [ ('www.example.com:https', exampleapp, None),
        ('www.example.org:https', exampleapp, None), ]

# Let's say we set up our development-mode server to listen on
# 8000 and 8443.

router = wfront.by_scheme(map,
                          schemes={'http':(80,8000),'https:':(443,8443)})

# For development, we're done.  'router' will work as-is.

# But we need a little glue for the production environment.  When
# mod_proxy sends requests over, they'll connect to our process as
# straight HTTP and may not have a host header.  We've added
# unique path prefixes in the proxy configuration to make precise
# mapping possible.

# Set up a WFront that translates prefixes to host information.

proxied = [
  ('::/example.com', router, { 'HTTP_HOST': 'www.example.com',
                               'HTTPS': 'on',
                               'SERVER_NAME': 'www.example.com',
                               'SERVER_PORT': '443',
                               'wfront.strip_path': '/example.com' }),
  ('::/example.org', router, { 'HTTP_HOST': 'www.example.org',
                               'HTTPS': 'on',
                               'SERVER_NAME': 'www.example.org',
                               'SERVER_PORT': '443',
                               'wfront.strip_path': '/example.org' }) ]

if running_in_development_mode:  # e.g.
  front = router
else:
  # Check each request for the path prefixes we added in the proxy
  # configuration.  When we get one of these, re-write the request
  # to make our process feel just like what the user's user agent
  # has connected to.
  front = wfront.route(proxied, default=front)

Notes

Tested with:

TODO