import re

from django import http
from django.apps import apps
from django.utils.cache import patch_vary_headers
from django.utils.six.moves.urllib.parse import urlparse

from .compat import MiddlewareMixin
from .conf import conf
from .signals import check_request_enabled

ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods'
ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age'


class CorsPostCsrfMiddleware(MiddlewareMixin):

    def _https_referer_replace_reverse(self, request):
        """
        Put the HTTP_REFERER back to its original value and delete the
        temporary storage
        """
        if conf.CORS_REPLACE_HTTPS_REFERER and 'ORIGINAL_HTTP_REFERER' in request.META:
            http_referer = request.META['ORIGINAL_HTTP_REFERER']
            request.META['HTTP_REFERER'] = http_referer
            del request.META['ORIGINAL_HTTP_REFERER']

    def process_request(self, request):
        self._https_referer_replace_reverse(request)
        return None

    def process_view(self, request, callback, callback_args, callback_kwargs):
        self._https_referer_replace_reverse(request)
        return None


class CorsMiddleware(MiddlewareMixin):

    def _https_referer_replace(self, request):
        """
        When https is enabled, django CSRF checking includes referer checking
        which breaks when using CORS. This function updates the HTTP_REFERER
        header to make sure it matches HTTP_HOST, provided that our cors logic
        succeeds
        """
        origin = request.META.get('HTTP_ORIGIN')

        if request.is_secure() and origin and 'ORIGINAL_HTTP_REFERER' not in request.META:

            url = urlparse(origin)
            if not conf.CORS_ORIGIN_ALLOW_ALL and not self.origin_found_in_white_lists(origin, url):
                return

            try:
                http_referer = request.META['HTTP_REFERER']
                http_host = "https://%s/" % request.META['HTTP_HOST']
                request.META = request.META.copy()
                request.META['ORIGINAL_HTTP_REFERER'] = http_referer
                request.META['HTTP_REFERER'] = http_host
            except KeyError:
                pass

    def process_request(self, request):
        """
        If CORS preflight header, then create an
        empty body response (200 OK) and return it

        Django won't bother calling any other request
        view/exception middleware along with the requested view;
        it will call any response middlewares
        """
        request._cors_enabled = self.is_enabled(request)
        if request._cors_enabled:
            if conf.CORS_REPLACE_HTTPS_REFERER:
                self._https_referer_replace(request)

            if (
                request.method == 'OPTIONS' and
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META
            ):
                return http.HttpResponse()

    def process_view(self, request, callback, callback_args, callback_kwargs):
        """
        Do the referer replacement here as well
        """
        if request._cors_enabled and conf.CORS_REPLACE_HTTPS_REFERER:
            self._https_referer_replace(request)
        return None

    def process_response(self, request, response):
        """
        Add the respective CORS headers
        """
        origin = request.META.get('HTTP_ORIGIN')
        if not origin:
            return response

        enabled = getattr(request, '_cors_enabled', None)
        if enabled is None:
            enabled = self.is_enabled(request)

        if not enabled:
            return response

        # todo: check hostname from db instead
        url = urlparse(origin)

        if conf.CORS_MODEL is not None:
            model = apps.get_model(*conf.CORS_MODEL.split('.'))
            if model.objects.filter(cors=url.netloc).exists():
                response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin

        if conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true'

        if (
            not conf.CORS_ORIGIN_ALLOW_ALL and
            not self.origin_found_in_white_lists(origin, url) and
            not self.check_signal(request)
        ):
            return response

        if conf.CORS_ORIGIN_ALLOW_ALL and not conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
            patch_vary_headers(response, ['Origin'])

        if len(conf.CORS_EXPOSE_HEADERS):
            response[ACCESS_CONTROL_EXPOSE_HEADERS] = ', '.join(conf.CORS_EXPOSE_HEADERS)

        if request.method == 'OPTIONS':
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ', '.join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ', '.join(conf.CORS_ALLOW_METHODS)
            if conf.CORS_PREFLIGHT_MAX_AGE:
                response[ACCESS_CONTROL_MAX_AGE] = conf.CORS_PREFLIGHT_MAX_AGE

        return response

    def origin_found_in_white_lists(self, origin, url):
        return (
            url.netloc in conf.CORS_ORIGIN_WHITELIST or
            self.regex_domain_match(origin)
        )

    def regex_domain_match(self, origin):
        for domain_pattern in conf.CORS_ORIGIN_REGEX_WHITELIST:
            if re.match(domain_pattern, origin):
                return origin

    def is_enabled(self, request):
        return (
            re.match(conf.CORS_URLS_REGEX, request.path) or
            self.check_signal(request)
        )

    def check_signal(self, request):
        signal_responses = check_request_enabled.send(
            sender=None,
            request=request,
        )
        return any(
            return_value for
            function, return_value in signal_responses
        )

