Skip to content

Session store #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added captcha/backends/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions captcha/backends/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from captcha.conf import settings as captcha_settings


class DoesNotExist(Exception):
""" Can't find captcha in store """
pass


class BaseStore(object):
DoesNotExist = DoesNotExist

def generate_key(self):
"""
Generate captcha with unique key
"""
return captcha_settings.get_challenge()()

def remove_expired(self):
"""
Remove expired captcha records
"""
pass

def get(self, response=None, hashkey=None, allow_expired=True):
"""
Get captcha from store, or rise exception if captcha wasn't found
"""
pass
38 changes: 38 additions & 0 deletions captcha/backends/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from ..models import CaptchaStore, get_safe_now
from .base import BaseStore


class DBStore(BaseStore):
def __init__(self, key=None):
self._captcha = {}
if key:
try:
cap = CaptchaStore.objects.get(hashkey=key)
self._captcha = {
'hashkey': cap.hashkey,
'challenge': cap.challenge,
'response': cap.response,
'expiration': cap.expiration,
}
except CaptchaStore.DoesNotExist:
raise self.DoesNotExist

def __getitem__(self, key):
return self._captcha[key]

def remove_expired(self):
CaptchaStore.objects.filter(expiration__lte=get_safe_now()).delete()

def generate_key(self):
return CaptchaStore.generate_key()

def get(self, response=None, hashkey=None, allow_expired=True):
store = DBStore(hashkey)
if response and store['response'] != response:
raise self.DoesNotExist
if not allow_expired and store['expiration'] < get_safe_now():
raise self.DoesNotExist
return store

def delete(self):
CaptchaStore.objects.filter(hashkey=self['hashkey']).delete()
38 changes: 38 additions & 0 deletions captcha/backends/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from .base import BaseStore
from captcha.conf import settings as captcha_settings
from django.conf import settings
import django
try:
from importlib import import_module
Store = import_module(settings.SESSION_ENGINE).SessionStore
except:
# py 2.6
backend = __import__(settings.SESSION_ENGINE, globals(), locals(), ['SessionStore'], -1)
Store = backend.SessionStore


class SessionStore(BaseStore):

def generate_key(self):
challenge, response = super(SessionStore, self).generate_key()
store = Store()
store.set_expiry(60 * int(captcha_settings.CAPTCHA_TIMEOUT))
store['challenge'] = challenge
store['response'] = response
store.save()
return store.session_key

def remove_expired(self):
if not django.get_version() < '1.5':
Store.clear_expired()

def get(self, response=None, hashkey=None, allow_expired=True):
s = Store(session_key=hashkey)
if not s.get('response'):
raise self.DoesNotExist
if response:
if s['response'] != response:
raise self.DoesNotExist
if not allow_expired and s.get_expiry_age() < 0:
raise self.DoesNotExist
return s
3 changes: 2 additions & 1 deletion captcha/conf/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
import os
from django.conf import settings

CAPTCHA_FONT_PATH = getattr(settings, 'CAPTCHA_FONT_PATH', os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'fonts/Vera.ttf')))
Expand All @@ -18,6 +18,7 @@
CAPTCHA_DICTIONARY_MIN_LENGTH = getattr(settings, 'CAPTCHA_DICTIONARY_MIN_LENGTH', 0)
CAPTCHA_DICTIONARY_MAX_LENGTH = getattr(settings, 'CAPTCHA_DICTIONARY_MAX_LENGTH', 99)
CAPTCHA_IMAGE_SIZE = getattr(settings, 'CAPTCHA_IMAGE_SIZE', None)
CAPTCHA_STORE = getattr(settings, 'CAPTCHA_STORE', 'DB')

if CAPTCHA_IMAGE_BEFORE_FIELD:
CAPTCHA_OUTPUT_FORMAT = getattr(settings, 'CAPTCHA_OUTPUT_FORMAT', '%(image)s %(hidden_field)s %(text_field)s')
Expand Down
33 changes: 27 additions & 6 deletions captcha/fields.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from captcha.conf import settings
from captcha.models import CaptchaStore, get_safe_now
from captcha.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import TextInput, MultiWidget, HiddenInput
from django.utils.translation import ugettext, ugettext_lazy
from six import u
from .backends.base import BaseStore


