Permalink
Browse files

Initial version.

  • Loading branch information...
carljm committed May 29, 2011
0 parents commit 8735f1dc8fb47652b2b815387de38502d37e2adc
Showing with 632 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +1 −0 AUTHORS.rst
  3. +8 −0 CHANGES.rst
  4. +28 −0 LICENSE.txt
  5. +5 −0 MANIFEST.in
  6. +61 −0 README.rst
  7. +6 −0 TODO.rst
  8. +1 −0 djangosecure/__init__.py
  9. +24 −0 djangosecure/conf.py
  10. +25 −0 djangosecure/middleware.py
  11. 0 djangosecure/models.py
  12. +219 −0 djangosecure/test_utils.py
  13. +132 −0 djangosecure/tests.py
  14. +35 −0 runtests.py
  15. +46 −0 setup.py
  16. +37 −0 tox.ini
@@ -0,0 +1,4 @@
+.tox/
+htmlcov/
+*.egg
+django_secure*.egg-info
@@ -0,0 +1 @@
+Carl Meyer <carl@oddbird.net>
@@ -0,0 +1,8 @@
+CHANGES
+=======
+
+0.1.0 (2011.05.29)
+------------------
+
+* Initial release.
+
@@ -0,0 +1,28 @@
+Copyright (c) 2011, Carl Meyer
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,5 @@
+include AUTHORS.rst
+include CHANGES.rst
+include LICENSE.txt
+include README.rst
+include TODO.rst
@@ -0,0 +1,61 @@
+=============
+django-secure
+=============
+
+Utilities for running a secure Django site (where all URLs in the site should
+be accessed over an HTTPS connection).
+
+Quickstart
+==========
+
+Dependencies
+------------
+
+Tested with `Django`_ 1.2 and trunk, and `Python`_ 2.5 through 2.7. Almost
+certainly works with older versions of Django, though.
+
+.. _Django: http://www.djangoproject.com/
+.. _Python: http://www.python.org/
+
+Installation
+------------
+
+Install from PyPI with ``pip``::
+
+ pip install django-secure
+
+or get the `in-development version`_::
+
+ pip install django-secure==dev
+
+.. _in-development version: https://github.com/carljm/django-secure/tarball/master#egg=django_secure-dev
+
+Usage
+-----
+
+* Add ``"djangosecure.middleware.SecurityMiddleware"`` to your
+ ``MIDDLEWARE_CLASSES`` setting (where depends on your other middlewares, but
+ near the beginning of the list is probably a good choice).
+
+* Set the ``SECURE_SSL_REDIRECT`` setting to True if all non-SSL requests
+ should be permanently redirected to SSL.
+
+* Set the ``SECURE_STS_SECONDS`` setting to an integer number of seconds, if
+ you want to use `Strict Transport Security`_.
+
+* Set ``SESSION_COOKIE_SECURE`` and ``SESSION_COOKIE_HTTPONLY`` to ``True`` if
+ you are using ``django.contrib.sessions``. These settings are not part of
+ ``django-secure``, but they should be used if running a secure site, and the
+ ``checksecurity`` management command will check their values.
+
+* Run ``python manage.py checksecurity`` to verify that your settings are
+ properly configured for serving a secure SSL site.
+
+.. _Strict Transport Security: http://en.wikipedia.org/wiki/Strict_Transport_Security
+
+Documentation
+-------------
+
+See the `full documentation`_ for more details.
+
+.. _full documentation: http://django-secure.readthedocs.org
@@ -0,0 +1,6 @@
+TODO
+====
+
+* checksecure management command (SESSION_COOKIE_SECURE,
+ SESSION_COOKIE_HTTPONLY, SECURE_STS_SECONDS, SECURE_SSL_REDIRECT,
+ SECURE_FRAME_DENY, SecurityMiddleware)
@@ -0,0 +1 @@
+__version__ = "0.1.0a1"
@@ -0,0 +1,24 @@
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+
+
+class Configuration(object):
+ def __init__(self, **kwargs):
+ self.defaults = kwargs
+
+
+ def __getattr__(self, k):
+ try:
+ return getattr(settings, k)
+ except AttributeError:
+ if k in self.defaults:
+ return self.defaults[k]
+ raise ImproperlyConfigured("django-secure requires %s setting." % k)
+
+
+conf = Configuration(
+ SECURE_STS_SECONDS=0,
+ SECURE_FRAME_DENY=True,
+ SECURE_SSL_REDIRECT=False,
+ )
@@ -0,0 +1,25 @@
+from django.http import HttpResponsePermanentRedirect
+
+from .conf import conf
+
+
+class SecurityMiddleware(object):
+ def __init__(self):
+ self.sts_seconds = conf.SECURE_STS_SECONDS
+ self.frame_deny = conf.SECURE_FRAME_DENY
+ self.redirect = conf.SECURE_SSL_REDIRECT
+
+
+ def process_request(self, request):
+ if self.redirect and not request.is_secure():
+ return HttpResponsePermanentRedirect(
+ "https://%s%s" % (request.get_host(), request.get_full_path()))
+
+
+ def process_response(self, request, response):
+ if self.frame_deny and not 'x-frame-options' in response:
+ response["x-frame-options"] = "DENY"
+ if self.sts_seconds and not 'strict-transport-security' in response:
+ response["strict-transport-security"] = ("max-age=%s"
+ % self.sts_seconds)
+ return response
No changes.
@@ -0,0 +1,219 @@
+"""
+Testing utilities backported from recent Django versions, for testing with
+older Django versions.
+
+"""
+from __future__ import with_statement
+
+from cStringIO import StringIO
+import urllib
+from urlparse import urlparse
+
+from django.conf import settings, UserSettingsHolder
+from django.core.handlers.wsgi import WSGIRequest
+from django.http import SimpleCookie
+from django.test.client import (
+ encode_multipart, FakePayload, BOUNDARY, MULTIPART_CONTENT, CONTENT_TYPE_RE)
+from django.utils.encoding import smart_str
+from django.utils.functional import wraps
+from django.utils.http import urlencode
+
+
+
+class override_settings(object):
+ """
+ Acts as either a decorator, or a context manager. If it's a decorator it
+ takes a function and returns a wrapped function. If it's a contextmanager
+ it's used with the ``with`` statement. In either event entering/exiting
+ are called before and after, respectively, the function/block is executed.
+
+ """
+ def __init__(self, **kwargs):
+ self.options = kwargs
+ self.wrapped = settings._wrapped
+
+ def __enter__(self):
+ self.enable()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.disable()
+
+ def __call__(self, func):
+ @wraps(func)
+ def inner(*args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+ return inner
+
+ def enable(self):
+ override = UserSettingsHolder(settings._wrapped)
+ for key, new_value in self.options.items():
+ setattr(override, key, new_value)
+ settings._wrapped = override
+
+ def disable(self):
+ settings._wrapped = self.wrapped
+
+
+
+class RequestFactory(object):
+ """
+ Class that lets you create mock Request objects for use in testing.
+
+ Usage:
+
+ rf = RequestFactory()
+ get_request = rf.get('/hello/')
+ post_request = rf.post('/submit/', {'foo': 'bar'})
+
+ Once you have a request object you can pass it to any view function,
+ just as if that view had been hooked up using a URLconf.
+ """
+ def __init__(self, **defaults):
+ self.defaults = defaults
+ self.cookies = SimpleCookie()
+ self.errors = StringIO()
+
+ def _base_environ(self, **request):
+ """
+ The base environment for a request.
+ """
+ environ = {
+ 'HTTP_COOKIE': self.cookies.output(header='', sep='; '),
+ 'PATH_INFO': '/',
+ 'QUERY_STRING': '',
+ 'REMOTE_ADDR': '127.0.0.1',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': 'testserver',
+ 'SERVER_PORT': '80',
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ 'wsgi.version': (1,0),
+ 'wsgi.url_scheme': 'http',
+ 'wsgi.errors': self.errors,
+ 'wsgi.multiprocess': True,
+ 'wsgi.multithread': False,
+ 'wsgi.run_once': False,
+ }
+ environ.update(self.defaults)
+ environ.update(request)
+ return environ
+
+ def request(self, **request):
+ "Construct a generic request object."
+ return WSGIRequest(self._base_environ(**request))
+
+ def _get_path(self, parsed):
+ # If there are parameters, add them
+ if parsed[3]:
+ return urllib.unquote(parsed[2] + ";" + parsed[3])
+ else:
+ return urllib.unquote(parsed[2])
+
+ def get(self, path, data={}, **extra):
+ "Construct a GET request"
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_TYPE': 'text/html; charset=utf-8',
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'GET',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def post(self, path, data={}, content_type=MULTIPART_CONTENT,
+ **extra):
+ "Construct a POST request."
+
+ if content_type is MULTIPART_CONTENT:
+ post_data = encode_multipart(BOUNDARY, data)
+ else:
+ # Encode the content so that the byte representation is correct.
+ match = CONTENT_TYPE_RE.match(content_type)
+ if match:
+ charset = match.group(1)
+ else:
+ charset = settings.DEFAULT_CHARSET
+ post_data = smart_str(data, encoding=charset)
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_LENGTH': len(post_data),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': parsed[4],
+ 'REQUEST_METHOD': 'POST',
+ 'wsgi.input': FakePayload(post_data),
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def head(self, path, data={}, **extra):
+ "Construct a HEAD request."
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_TYPE': 'text/html; charset=utf-8',
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'HEAD',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def options(self, path, data={}, **extra):
+ "Constrict an OPTIONS request"
+
+ parsed = urlparse(path)
+ r = {
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'OPTIONS',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def put(self, path, data={}, content_type=MULTIPART_CONTENT,
+ **extra):
+ "Construct a PUT request."
+
+ if content_type is MULTIPART_CONTENT:
+ post_data = encode_multipart(BOUNDARY, data)
+ else:
+ post_data = data
+
+ # Make `data` into a querystring only if it's not already a string. If
+ # it is a string, we'll assume that the caller has already encoded it.
+ query_string = None
+ if not isinstance(data, basestring):
+ query_string = urlencode(data, doseq=True)
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_LENGTH': len(post_data),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': query_string or parsed[4],
+ 'REQUEST_METHOD': 'PUT',
+ 'wsgi.input': FakePayload(post_data),
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def delete(self, path, data={}, **extra):
+ "Construct a DELETE request."
+
+ parsed = urlparse(path)
+ r = {
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'DELETE',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
Oops, something went wrong.

0 comments on commit 8735f1d

Please sign in to comment.