corsheaders/middleware.py
import re

from django import http
from django.apps import apps
from django.utils.cache import patch_vary_headers
from django.utils.six.moves.urllib.parse import urlparse

from .compat import MiddlewareMixin
from .conf import conf
from .signals import check_request_enabled

ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods'
ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age'


class CorsPostCsrfMiddleware(MiddlewareMixin):

    def _https_referer_replace_reverse(self, request):
        """
        Put the HTTP_REFERER back to its original value and delete the
        temporary storage
        """
        if conf.CORS_REPLACE_HTTPS_REFERER and 'ORIGINAL_HTTP_REFERER' in request.META:
            http_referer = request.META['ORIGINAL_HTTP_REFERER']
            request.META['HTTP_REFERER'] = http_referer
            del request.META['ORIGINAL_HTTP_REFERER']

    def process_request(self, request):
        self._https_referer_replace_reverse(request)
        return None

    def process_view(self, request, callback, callback_args, callback_kwargs):
        self._https_referer_replace_reverse(request)
        return None


class CorsMiddleware(MiddlewareMixin):

    def _https_referer_replace(self, request):
        """
        When https is enabled, django CSRF checking includes referer checking
        which breaks when using CORS. This function updates the HTTP_REFERER
        header to make sure it matches HTTP_HOST, provided that our cors logic
        succeeds
        """
        origin = request.META.get('HTTP_ORIGIN')

        if request.is_secure() and origin and 'ORIGINAL_HTTP_REFERER' not in request.META:

            url = urlparse(origin)
            if not conf.CORS_ORIGIN_ALLOW_ALL and not self.origin_found_in_white_lists(origin, url):
                return

            try:
                http_referer = request.META['HTTP_REFERER']
                http_host = "https://%s/" % request.META['HTTP_HOST']
                request.META = request.META.copy()
                request.META['ORIGINAL_HTTP_REFERER'] = http_referer
                request.META['HTTP_REFERER'] = http_host
            except KeyError:
                pass

    def process_request(self, request):
        """
        If CORS preflight header, then create an
        empty body response (200 OK) and return it

        Django won't bother calling any other request
        view/exception middleware along with the requested view;
        it will call any response middlewares
        """
        request._cors_enabled = self.is_enabled(request)
        if request._cors_enabled:
            if conf.CORS_REPLACE_HTTPS_REFERER:
                self._https_referer_replace(request)

            if (
                request.method == 'OPTIONS' and
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META
            ):
                return http.HttpResponse()

    def process_view(self, request, callback, callback_args, callback_kwargs):
        """
        Do the referer replacement here as well
        """
        if request._cors_enabled and conf.CORS_REPLACE_HTTPS_REFERER:
            self._https_referer_replace(request)
        return None

    def process_response(self, request, response):
        """
        Add the respective CORS headers
        """
        origin = request.META.get('HTTP_ORIGIN')
        if not origin:
            return response

        enabled = getattr(request, '_cors_enabled', None)
        if enabled is None:
            enabled = self.is_enabled(request)

        if not enabled:
            return response

        # todo: check hostname from db instead
        url = urlparse(origin)

        if conf.CORS_MODEL is not None:
            model = apps.get_model(*conf.CORS_MODEL.split('.'))
            if model.objects.filter(cors=url.netloc).exists():
                response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin

        if conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true'

        if (
            not conf.CORS_ORIGIN_ALLOW_ALL and
            not self.origin_found_in_white_lists(origin, url) and
            not self.check_signal(request)
        ):
            return response

        if conf.CORS_ORIGIN_ALLOW_ALL and not conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
            patch_vary_headers(response, ['Origin'])

        if len(conf.CORS_EXPOSE_HEADERS):
            response[ACCESS_CONTROL_EXPOSE_HEADERS] = ', '.join(conf.CORS_EXPOSE_HEADERS)

        if request.method == 'OPTIONS':
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ', '.join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ', '.join(conf.CORS_ALLOW_METHODS)
            if conf.CORS_PREFLIGHT_MAX_AGE:
                response[ACCESS_CONTROL_MAX_AGE] = conf.CORS_PREFLIGHT_MAX_AGE

        return response

    def origin_found_in_white_lists(self, origin, url):
        return (
            url.netloc in conf.CORS_ORIGIN_WHITELIST or
            (origin == 'null' and origin in conf.CORS_ORIGIN_WHITELIST) or
            self.regex_domain_match(origin)
        )

    def regex_domain_match(self, origin):
        for domain_pattern in conf.CORS_ORIGIN_REGEX_WHITELIST:
            if re.match(domain_pattern, origin):
                return origin

    def is_enabled(self, request):
        return (
            re.match(conf.CORS_URLS_REGEX, request.path) or
            self.check_signal(request)
        )

    def check_signal(self, request):
        signal_responses = check_request_enabled.send(
            sender=None,
            request=request,
        )
        return any(
            return_value for
            function, return_value in signal_responses
        )