class BaseCaptchaTextInput(MultiWidget):
Expand All @@ -30,6 +30,17 @@ def fetch_captcha_store(self, name, value, attrs=None):
Fetches a new CaptchaStore
This has to be called inside render
"""

CaptchaStore = None
if settings.CAPTCHA_STORE == 'SESSION':
from .backends.session import SessionStore
CaptchaStore = SessionStore()
elif settings.CAPTCHA_STORE == 'DB':
from .backends.db import DBStore
CaptchaStore = DBStore()
else:
raise ImproperlyConfigured

try:
reverse('captcha-image', args=('dummy',))
except NoReverseMatch:
Expand Down Expand Up @@ -125,22 +136,32 @@ def compress(self, data_list):
return None

def clean(self, value):
CaptchaStore = None
if settings.CAPTCHA_STORE == 'SESSION':
from .backends.session import SessionStore
CaptchaStore = SessionStore()
elif settings.CAPTCHA_STORE == 'DB':
from .backends.db import DBStore
CaptchaStore = DBStore()
else:
raise ImproperlyConfigured

super(CaptchaField, self).clean(value)
response, value[1] = (value[1] or '').strip().lower(), ''
CaptchaStore.remove_expired()
if settings.CAPTCHA_TEST_MODE and response.lower() == 'passed':
# automatically pass the test
try:
# try to delete the captcha based on its hash
CaptchaStore.objects.get(hashkey=value[0]).delete()
except CaptchaStore.DoesNotExist:
CaptchaStore.get(hashkey=value[0]).delete()
except BaseStore.DoesNotExist:
# ignore errors
pass
elif not self.required and not response:
pass
else:
try:
CaptchaStore.objects.get(response=response, hashkey=value[0], expiration__gt=get_safe_now()).delete()
except CaptchaStore.DoesNotExist:
CaptchaStore.get(response=response, hashkey=value[0], allow_expired=False).delete()
except BaseStore.DoesNotExist:
raise ValidationError(getattr(self, 'error_messages', {}).get('invalid', ugettext_lazy('Invalid CAPTCHA')))
return value
39 changes: 24 additions & 15 deletions captcha/management/commands/captcha_clean.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
from django.core.management.base import BaseCommand
from captcha.models import get_safe_now
from captcha.conf import settings
import sys


class Command(BaseCommand):
help = "Clean up expired captcha hashkeys."

def handle(self, **options):
from captcha.models import CaptchaStore
verbose = int(options.get('verbosity'))
expired_keys = CaptchaStore.objects.filter(expiration__lte=get_safe_now()).count()
if verbose >= 1:
print("Currently %d expired hashkeys" % expired_keys)
try:
CaptchaStore.remove_expired()
except:
if settings.CAPTCHA_STORE == 'SESSION':
from ...backends.session import SessionStore
try:
SessionStore().remove_expired()
print("Expired sessions removed.")
except:
print("Unable to delete expired sessions.")
elif settings.CAPTCHA_STORE == 'DB':
from captcha.models import CaptchaStore
verbose = int(options.get('verbosity'))
expired_keys = CaptchaStore.objects.filter(expiration__lte=get_safe_now()).count()
if verbose >= 1:
print("Unable to delete expired hashkeys.")
sys.exit(1)
if verbose >= 1:
if expired_keys > 0:
print("%d expired hashkeys removed." % expired_keys)
else:
print("No keys to remove.")
print("Currently %d expired hashkeys" % expired_keys)
try:
CaptchaStore.remove_expired()
except:
if verbose >= 1:
print("Unable to delete expired hashkeys.")
sys.exit(1)
if verbose >= 1:
if expired_keys > 0:
print("%d expired hashkeys removed." % expired_keys)
else:
print("No keys to remove.")
70 changes: 35 additions & 35 deletions captcha/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,38 @@ def get_safe_now():
pass
return datetime.datetime.now()


class CaptchaStore(models.Model):
challenge = models.CharField(blank=False, max_length=32)
response = models.CharField(blank=False, max_length=32)
hashkey = models.CharField(blank=False, max_length=40, unique=True)
expiration = models.DateTimeField(blank=False)

def save(self, *args, **kwargs):
self.response = self.response.lower()
if not self.expiration:
self.expiration = get_safe_now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
if not self.hashkey:
key_ = (
smart_text(randrange(0, MAX_RANDOM_KEY)) +
smart_text(time.time()) +
smart_text(self.challenge, errors='ignore') +
smart_text(self.response, errors='ignore')
).encode('utf8')
self.hashkey = hashlib.sha1(key_).hexdigest()
del(key_)
super(CaptchaStore, self).save(*args, **kwargs)

def __unicode__(self):
return self.challenge

def remove_expired(cls):
cls.objects.filter(expiration__lte=get_safe_now()).delete()
remove_expired = classmethod(remove_expired)

@classmethod
def generate_key(cls):
challenge, response = captcha_settings.get_challenge()()
store = cls.objects.create(challenge=challenge, response=response)

return store.hashkey
if captcha_settings.CAPTCHA_STORE == "DB":
class CaptchaStore(models.Model):
challenge = models.CharField(blank=False, max_length=32)
response = models.CharField(blank=False, max_length=32)
hashkey = models.CharField(blank=False, max_length=40, unique=True)
expiration = models.DateTimeField(blank=False)

def save(self, *args, **kwargs):
self.response = self.response.lower()
if not self.expiration:
self.expiration = get_safe_now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
if not self.hashkey:
key_ = (
smart_text(randrange(0, MAX_RANDOM_KEY)) +
smart_text(time.time()) +
smart_text(self.challenge, errors='ignore') +
smart_text(self.response, errors='ignore')
).encode('utf8')
self.hashkey = hashlib.sha1(key_).hexdigest()
del(key_)
super(CaptchaStore, self).save(*args, **kwargs)

def __unicode__(self):
return self.challenge

def remove_expired(cls):
cls.objects.filter(expiration__lte=get_safe_now()).delete()
remove_expired = classmethod(remove_expired)

@classmethod
def generate_key(cls):
challenge, response = captcha_settings.get_challenge()()
store = cls.objects.create(challenge=challenge, response=response)

return store.hashkey
Loading