diff --git a/.coveragerc b/.coveragerc
index cd0537e..52aa906 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,2 @@
[run]
-omit = *_settings.py, defender/*migrations/*
+omit = *_settings.py, defender/*migrations/*, defender/exampleapp/*
diff --git a/.gitignore b/.gitignore
index db4561e..2e3ea0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,7 @@ docs/_build/
# PyBuilder
target/
+
+# exampleapp
+defender/exampleapp/static/
+defender/exampleapp/media/
diff --git a/.landscape.yaml b/.landscape.yaml
index ee0d355..83151ce 100644
--- a/.landscape.yaml
+++ b/.landscape.yaml
@@ -8,3 +8,4 @@ uses:
autodetect: yes
ignore-patterns:
- .*_settings.py$
+ - defender/exampleapp/*
diff --git a/README.md b/README.md
index b287f50..601f4bb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
django-defender
===============
-A simple django reusable app that blocks people from brute forcing login
+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.
@@ -18,6 +18,10 @@ Build status
[](https://travis-ci.org/kencochrane/django-defender) [](https://coveralls.io/r/kencochrane/django-defender)[](https://landscape.io/github/kencochrane/django-defender/master)
+Sites using Defender:
+=====================
+- https://hub.docker.com
+
Goals for 0.1
=============
@@ -129,63 +133,40 @@ 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 10. If not blocked go to step 2.
+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 20. If not valid go to step 3.
+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 10. If not over the limit goto step 4.
+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.
-10. User is blocked: Send them to the blocked page, telling them they are
+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.
-20. Login is valid. Reset any failed login attempts, and forward to their
+6. Login is valid. Reset any failed login attempts, and forward to their
destination.
Cache backend:
==============
-- ip_attempts (count, TTL)
-- username_attempts (count, TTL)
-- ip_blocks (list)
-- username_blocks (list)
-
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)
-Rate limiting Example
----------------------
-```
-# example of how to do rate limiting by IP
-# assuming it is 10 requests being the limit
-# this assumes there is a DECAY of DECAY_TIME
-# to remove invalid logins after a set number of time
-# For every incorrect login, we reset the block time.
-
-FUNCTION LIMIT_API_CALL(ip)
-current = LLEN(ip)
-IF current > 10 THEN
- ERROR "too many requests per second"
-ELSE
- MULTI
- RPUSH(ip, ip)
- EXPIRE(ip, DECAY_TIME)
- EXEC
-END
-```
-
Installing Django-defender
==========================
@@ -229,6 +210,25 @@ Next, install the ``FailedLoginMiddleware`` middleware::
)
```
+If you want to manage the blocked users via the Django admin, then add the
+following to your ``urls.py``
+
+```
+urlpatterns = patterns(
+ '',
+ (r'^admin/', include(admin.site.urls)), # normal admin
+ (r'^admin/defender/', include('defender.urls')), # defender admin
+ # your own patterns follow…
+)
+```
+
+
+Admin Pages:
+------------
+
+
+
+
Database tables:
----------------
@@ -257,30 +257,31 @@ You have a couple options available to you to customize ``django-defender`` a bi
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``
+record is created for the failed logins. [Default: ``3``]
* ``DEFENDER_USE_USER_AGENT``: Boolean: If ``True``, lock out / log based on an IP address
AND a user agent. This means requests from different user agents but from
-the same IP are treated differently. Default: ``False``
+the same IP are treated differently. [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: If set, specifies a template to render when a
-user is locked out. Template receives cooloff_time and failure_limit as
-context variables. Default: ``None``
+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``
+users usernames. [Default: ``username``]
* ``DEFENDER_REVERSE_PROXY_HEADER``: String: the name of the http header with your
-reverse proxy IP address Default: ``HTTP_X_FORWARDED_FOR``
+reverse proxy IP address [Default: ``HTTP_X_FORWARDED_FOR``]
* ``DEFENDER_CACHE_PREFIX``: String: The cache prefix for your defender keys.
-Default: ``defender``
+[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``
+[Default: ``redis://localhost:6379/0``]
(Example with password: ``redis://:mypassword@localhost:6379/0``)
* ``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``
+[Default: ``False``]
Running Tests
=============
diff --git a/defender/admin.py b/defender/admin.py
index ccfaa8e..491c295 100644
--- a/defender/admin.py
+++ b/defender/admin.py
@@ -1,5 +1,4 @@
from django.contrib import admin
-
from .models import AccessAttempt
@@ -33,12 +32,10 @@ class AccessAttemptAdmin(admin.ModelAdmin):
(None, {
'fields': ('path_info', 'login_valid')
}),
- ('Form Data', {
- 'fields': ('get_data', 'post_data')
- }),
('Meta Data', {
- 'fields': ('user_agent', 'ip_address', 'http_accept')
+ 'fields': ('user_agent', 'ip_address')
})
)
+
admin.site.register(AccessAttempt, AccessAttemptAdmin)
diff --git a/defender/exampleapp/__init__.py b/defender/exampleapp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/defender/exampleapp/defender.sb b/defender/exampleapp/defender.sb
new file mode 100644
index 0000000..604e87b
Binary files /dev/null and b/defender/exampleapp/defender.sb differ
diff --git a/defender/exampleapp/readme.md b/defender/exampleapp/readme.md
new file mode 100644
index 0000000..ba61ac1
--- /dev/null
+++ b/defender/exampleapp/readme.md
@@ -0,0 +1,14 @@
+Example App
+===========
+
+admin password is ``admin:password``
+
+
+This is just a simple example app, used for testing and showing how things work
+```
+mkdir -p exampleapp/static exampleapp/media/static
+
+PYTHONPATH=$PYTHONPATH:$PWD django-admin.py collectstatic --noinput --settings=defender.exampleapp.settings
+
+PYTHONPATH=$PYTHONPATH:$PWD django-admin.py runserver --settings=defender.exampleapp.settings
+```
diff --git a/defender/exampleapp/settings.py b/defender/exampleapp/settings.py
new file mode 100644
index 0000000..d331af7
--- /dev/null
+++ b/defender/exampleapp/settings.py
@@ -0,0 +1,81 @@
+import os
+PROJECT_DIR = lambda base: os.path.abspath(
+ os.path.join(os.path.dirname(__file__), base).replace('\\', '/'))
+
+
+MEDIA_ROOT = PROJECT_DIR(os.path.join('media'))
+MEDIA_URL = '/media/'
+STATIC_ROOT = PROJECT_DIR(os.path.join('static'))
+STATIC_URL = '/static/'
+
+STATICFILES_DIRS = (
+ PROJECT_DIR(os.path.join('media', 'static')),
+)
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': PROJECT_DIR('defender.sb'),
+ }
+}
+
+
+SITE_ID = 1
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'defender.middleware.FailedLoginMiddleware',
+)
+
+ROOT_URLCONF = 'defender.exampleapp.urls'
+
+INSTALLED_APPS = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.admin',
+ 'django.contrib.staticfiles',
+ 'defender',
+]
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+)
+
+SECRET_KEY = os.environ.get('SECRET_KEY', 'too-secret-for-test')
+
+LOGIN_REDIRECT_URL = '/admin'
+
+DEFENDER_LOGIN_FAILURE_LIMIT = 1
+DEFENDER_COOLOFF_TIME = 60
+DEFENDER_REDIS_URL = "redis://localhost:6379/1"
+# don't use mock redis in unit tests, we will use real redis on travis.
+DEFENDER_MOCK_REDIS = False
+
+# Celery settings:
+CELERY_ALWAYS_EAGER = True
+BROKER_BACKEND = 'memory'
+BROKER_URL = 'memory://'
+
+import os
+
+from celery import Celery
+
+# set the default Django settings module for the 'celery' program.
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.exampleapp.settings')
+
+app = Celery('defender')
+
+# Using a string here means the worker will not have to
+# pickle the object when using Windows.
+app.config_from_object('django.conf:settings')
+app.autodiscover_tasks(lambda: INSTALLED_APPS)
+
+DEBUG = True
diff --git a/defender/exampleapp/urls.py b/defender/exampleapp/urls.py
new file mode 100644
index 0000000..c0d8596
--- /dev/null
+++ b/defender/exampleapp/urls.py
@@ -0,0 +1,17 @@
+from django.conf.urls import patterns, include
+from django.conf import settings
+from django.contrib import admin
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+from django.conf.urls.static import static
+
+admin.autodiscover()
+
+urlpatterns = patterns(
+ '',
+ (r'^admin/', include(admin.site.urls)),
+ (r'^admin/defender/', include('defender.urls')),
+)
+
+
+urlpatterns += staticfiles_urlpatterns()
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/defender/templates/admin/defender/app_index.html b/defender/templates/admin/defender/app_index.html
new file mode 100644
index 0000000..ccf8914
--- /dev/null
+++ b/defender/templates/admin/defender/app_index.html
@@ -0,0 +1,25 @@
+{% extends "admin/index.html" %}
+{% load i18n %}
+
+{% if not is_popup %}
+{% block breadcrumbs %}
+
+{% endblock %}
+{% endif %}
+
+{% block sidebar %}{% endblock %}
+
+{% block content %}
+{{ block.super }}
+
+{% endblock content%}
diff --git a/defender/templates/defender/admin/blocks.html b/defender/templates/defender/admin/blocks.html
new file mode 100644
index 0000000..5bda17e
--- /dev/null
+++ b/defender/templates/defender/admin/blocks.html
@@ -0,0 +1,74 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_urls %}
+
+{% block bodyclass %}dashboard{% endblock %}
+
+{% block extrastyle %}
+{{ block.super }}
+
+{% endblock %}
+
+{% block breadcrumbs %}
+
+{% endblock breadcrumbs %}
+
+{% block content %}
+
+
+
Blocked Logins
+
Here is a list of IP's and usernames that are blocked
+
+
+
+
+ Blocked IP's
+
+ | IP | Action |
+
+
+ {% for block in blocked_ip_list %}
+
+ | {{block}} |
+
+
+ |
+
+ {% empty %}
+ | No IP's |
+ {% endfor %}
+
+
+
+
+
+
+ Blocked Usernames
+
+ | Usernames | Action |
+
+
+ {% for block in blocked_username_list %}
+
+ | {{block}} |
+
+
+ |
+
+ {% empty %}
+ | No Username's |
+ {% endfor %}
+
+
+
+
+
+{% endblock content %}
diff --git a/defender/templates/defender/lockout.html b/defender/templates/defender/lockout.html
index d84ec21..a365513 100644
--- a/defender/templates/defender/lockout.html
+++ b/defender/templates/defender/lockout.html
@@ -2,6 +2,6 @@
Locked out
Your have attempted to login {{failure_limit}} times, with no success.
-Your account is locked for {{cooloff_time}} seconds
+Your account is locked for {{cooloff_time_seconds}} seconds