corsheaders/middleware.py
diff --git a/HISTORY.rst b/HISTORY.rst
index d7c5fec3..5a7f306f 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -5,6 +5,7 @@ Pending
 -------
 
 * New release notes go here.
+* Add support for specifying 'null' in ``CORS_ORIGIN_WHITELIST``.
 
 2.0.0 (2017-01-07)
 ------------------
diff --git a/README.rst b/README.rst
index 1914ff3a..b5834fb4 100644
--- a/README.rst
+++ b/README.rst
@@ -89,6 +89,11 @@ Example:
         '127.0.0.1:9000'
     )
 
+The value ``'null'`` can also appear in this list, and will match the
+``Origin: null`` header that is used in `some circumstances
+<https://tools.ietf.org/html/rfc6454#section-6>`_ (in particular,
+when the client is running from a ``file://`` domain).
+
 ``CORS_ORIGIN_REGEX_WHITELIST``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/corsheaders/middleware.py b/corsheaders/middleware.py
index 8639aec7..48c30895 100755
--- a/corsheaders/middleware.py
+++ b/corsheaders/middleware.py
@@ -145,6 +145,7 @@ def process_response(self, request, response):
     def origin_found_in_white_lists(self, origin, url):
         return (
             url.netloc in conf.CORS_ORIGIN_WHITELIST or
+            (origin == 'null' and origin in conf.CORS_ORIGIN_WHITELIST) or
             self.regex_domain_match(origin)
         )
 
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 02352c9e..de26f5fe 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -35,6 +35,11 @@ def test_get_in_whitelist(self):
         resp = self.client.get('/', HTTP_ORIGIN='http://example.org')
         assert resp[ACCESS_CONTROL_ALLOW_ORIGIN] == 'http://example.org'
 
+    @override_settings(CORS_ORIGIN_WHITELIST=['example.com', 'null'])
+    def test_null_in_whitelist(self):
+        resp = self.client.get('/', HTTP_ORIGIN='null')
+        assert resp[ACCESS_CONTROL_ALLOW_ORIGIN] == 'null'
+
     @override_settings(
         CORS_ORIGIN_ALLOW_ALL=True,
         CORS_EXPOSE_HEADERS=['accept', 'origin', 'content-type'],

Support "null" origin
A PR for this (#101) was recently rejected with the comment "I couldn't find a source that said setting `Origin` to `null` is part of the standard". I want to at least document that this is indeed a first-class part of the standard.

The syntax for the `Origin` header is defined in [Section 7.1 of RFC 6454](https://tools.ietf.org/html/rfc6454#section-7.1). The ABNF there specifies the header's value as either the string `null` or a list of origins. Sections 4 and 6 define the circumstances under which this value should be set to `null`, which include using a `file://` origin.

Similarly, the W3C CORS specification [explicitly notes](https://www.w3.org/TR/cors/#resource-sharing-check-0) that the matching algorithm "also functions when the ASCII serialization of an origin is the string `null`".

In practical terms this isn't a very pressing concern, since you can just use `CORS_ORIGIN_REGEX_WHITELIST = (r"^null$",)`. That said, I think it would make sense to allow the value to be used in the whitelist directly.
