#!/usr/bin/env python # -*- coding:utf-8 -*- # Copyright (c) 2007, Dima Dogadaylo (www.mysoftparade.com) import re import sha import pickle import base64 import time from random import randint from django import newforms as forms from django.conf import settings class MathCaptchaForm(forms.Form): """Lightweight mathematical captcha where human is asked to solve a simple mathematical calculation like 3+5=?. It don't use database and don't require external libraries. From concatenation of time, question, answer, settings.SITE_URL and settings.SECRET_KEY is built hash that is validated on each form submission. It makes impossible to "record" valid captcha form submission and "replay" it later - form will not be validated because captcha will be expired. For more info see: http://www.mysoftparade.com/blog/improved-mathematical-captcha/ """ A_RE = re.compile("^(\d+)$") captcha_answer = forms.CharField(max_length = 2, required=True, widget = forms.TextInput(attrs={'size':'2'})) captcha_token = forms.CharField(max_length=200, required=True, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): """Initalise captcha_question and captcha_token for the form.""" super(MathCaptchaForm, self).__init__(*args, **kwargs) # reset captcha for unbound forms if not self.data: self.reset_captcha() def reset_captcha(self): """Generate new question and valid token for it, reset previous answer if any.""" q, a = self._generate_captcha() expires = time.time() +\ getattr(settings, 'CAPTCHA_EXPIRES_SECONDS', 60*60) token = self._make_token(q, a, expires) self.initial['captcha_token'] = token self._plain_question = q # reset captcha fields for bound form if self.data: def _reset(): self.data['captcha_token'] = token self.data['captcha_answer'] = '' if hasattr(self.data, '_mutable') and not self.data._mutable: self.data._mutable = True _reset() self.data._mutable = False else: _reset() def _generate_captcha(self): """Generate question and return it along with correct answer.""" a, b = randint(1,9), randint(1,9) return ("%s+%s" % (a,b), a+b) def _make_token(self, q, a, expires): data = base64.urlsafe_b64encode(\ pickle.dumps({'q': q, 'expires': expires})) return self._sign(q, a, expires) + data def _sign(self, q, a, expires): plain = [getattr(settings, 'SITE_URL', ''), settings.SECRET_KEY,\ q, a, expires] plain = "".join([str(p) for p in plain]) return sha.new(plain).hexdigest() @property def plain_question(self): return self._plain_question @property def knotty_question(self): """Wrap plain_question in some invisibe for humans markup with random nonexisted classes, that makes life of spambots a bit harder because form of question is vary from request to request.""" digits = self._plain_question.split('+') return "+".join(['%s' %\ (randint(1,9), d) for d in digits]) def clean_captcha_token(self): t = self._parse_token(self.cleaned_data['captcha_token']) if time.time() > t['expires']: raise forms.ValidationError("Captcha is expired.") self._plain_question = t['q'] return t def _parse_token(self, t): try: sign, data = t[:40], t[40:] data = pickle.loads(base64.urlsafe_b64decode(str(data))) return {'q': data['q'], 'expires': float(data['expires']), 'sign': sign} except Exception, e: import sys sys.stderr.write("Captcha error: %r\n" % e) raise forms.ValidationError("Invalid captcha!") def clean_captcha_answer(self): a = self.A_RE.match(self.cleaned_data.get('captcha_answer')) if not a: raise forms.ValidationError("Number is expected!") return int(a.group(0)) def clean(self): """Check captcha answer.""" cd = self.cleaned_data # don't check captcha if no answer if 'captcha_answer' not in cd: return cd t = cd.get('captcha_token') if t: form_sign = self._sign(t['q'], cd['captcha_answer'], t['expires']) if form_sign != t['sign']: self._errors['captcha_answer'] = ["Are you human?"] else: self.reset_captcha() return super(MathCaptchaForm, self).clean()