diff --git a/CHANGES b/CHANGES
deleted file mode 100644
index 63d594c..0000000
--- a/CHANGES
+++ /dev/null
@@ -1,110 +0,0 @@
-0.6.2
-=====
-- Add and test support for Django 2.2 [@chrisledet]
-- Added support for redis client 3.2.1 [@softinio]
-
-0.6.1
-=====
-- added redispy 3.2.0 compatibility [@nrth]
-
-0.6.0
-=====
-- remove Python 3.3 [@fr0mhell]
-- remove Django 1.8-1.10 [@fr0mhell]
-- add Celery v4 [@fr0mhell]
-- update travis config [@fr0mhell]
-- update admin URL [@fr0mhell]
-
-0.5.5
-=====
-- Added new setting ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH` for control how username is accessed from request [@andrewshkovskii]
-- Added new argument ``get_username` for ``decorators.watch_login`` to propagate ``get_username`` argument to other utils functions calls done in ``watch_login`` [@andrewshkovskii]
-
-0.5.4
-=====
-- Added 2 new setting variables for more granular failure limit control [@williamboman]
-- Added ssl option when instantiating StrictRedis [@mjrimrie]
-- Send signals when blocking username or ip [@williamboman]
-
-0.5.3
-=====
-- Remove mockredis as install requirement, make only test requirement [@blueyed]
-
-0.5.2
-=====
-- Fix regex in 'unblock_username_view' to handle special symbols [@ruthus18]
-- Fix django requires version for 1.11.x [@kencochrane]
-- remove hiredis dependency [@ericbuckley]
-- Correctly get raw client when using django_redis cache. [@cburger]
-- replace django.core.urlresolvers with django.urls For Django 2.0 [@s-wirth]
-- add username kwarg for providing username directly rather than via callback arg [@williamboman]
-- Only use the username if it is actually provided [@cobusc]
-
-0.5.1
-=====
-- Middleware fix for django >= 1.10 #93 [@Temeez]
-- Force the username to lowercase #90 [@MattBlack85]
-
-0.5.0
-=====
-- Better support for Django 1.11 [@dukebody]
-- Added support to share redis config with django.core.cache [@Franr]
-- Allow decoration of functions beyond the admin login [@MattBlack85]
-- Doc improvements [@dukebody]
-- Allow usernames with plus signs in unblock view [@dukebody]
-- Code cleanup [@KenCochrane]
-
-0.4.3
-=====
-- Flex version requirements for dependencies
-- Better support for Django 1.10
-
-0.4.2
-=====
-Better support for Django 1.9
-
-0.4.1
-=====
-minor refactor to make it easier to retrieve username.
-
-0.4.0
-=====
-added ``DEFENDER_DISABLE_IP_LOCKOUT`` and added support for Python 3.5
-
-0.3.2
-=====
-added ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``, and changed settings to support
-django 1.8.
-
-0.3.1
-=====
-fixed the management command name
-
-0.3
-===
-
-- Added management command ``cleanup_django_defender`` to clean up access
- attempt table.
-- Added ``DEFENDER_STORE_ACCESS_ATTEMPTS`` config to say if you want to
- store attempts to DB or not.
-- Added ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` config to specify how long
- to store the access attempt records in the db, before the management command
- cleans them up.
-- changed the Django admin page to remove some filters which were making the
- page load slow with lots of login attempts in the database.
-
-0.2.2
-=====
-Another bug fix release for more missing files in distribution
-
-0.2.1
-=====
-Bug fixes for packing missing files
-
-0.2
-===
-Added fixes to include possible security issue
-
-0.1
-===
-Initial Version
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..5e9364c
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,146 @@
+
+Changes
+=======
+
+
+0.6.2
+-----
+
+- Add and test support for Django 2.2 [@chrisledet]
+- Add support for redis client 3.2.1 [@softinio]
+
+
+0.6.1
+-----
+
+- Add redispy 3.2.0 compatibility [@nrth]
+
+
+0.6.0
+-----
+
+- Remove Python 3.3 [@fr0mhell]
+- Remove Django 1.8-1.10 [@fr0mhell]
+- Add Celery v4 [@fr0mhell]
+- Update travis config [@fr0mhell]
+- Update admin URL [@fr0mhell]
+
+
+0.5.5
+-----
+
+- Add new setting ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH`` for control how username is accessed from request [@andrewshkovskii]
+- Add new argument ``get_username`` for ``decorators.watch_login`` to propagate ``get_username`` argument to other utils functions calls done in ``watch_login`` [@andrewshkovskii]
+
+
+0.5.4
+-----
+
+- Add 2 new setting variables for more granular failure limit control [@williamboman]
+- Add ssl option when instantiating StrictRedis [@mjrimrie]
+- Send signals when blocking username or ip [@williamboman]
+
+
+0.5.3
+-----
+
+- Remove mockredis as install requirement, make only test requirement [@blueyed]
+
+
+0.5.2
+-----
+
+- Fix regex in 'unblock_username_view' to handle special symbols [@ruthus18]
+- Fix django requires version for 1.11.x [@kencochrane]
+- Remove hiredis dependency [@ericbuckley]
+- Correctly get raw client when using django_redis cache. [@cburger]
+- Replace django.core.urlresolvers with django.urls For Django 2.0 [@s-wirth]
+- Add username kwarg for providing username directly rather than via callback arg [@williamboman]
+- Only use the username if it is actually provided [@cobusc]
+
+
+0.5.1
+-----
+
+- Middleware fix for django >- 1.10 #93 [@Temeez]
+- Force the username to lowercase #90 [@MattBlack85]
+
+
+0.5.0
+-----
+
+- Better support for Django 1.11 [@dukebody]
+- Add support to share redis config with django.core.cache [@Franr]
+- Allow decoration of functions beyond the admin login [@MattBlack85]
+- Doc improvements [@dukebody]
+- Allow usernames with plus signs in unblock view [@dukebody]
+- Code cleanup [@KenCochrane]
+
+
+0.4.3
+-----
+
+- Flex version requirements for dependencies
+- Better support for Django 1.10
+
+
+0.4.2
+-----
+
+- Better support for Django 1.9
+
+
+0.4.1
+-----
+
+- Minor refactor to make it easier to retrieve username.
+
+
+0.4.0
+-----
+
+- Add ``DEFENDER_DISABLE_IP_LOCKOUT`` and added support for Python 3.5
+
+
+0.3.2
+-----
+
+- Add ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``, and changed settings to support django 1.8.
+
+
+0.3.1
+-----
+
+- Fix the management command name
+
+
+0.3
+---
+
+- Add management command ``cleanup_django_defender`` to clean up access attempt table.
+- Add ``DEFENDER_STORE_ACCESS_ATTEMPTS`` config to say if you want to store attempts to DB or not.
+- Add ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` config to specify how long to store the access attempt records in the db, before the management command cleans them up.
+- Change the Django admin page to remove some filters which were making the page load slow with lots of login attempts in the database.
+
+0.2.2
+-----
+
+- Another bug fix release for more missing files in distribution
+
+
+0.2.1
+-----
+
+- Bug fixes for packing missing files
+
+
+0.2
+---
+
+- Add fixes to include possible security issue
+
+
+0.1
+---
+
+- Initial Version
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index ad78220..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,3 +0,0 @@
-[](https://jazzband.co/)
-
-This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines).
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..5ff99f4
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,10 @@
+
+Contributing
+============
+
+.. image:: https://jazzband.co/static/img/jazzband.svg
+ :target: https://jazzband.co/
+ :alt: Jazzband
+
+
+This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_.
diff --git a/MANIFEST.in b/MANIFEST.in
index 036216d..e7998d1 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
-include CHANGES
-include README.md
+include CONTRIBUTING.rst
+include CHANGES.rst
+include README.rst
include LICENSE
recursive-include defender/templates *.html
recursive-exclude * *.py[co]
diff --git a/README.md b/README.md
deleted file mode 100644
index 1caea5a..0000000
--- a/README.md
+++ /dev/null
@@ -1,534 +0,0 @@
-django-defender
-===============
-
-[](https://jazzband.co/)
-[](https://travis-ci.org/jazzband/django-defender)
-[](https://coveralls.io/r/jazzband/django-defender)
-
-A simple Django reusable app that blocks people from brute forcing login
-attempts. The goal is to make this as fast as possible, so that we do not
-slow down the login attempts.
-
-We will use a cache so that it doesn't have to hit the database in order to
-check the database on each login attempt. The first version will be based on
-Redis, but the goal is to make this configurable so that people can use whatever
-backend best fits their needs.
-
-
-Sites using Defender
-====================
-
-If you are using defender on your site, submit a PR to add to the list.
-
-- https://hub.docker.com
-- https://www.mycosbuilder.com
-
-
-Versions
-========
-
-- 0.6.2
- - Add and test support for Django 2.2 [@chrisledet]
- - Added support for redis client 3.2.1 [@softinio]
-
-- 0.6.1
- - added redispy 3.2.0 compatibility [@nrth]
-
-- 0.6.0
- - remove Python 3.3 [@fr0mhell]
- - remove Django 1.8-1.10 [@fr0mhell]
- - add Celery v4 [@fr0mhell]
- - update travis config [@fr0mhell]
- - update admin URL [@fr0mhell]
-
-- 0.5.5
- - Added new setting ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH`` for control how username is accessed from request [@andrewshkovskii]
- - Added new argument ``get_username`` for ``decorators.watch_login`` to propagate ``get_username`` argument to other utils functions calls done in ``watch_login`` [@andrewshkovskii]
-
-- 0.5.4
- - Added 2 new setting variables for more granular failure limit control [@williamboman]
- - Added ssl option when instantiating StrictRedis [@mjrimrie]
- - Send signals when blocking username or ip [@williamboman]
-
-- 0.5.3
- - Remove mockredis as install requirement, make only test requirement [@blueyed]
-
-- 0.5.2
- - Fix regex in 'unblock_username_view' to handle special symbols [@ruthus18]
- - Fix django requires version for 1.11.x [@kencochrane]
- - remove hiredis dependency [@ericbuckley]
- - Correctly get raw client when using django_redis cache. [@cburger]
- - replace django.core.urlresolvers with django.urls For Django 2.0 [@s-wirth]
- - add username kwarg for providing username directly rather than via callback arg [@williamboman]
- - Only use the username if it is actually provided [@cobusc]
-
-- 0.5.1
- - Middleware fix for django >= 1.10 #93 [@Temeez]
- - Force the username to lowercase #90 [@MattBlack85]
-
-- 0.5.0
- - Better support for Django 1.11 [@dukebody]
- - Added support to share redis config with django.core.cache [@Franr]
- - Allow decoration of functions beyond the admin login [@MattBlack85]
- - Doc improvements [@dukebody]
- - Allow usernames with plus signs in unblock view [@dukebody]
- - Code cleanup [@KenCochrane]
-- 0.4.3 - Better Support for Django 1.10
-- 0.4.2 - Better support for Django 1.9
-- 0.4.1 - minor refactor to make it easier to retrieve username.
-- 0.4.0 - added ``DEFENDER_DISABLE_IP_LOCKOUT`` and added support for Python 3.5
-- 0.3.2 - added ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``, and changed settings
- to support django 1.8.
-- 0.3.1 - fixed the management command name
-- 0.3
- - Added management command ``cleanup_django_defender`` to clean up access
- attempt table.
- - Added ``DEFENDER_STORE_ACCESS_ATTEMPTS`` config to say if you want to
- store attempts to DB or not.
- - Added ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` config to specify how long
- to store the access attempt records in the db, before the management command
- cleans them up.
- - changed the Django admin page to remove some filters which were making the
- page load slow with lots of login attempts in the database.
-- 0.2.2 - bug fix add missing files to pypi package
-- 0.2.1 - bug fix
-- 0.2 - security fix for XFF headers
-- 0.1.1 - setup.py fix
-- 0.1 - initial release
-
-
-Features
-========
-
-- Log all login attempts to the database
-- support for reverse proxies with different headers for IP addresses
-- rate limit based on:
- - username
- - ip address
-- use redis for the blacklist
-- configuration
- - redis server
- - host
- - port
- - database
- - password
- - key_prefix
- - block length
- - number of incorrect attempts before block
-- 95% code coverage
-- full documentation
-- Ability to store login attempts to the database
-- Management command to clean up login attempts database table
-- admin pages
- - list of blocked usernames and ip's
- - ability to unblock people
- - list of recent login attempts
-- Can be easily adapted to custom authentication method.
-- Signals are sent when blocking username or IP
-
-
-Long term goals
-===============
-
-- pluggable backends, so people can use something other then redis.
-- email users when their account is blocked
-- add a whitelist for username and ip's that we will never block (admin's, etc)
-- add a permanent black list
- - ip address
-- scan for known proxy ip's and don't block requests coming from those
-(improve the chances that a good IP is blocked)
-- add management command to prune old (configurable) login attempts.
-
-
-Performance
-===========
-
-The goal of defender is to make it as fast as possible so that it doesn't slow
-down the login process. In order to make sure our goals are met we need a way
-to test the application to make sure we are on the right track. The best
-way to do this is to compare how fast a normal Django login takes with defender
-and django-axes.
-
-The normal django login, would be our baseline, and we expect it to be the
-fastest of the 3 methods, because there are no additional checks happening.
-
-The defender login would most likely be slower then the django login, and
-hopefully faster then the django-axes login. The goal is to make it as little
-of a difference between the regular raw login, and defender.
-
-The django-axes login speed, will probably be the slowest of the three since it
-does more checks and does a lot of database queries.
-
-The best way to determine the speed of a login is to do a load test against an
-application with each setup, and compare the login times for each type.
-
-
-Types of Load tests
--------------------
-In order to make sure we cover all the different types of logins, in our load
-test we need to have more then one test.
-
-1. All success:
- - We will do a load test with nothing but successful logins
-2. Mixed: some success some failure:
- - We will load test with some successful logins and some failures to see how
- the failure effect the performance.
-3. All Failures:
- - We will load test with all failure logins and see the difference in
- performance.
-
-We will need a sample application that we can use for the load test, with the
-only difference is the configuration where we either load defender, axes, or
-none of them.
-
-We can use a hosted load testing service, or something like jmeter. Either way
-we need to be consistent for all of the tests. If we use jmeter, we should have
-our jmeter configuration for others to run the tests on their own.
-
-
-Results
--------
-We will post the results here. We will explain each test, and show the results
-along with some charts.
-
-
-Why not django-axes
-===================
-
-django-axes is great but it puts everything in the database, and this causes
-a bottle neck when you have a lot of data. It slows down the auth requests by
-as much as 200-300ms. This might not be much for some sites, but for others it
-is too long.
-
-This started out as a fork of django-axes, and is using as much of their code
-as possible, and removing the parts not needed, and speeding up the lookups
-to improve the login.
-
-
-Requirements
-============
-
-- django: 1.8.x, 1.9.x, 1.10.x, 1.11.x
-- redis
-- python: 2.7.x, 3.3.x, 3.4.x, 3.5.x, 3.6.x, PyPy
-
-
-How it works
-============
-
-1. When someone tries to login, we first check to see if they are currently
-blocked. We check the username they are trying to use, as well as the IP
-address. If they are blocked, goto step 5. If not blocked go to step 2.
-
-2. They are not blocked, so we check to see if the login was valid. If valid
-go to step 6. If not valid go to step 3.
-
-3. Login attempt wasn't valid. Add their username and IP address for this
-attempt to the cache. If this brings them over the limit, add them to the
-blocked list, and then goto step 5. If not over the limit goto step 4.
-
-4. login was invalid, but not over the limit. Send them back to the login screen
-to try again.
-
-5. User is blocked: Send them to the blocked page, telling them they are
-blocked, and give an estimate on when they will be unblocked.
-
-6. Login is valid. Reset any failed login attempts, and forward to their
-destination.
-
-
-Cache backend
-=============
-
-Defender uses the cache to save the failed attempts.
-
-Cache keys
-----------
-
-Counters:
-
-- prefix:failed:ip:[ip] (count, TTL)
-- prefix:failed:username:[username] (count, TTL)
-
-Booleans (if present it is blocked):
-
-- prefix:blocked:ip:[ip] (true, TTL)
-- prefix:blocked:username:[username] (true, TTL)
-
-
-Installing Django-defender
-==========================
-
-Download code, and run setup.
-
-```
- $ pip install django-defender
-
- or
-
- $ python setup.py install
-
- or
-
- $ pip install -e git+http://github.com/kencochrane/django-defender.git#egg=django_defender-dev
-
-```
-
-First of all, you must add this project to your list of ``INSTALLED_APPS`` in
-``settings.py``::
-
-```
-INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- # ...
- 'defender',
- # ...
-]
-```
-
-Next, install the ``FailedLoginMiddleware`` middleware::
-
-```
-MIDDLEWARE_CLASSES = [
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'defender.middleware.FailedLoginMiddleware',
-]
-```
-
-If you want to manage the blocked users via the Django admin, then add the
-following to your ``urls.py``
-
-```
-urlpatterns = [
- url(r'^admin/', include(admin.site.urls)), # normal admin
- url(r'^admin/defender/', include('defender.urls')), # defender admin
- # your own patterns follow...
-]
-```
-
-
-Management Commands
--------------------
-
-``cleanup_django_defender``
-
-If you have a website with a lot of traffic, the AccessAttempts table will get
-full pretty quickly. If you don't need to keep the data for auditing purposes
-there is a management command to help you keep it clean.
-
-It will look at your ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` setting to determine
-which records will be deleted. Default if not specified, is 24 hours.
-
-```bash
-$ python manage.py cleanup_django_defender
-```
-
-You can set this up as a daily or weekly cron job to keep the table size down.
-
-```bash
-# run at 12:24 AM every morning.
-24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log
-```
-
-
-Admin Pages
------------
-
-
-
-
-
-
-Database tables
----------------
-
-You will need to create tables in your database that are necessary
-for operation.
-
-```bash
-python manage.py migrate defender
-```
-
-Customizing Defender
---------------------
-
-You have a couple options available to you to customize ``django-defender`` a bit.
-These should be defined in your ``settings.py`` file.
-
-* ``DEFENDER_LOGIN_FAILURE_LIMIT``: Int: The number of login attempts allowed before a
-record is created for the failed logins. [Default: ``3``]
-* ``DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME``: Int: The number of login attempts allowed
-on a username before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``]
-* ``DEFENDER_LOGIN_FAILURE_LIMIT_IP``: Int: The number of login attempts allowed
-from an IP before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``]
-* ``DEFENDER_BEHIND_REVERSE_PROXY``: Boolean: Is defender behind a reverse proxy?
-[Default: ``False``]
-* ``DEFENDER_REVERSE_PROXY_HEADER``: String: the name of the http header with your
-reverse proxy IP address [Default: ``HTTP_X_FORWARDED_FOR``]
-* ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``: Boolean: Locks a user out based on a combination of IP and Username. This stops a user denying access to the application for all other users accessing the app from behind the same IP address. [Default: ``False``]
-* ``DEFENDER_DISABLE_IP_LOCKOUT``: Boolean: If this is True, it will not lockout the users IP address, it will only lockout the username. [Default: False]
-* ``DEFENDER_DISABLE_USERNAME_LOCKOUT``: Boolean: If this is True, it will not lockout usernames, it will only lockout IP addresess. [Default: False]
-* ``DEFENDER_COOLOFF_TIME``: Int: If set, defines a period of inactivity after which
-old failed login attempts will be forgotten. An integer, will be interpreted as a
-number of seconds. If ``0``, the locks will not expire. [Default: ``300``]
-* ``DEFENDER_LOCKOUT_TEMPLATE``: String: [Default: ``None``] If set, specifies a template to render when a user is locked out. Template receives the following context variables:
- - ``cooloff_time_seconds``: The cool off time in seconds
- - ``cooloff_time_minutes``: The cool off time in minutes
- - ``failure_limit``: The number of failures before you get blocked.
-* ``DEFENDER_USERNAME_FORM_FIELD``: String: the name of the form field that contains your
-users usernames. [Default: ``username``]
-* ``DEFENDER_CACHE_PREFIX``: String: The cache prefix for your defender keys.
-[Default: ``defender``]
-* ``DEFENDER_LOCKOUT_URL``: String: The URL you want to redirect to if someone is
-locked out.
-* ``DEFENDER_REDIS_URL``: String: the redis url for defender.
-[Default: ``redis://localhost:6379/0``]
-(Example with password: ``redis://:mypassword@localhost:6379/0``)
-* ``DEFENDER_REDIS_NAME``: String: the name of your cache client on the CACHES django setting. If set, ``DEFENDER_REDIS_URL`` will be ignored.
-[Default: ``None``]
-* ``DEFENDER_STORE_ACCESS_ATTEMPTS``: Boolean: If you want to store the login
-attempt to the database, set to True. If False, it is not saved
-[Default: ``True``]
-* ``DEFENDER_USE_CELERY``: Boolean: If you want to use Celery to store the login
-attempt to the database, set to True. If False, it is saved inline.
-[Default: ``False``]
-* ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION``: Int: Length of time in hours for how
-long to keep the access attempt records in the database before the management
-command cleans them up.
-[Default: ``24``]
-* ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH``: String: The import path of the function that access username from request.
-If you want to use custom function to access and process username from request - you can specify it here.
-[Default: ``defender.utils.username_from_request``]
-
-
-Adapting to other authentication method
---------------------
-
-`defender` can be used for authentication other than `Django authentication system`.
-E.g. if `django-rest-framework` authentication has to be protected from brute force attack, a custom authentication method can be implemented.
-
-There's sample `BasicAuthenticationDefender` class based on `djangorestframework.BasicAuthentication`:
-
-```python
-import base64
-import binascii
-
-from defender import utils
-from defender import config
-from django.utils.translation import ugettext_lazy as _
-
-from rest_framework import HTTP_HEADER_ENCODING, exceptions
-
-from rest_framework.authentication import (
- BasicAuthentication,
- get_authorization_header,
- )
-
-
-class BasicAuthenticationDefender(BasicAuthentication):
-
- def get_username_from_request(self, request):
- auth = get_authorization_header(request).split()
- return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]
-
- def authenticate(self, request):
- auth = get_authorization_header(request).split()
-
- if not auth or auth[0].lower() != b'basic':
- return None
-
- if len(auth) == 1:
- msg = _('Invalid basic header. No credentials provided.')
- raise exceptions.AuthenticationFailed(msg)
- elif len(auth) > 2:
- msg = _('Invalid basic header. Credentials string should not contain spaces.')
- raise exceptions.AuthenticationFailed(msg)
-
- if utils.is_already_locked(request, get_username=self.get_username_from_request):
- detail = "You have attempted to login {failure_limit} times, with no success." \
- "Your account is locked for {cooloff_time_seconds} seconds" \
- "".format(
- failure_limit=config.FAILURE_LIMIT,
- cooloff_time_seconds=config.COOLOFF_TIME
- )
- raise exceptions.AuthenticationFailed(_(detail))
-
- try:
- auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
- except (TypeError, UnicodeDecodeError, binascii.Error):
- msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
- raise exceptions.AuthenticationFailed(msg)
-
- userid, password = auth_parts[0], auth_parts[2]
- login_unsuccessful = False
- login_exception = None
- try:
- response = self.authenticate_credentials(userid, password)
- except exceptions.AuthenticationFailed as e:
- login_unsuccessful = True
- login_exception = e
-
- utils.add_login_attempt_to_db(request,
- login_valid=not login_unsuccessful,
- get_username=self.get_username_from_request)
-
- user_not_blocked = utils.check_request(request,
- login_unsuccessful=login_unsuccessful,
- get_username=self.get_username_from_request)
- if user_not_blocked and not login_unsuccessful:
- return response
-
- raise login_exception
-
-```
-
-To make it works add `BasicAuthenticationDefender` to `DEFAULT_AUTHENTICATION_CLASSES` above all other authentication methods in your `settings.py`.
-
-
-Django Signals
---------------
-
-`django-defender` will send signals when blocking a username or an IP address. To set up signal receiver functions:
-
-```python
-from django.dispatch import receiver
-from defender import signals
-
-@receiver(signals.username_block)
-def username_blocked(username, **kwargs):
- print("%s was blocked!" % username)
-
-@receiver(signals.ip_block)
-def ip_blocked(ip_address, **kwargs):
- print("%s was blocked!" % ip_address)
-
-```
-
-
-Running Tests
-=============
-
-Tests can be run, after you clone the repository and having Django installed,
-like:
-
-```
-$ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test defender --settings=defender.test_settings
-```
-
-With Code coverage:
-
-```
-PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin.py) test defender --settings=defender.test_settings
-```
-
-
-How to release
-==============
-1. python setup.py sdist
-2. twine upload dist/*
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..dbaae07
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,482 @@
+
+django-defender
+===============
+
+.. image:: https://jazzband.co/static/img/badge.svg
+ :target: https://jazzband.co/
+ :alt: Jazzband
+
+.. image:: https://travis-ci.org/jazzband/django-defender.svg
+ :target: https://travis-ci.org/jazzband/django-defender
+ :alt: Build Status
+
+.. image:: https://img.shields.io/coveralls/jazzband/django-defender.svg
+ :target: https://coveralls.io/r/jazzband/django-defender
+ :alt: Coverage Status
+
+
+A simple Django reusable app that blocks people from brute forcing login
+attempts. The goal is to make this as fast as possible, so that we do not
+slow down the login attempts.
+
+We will use a cache so that it doesn't have to hit the database in order to
+check the database on each login attempt. The first version will be based on
+Redis, but the goal is to make this configurable so that people can use whatever
+backend best fits their needs.
+
+
+Sites using django-defender
+---------------------------
+
+If you are using defender on your site, submit a PR to add to the list.
+
+* https://hub.docker.com
+* https://www.mycosbuilder.com
+
+
+Features
+--------
+
+* Log all login attempts to the database
+* Support for reverse proxies with different headers for IP addresses
+* Rate limit based on
+
+ * Username
+ * IP address
+
+* Use Redis for the blacklist
+* Configuration
+
+ * Redis server
+
+ * Host
+ * Port
+ * Database
+ * Password
+ * Key prefix
+
+ * Block length
+
+ * Number of incorrect attempts before block
+
+* 95% code coverage
+* Full documentation
+* Ability to store login attempts to the database
+* Management command to clean up login attempts database table
+* Admin pages
+
+ * List of blocked usernames and IP addresses
+ * List of recent login attempts
+ * Ability to unblock people
+
+* Can be easily adapted to custom authentication method.
+* Signals are sent when blocking username or IP
+
+
+Admin pages
+***********
+
+.. image:: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png
+ :target: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png
+ :alt: alt tag
+
+.. image:: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png
+ :target: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png
+ :alt: alt tag
+
+
+Requirements
+------------
+
+* Python: 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, PyPy
+* Django: 1.11, 2.0, 2.1, 2.2
+* Redis
+
+
+Installation
+------------
+
+Download code, and run setup.
+
+.. code-block::
+
+ $ pip install django-defender
+
+ or
+
+ $ python setup.py install
+
+ or
+
+ $ pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev
+
+First of all, you must add this project to your list of ``INSTALLED_APPS`` in
+``settings.py``
+
+.. code-block:: python
+
+ INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ # ...
+ 'defender',
+ # ...
+ ]
+
+Next, install the ``FailedLoginMiddleware`` middleware
+
+.. code-block:: python
+
+ MIDDLEWARE_CLASSES = [
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'defender.middleware.FailedLoginMiddleware',
+ ]
+
+If you want to manage the blocked users via the Django admin, then add the
+following to your ``urls.py``
+
+.. code-block:: python
+
+ urlpatterns = [
+ url(r'^admin/', include(admin.site.urls)), # normal admin
+ url(r'^admin/defender/', include('defender.urls')), # defender admin
+ # your own patterns follow...
+ ]
+
+
+Migrations
+**********
+
+You will need to create tables in your database that are necessary
+for operation.
+
+.. code-block:: bash
+
+ python manage.py migrate defender
+
+
+Management commands
+*******************
+
+``cleanup_django_defender``
+
+If you have a website with a lot of traffic, the AccessAttempts table will get
+full pretty quickly. If you don't need to keep the data for auditing purposes
+there is a management command to help you keep it clean.
+
+It will look at your ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION`` setting to determine
+which records will be deleted. Default if not specified, is 24 hours.
+
+.. code-block:: bash
+
+ $ python manage.py cleanup_django_defender
+
+You can set this up as a daily or weekly cron job to keep the table size down.
+
+.. code-block:: bash
+
+ # run at 12:24 AM every morning.
+ 24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log
+
+
+Long term goals
+---------------
+
+* Pluggable backends, so people can use something other than Redis
+* Email users when their account is blocked
+* Add a whitelist for username and ip's that we will never block (admin's, etc)
+* Add a permanent black list for IP addresses
+* Scan for known proxy IPs and do not block requests coming from those
+ (improve the chances that a good IP is blocked)
+* Add management command to prune old (configurable) login attempts.
+
+
+Performance
+***********
+
+The goal of defender is to make it as fast as possible so that it doesn't slow
+down the login process. In order to make sure our goals are met we need a way
+to test the application to make sure we are on the right track. The best
+way to do this is to compare how fast a normal Django login takes with defender
+and django-axes.
+
+The normal django login, would be our baseline, and we expect it to be the
+fastest of the 3 methods, because there are no additional checks happening.
+
+The defender login would most likely be slower then the django login, and
+hopefully faster then the django-axes login. The goal is to make it as little
+of a difference between the regular raw login, and defender.
+
+The django-axes login speed, will probably be the slowest of the three since it
+does more checks and does a lot of database queries.
+
+The best way to determine the speed of a login is to do a load test against an
+application with each setup, and compare the login times for each type.
+
+
+Load testing
+************
+
+In order to make sure we cover all the different types of logins, in our load
+test we need to have more then one test.
+
+#. All success: We will do a load test with nothing but successful logins.
+
+#. Mixed: some success some failure: We will load test with some successful logins and some failures to see how the failure effect the performance.
+
+#. All Failures: We will load test with all failure logins and see the difference in performance.
+
+We will need a sample application that we can use for the load test, with the
+only difference is the configuration where we either load defender, axes, or
+none of them.
+
+We can use a hosted load testing service, or something like jmeter. Either way
+we need to be consistent for all of the tests. If we use jmeter, we should have
+our jmeter configuration for others to run the tests on their own.
+
+
+Results of load tests
+*********************
+
+We will post the results here. We will explain each test, and show the results
+along with some charts.
+
+
+Why not django-axes
+-------------------
+
+django-axes is great but it puts everything in the database, and this causes
+a bottle neck when you have a lot of data. It slows down the auth requests by
+as much as 200-300ms. This might not be much for some sites, but for others it
+is too long.
+
+This started out as a fork of django-axes, and is using as much of their code
+as possible, and removing the parts not needed, and speeding up the lookups
+to improve the login.
+
+
+How django-defender works
+-------------------------
+
+#. When someone tries to login, we first check to see if they are currently
+ blocked. We check the username they are trying to use, as well as the IP
+ address. If they are blocked, goto step 5. If not blocked go to step 2.
+
+#. They are not blocked, so we check to see if the login was valid. If valid
+ go to step 6. If not valid go to step 3.
+
+#. Login attempt wasn't valid. Add their username and IP address for this
+ attempt to the cache. If this brings them over the limit, add them to the
+ blocked list, and then goto step 5. If not over the limit goto step 4.
+
+#. Login was invalid, but not over the limit. Send them back to the login screen
+ to try again.
+
+#. User is blocked: Send them to the blocked page, telling them they are
+ blocked, and give an estimate on when they will be unblocked.
+
+#. Login is valid. Reset any failed login attempts, and forward to their
+ destination.
+
+
+Cache backend
+-------------
+
+Defender uses the cache to save the failed attempts.
+
+
+Cache keys
+**********
+
+Counters:
+
+* prefix:failed:ip:[ip] (count, TTL)
+* prefix:failed:username:[username] (count, TTL)
+
+Booleans (if present it is blocked):
+
+* prefix:blocked:ip:[ip] (true, TTL)
+* prefix:blocked:username:[username] (true, TTL)
+
+
+Customizing django-defender
+---------------------------
+
+You have a couple options available to you to customize ``django-defender`` a bit.
+These should be defined in your ``settings.py`` file.
+
+* ``DEFENDER_LOGIN_FAILURE_LIMIT``\ : Int: The number of login attempts allowed before a
+ record is created for the failed logins. [Default: ``3``\ ]
+* ``DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME``\ : Int: The number of login attempts allowed
+ on a username before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\ ]
+* ``DEFENDER_LOGIN_FAILURE_LIMIT_IP``\ : Int: The number of login attempts allowed
+ from an IP before a record is created for the failed logins. [Default: ``DEFENDER_LOGIN_FAILURE_LIMIT``\ ]
+* ``DEFENDER_BEHIND_REVERSE_PROXY``\ : Boolean: Is defender behind a reverse proxy?
+ [Default: ``False``\ ]
+* ``DEFENDER_REVERSE_PROXY_HEADER``\ : String: the name of the http header with your
+ reverse proxy IP address [Default: ``HTTP_X_FORWARDED_FOR``\ ]
+* ``DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME``\ : Boolean: Locks a user out based on a combination of IP and Username. This stops a user denying access to the application for all other users accessing the app from behind the same IP address. [Default: ``False``\ ]
+* ``DEFENDER_DISABLE_IP_LOCKOUT``\ : Boolean: If this is True, it will not lockout the users IP address, it will only lockout the username. [Default: False]
+* ``DEFENDER_DISABLE_USERNAME_LOCKOUT``\ : Boolean: If this is True, it will not lockout usernames, it will only lockout IP addresess. [Default: False]
+* ``DEFENDER_COOLOFF_TIME``\ : Int: If set, defines a period of inactivity after which
+ old failed login attempts will be forgotten. An integer, will be interpreted as a
+ number of seconds. If ``0``\ , the locks will not expire. [Default: ``300``\ ]
+* ``DEFENDER_LOCKOUT_TEMPLATE``\ : String: [Default: ``None``\ ] If set, specifies a template to render when a user is locked out. Template receives the following context variables:
+ * ``cooloff_time_seconds``\ : The cool off time in seconds
+ * ``cooloff_time_minutes``\ : The cool off time in minutes
+ * ``failure_limit``\ : The number of failures before you get blocked.
+* ``DEFENDER_USERNAME_FORM_FIELD``\ : String: the name of the form field that contains your
+ users usernames. [Default: ``username``\ ]
+* ``DEFENDER_CACHE_PREFIX``\ : String: The cache prefix for your defender keys.
+ [Default: ``defender``\ ]
+* ``DEFENDER_LOCKOUT_URL``\ : String: The URL you want to redirect to if someone is
+ locked out.
+* ``DEFENDER_REDIS_URL``\ : String: the redis url for defender.
+ [Default: ``redis://localhost:6379/0``\ ]
+ (Example with password: ``redis://:mypassword@localhost:6379/0``\ )
+* ``DEFENDER_REDIS_NAME``\ : String: the name of your cache client on the CACHES django setting. If set, ``DEFENDER_REDIS_URL`` will be ignored.
+ [Default: ``None``\ ]
+* ``DEFENDER_STORE_ACCESS_ATTEMPTS``\ : Boolean: If you want to store the login
+ attempt to the database, set to True. If False, it is not saved
+ [Default: ``True``\ ]
+* ``DEFENDER_USE_CELERY``\ : Boolean: If you want to use Celery to store the login
+ attempt to the database, set to True. If False, it is saved inline.
+ [Default: ``False``\ ]
+* ``DEFENDER_ACCESS_ATTEMPT_EXPIRATION``\ : Int: Length of time in hours for how
+ long to keep the access attempt records in the database before the management
+ command cleans them up.
+ [Default: ``24``\ ]
+* ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH``\ : String: The import path of the function that access username from request.
+ If you want to use custom function to access and process username from request - you can specify it here.
+ [Default: ``defender.utils.username_from_request``\ ]
+
+
+Adapting to other authentication methods
+----------------------------------------
+
+``defender`` can be used for authentication other than ``Django authentication system``.
+E.g. if ``django-rest-framework`` authentication has to be protected from brute force attack, a custom authentication method can be implemented.
+
+There's sample ``BasicAuthenticationDefender`` class based on ``djangorestframework.BasicAuthentication``\ :
+
+.. code-block:: python
+
+ import base64
+ import binascii
+
+ from django.utils.translation import ugettext_lazy as _
+
+ from rest_framework import HTTP_HEADER_ENCODING, exceptions
+ from rest_framework.authentication import (
+ BasicAuthentication,
+ get_authorization_header,
+ )
+
+ from defender import utils
+ from defender import config
+
+ class BasicAuthenticationDefender(BasicAuthentication):
+
+ def get_username_from_request(self, request):
+ auth = get_authorization_header(request).split()
+ return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]
+
+ def authenticate(self, request):
+ auth = get_authorization_header(request).split()
+
+ if not auth or auth[0].lower() != b'basic':
+ return None
+
+ if len(auth) == 1:
+ msg = _('Invalid basic header. No credentials provided.')
+ raise exceptions.AuthenticationFailed(msg)
+ elif len(auth) > 2:
+ msg = _('Invalid basic header. Credentials string should not contain spaces.')
+ raise exceptions.AuthenticationFailed(msg)
+
+ if utils.is_already_locked(request, get_username=self.get_username_from_request):
+ detail = "You have attempted to login {failure_limit} times, with no success." \
+ "Your account is locked for {cooloff_time_seconds} seconds" \
+ "".format(
+ failure_limit=config.FAILURE_LIMIT,
+ cooloff_time_seconds=config.COOLOFF_TIME
+ )
+ raise exceptions.AuthenticationFailed(_(detail))
+
+ try:
+ auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
+ except (TypeError, UnicodeDecodeError, binascii.Error):
+ msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
+ raise exceptions.AuthenticationFailed(msg)
+
+ userid, password = auth_parts[0], auth_parts[2]
+ login_unsuccessful = False
+ login_exception = None
+ try:
+ response = self.authenticate_credentials(userid, password)
+ except exceptions.AuthenticationFailed as e:
+ login_unsuccessful = True
+ login_exception = e
+
+ utils.add_login_attempt_to_db(request,
+ login_valid=not login_unsuccessful,
+ get_username=self.get_username_from_request)
+
+ user_not_blocked = utils.check_request(request,
+ login_unsuccessful=login_unsuccessful,
+ get_username=self.get_username_from_request)
+ if user_not_blocked and not login_unsuccessful:
+ return response
+
+ raise login_exception
+
+To make it work add ``BasicAuthenticationDefender`` to ``DEFAULT_AUTHENTICATION_CLASSES`` above all other authentication methods in your ``settings.py``.
+
+
+Django signals
+--------------
+
+``django-defender`` will send signals when blocking a username or an IP address. To set up signal receiver functions:
+
+.. code-block:: python
+
+ from django.dispatch import receiver
+
+ from defender import signals
+
+ @receiver(signals.username_block)
+ def username_blocked(username, **kwargs):
+ print("%s was blocked!" % username)
+
+ @receiver(signals.ip_block)
+ def ip_blocked(ip_address, **kwargs):
+ print("%s was blocked!" % ip_address)
+
+
+Running tests
+-------------
+
+Tests can be run, after you clone the repository and having Django installed,
+like:
+
+.. code-block::
+
+ $ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test defender --settings=defender.test_settings
+
+With Code coverage:
+
+.. code-block::
+
+ PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin.py) test defender --settings=defender.test_settings
+
+
+How to release
+--------------
+
+#. ``python setup.py sdist``
+#. ``twine upload dist/*``
diff --git a/docs/1_readme.rst b/docs/1_readme.rst
new file mode 100644
index 0000000..afebc71
--- /dev/null
+++ b/docs/1_readme.rst
@@ -0,0 +1,3 @@
+.. readme:
+
+.. include:: ../README.rst
diff --git a/docs/2_contributing.rst b/docs/2_contributing.rst
new file mode 100644
index 0000000..6af7dea
--- /dev/null
+++ b/docs/2_contributing.rst
@@ -0,0 +1,3 @@
+.. contributing:
+
+.. include:: ../CONTRIBUTING.rst
diff --git a/docs/3_changelog.rst b/docs/3_changelog.rst
new file mode 100644
index 0000000..2bb2503
--- /dev/null
+++ b/docs/3_changelog.rst
@@ -0,0 +1,3 @@
+.. changelog:
+
+.. include:: ../CHANGES.rst
diff --git a/docs/index.rst b/docs/index.rst
index 1d8a91a..1c405d7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -6,9 +6,16 @@
django-defender documentation
=============================
+Contents
+--------
+
.. toctree::
:maxdepth: 2
- :caption: Contents:
+ :numbered: 1
+
+ 1_readme
+ 2_contributing
+ 3_changelog