Christopher Williams

Django Subdomain Middleware with CNAME Support

For a personal project (details forthcoming), I've been trying to have separate subdomains for accounts and allow some of these accounts to have CNAME support. So, if I have an account cwilliams, my data should be available at cwilliams.myproject.com. I could also add myproject.christopher-williams.net as a CNAME and get the same view.

The implementation is fairly straightforward. I ended up taking django-subdomains, removing some functionality that I didn't need and adding the CNAME handling.

To use this middleware:

  1. Put middleware.py (see below) somewhere in your project. I have a Django app called core for things like this.
  2. Add a CNAME field to a model of your choosing (a user profile might be appropriate), preferably as a CharField. URLField will not work.
  3. Add a SlugField to the same model and wire that up like you would any other slug.
  4. Add a new subdomain_urls.py and put the URLconf that should be used for your subdomains/CNAMEs.
  5. Add SUBDOMAIN_URLCONF='yourproject.subdomain_urls' to settings.py.
  6. Add core.middleware.SubdomainMiddleware to MIDDLEWARE_CLASSES in settings.py (bottom of the list should work for most usages).
  7. Create some fake DNS entries in your hosts file (see below).
  8. Set up your slug/CNAME entries created in your model (i.e. for slugs, test1, test2. For CNAMEs, djangotest.christopher-williams.net, etc.)
  9. Open your fake subdomains in your browser.

Any hostname that doesn't have a corresponding slug/CNAME will be routed through your ROOT_URLCONF. Otherwise, you should see what's in your SUBDOMAIN_URLCONF.

The Code

/etc/hosts:

[...]

# Fake DNS entries for testing the subdomain middleware
127.0.0.1       test1.django.local
127.0.0.1       test2.django.local
127.0.0.1       djangotest.christopher-williams.net
127.0.0.1       django.local

middleware.py:

from django.utils.cache import patch_vary_headers
from django.conf import settings
from django.db.models import Q

from account.models import Account

class SubdomainMiddleware:
    def process_request(self, request):
        # Strip off the port, mostly useful for development environments
        fqdn = request.get_host().split(':')[0]

        # Break up the domain into parts and get the subdomain slug
        domain_parts = fqdn.split('.')
        if len(domain_parts) > 2:
            subdomain = domain_parts[0]
            if subdomain.lower() == 'www':
                subdomain = None
        else:
            subdomain = None

        try:
            request.account = Account.objects.get(
                Q(cname=fqdn) |
                Q(slug=subdomain)
            )
        except Account.DoesNotExist:
            pass
        else:
            request.urlconf = settings.SUBDOMAIN_URLCONF

    def process_response(self, request, response):
        patch_vary_headers(response, ('Host',))
        return response