[snowy] Add reCaptcha support to the registration form
- From: Brad Taylor <btaylor src gnome org>
- To: svn-commits-list gnome org
- Subject: [snowy] Add reCaptcha support to the registration form
- Date: Wed, 22 Jul 2009 19:54:08 +0000 (UTC)
commit c19d951cde52bd23a90c134a0ca8601f7d6f9343
Author: Brad Taylor <brad getcoded net>
Date: Wed Jul 22 15:45:40 2009 -0400
Add reCaptcha support to the registration form
TODO | 2 -
lib/recaptcha/__init__.py | 1 +
lib/recaptcha/client/captcha.py | 96 ++++++++++++++++++++++++++++++++++++
lib/recaptcha/client/mailhide.py | 68 +++++++++++++++++++++++++
lib/recaptcha_django/__init__.py | 60 ++++++++++++++++++++++
lib/recaptcha_django/middleware.py | 12 +++++
local_settings.py.in | 9 +++-
settings.py | 5 ++
users/forms.py | 32 ++++++++++++-
9 files changed, 280 insertions(+), 5 deletions(-)
---
diff --git a/TODO b/TODO
index 293609b..0a7fdfa 100644
--- a/TODO
+++ b/TODO
@@ -17,8 +17,6 @@ TODO
- Interface for detecting/selecting preferred language
* Accounts
- - Verify password sanity/strength
- - Add recaptcha to prevent spammy accounts
- Preferences page
+ Language selection
+ API access
diff --git a/lib/recaptcha/__init__.py b/lib/recaptcha/__init__.py
new file mode 100644
index 0000000..de40ea7
--- /dev/null
+++ b/lib/recaptcha/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/lib/recaptcha/client/__init__.py b/lib/recaptcha/client/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib/recaptcha/client/captcha.py b/lib/recaptcha/client/captcha.py
new file mode 100644
index 0000000..4b85f68
--- /dev/null
+++ b/lib/recaptcha/client/captcha.py
@@ -0,0 +1,96 @@
+import urllib2, urllib
+
+API_SSL_SERVER="https://api-secure.recaptcha.net"
+API_SERVER="http://api.recaptcha.net"
+VERIFY_SERVER="api-verify.recaptcha.net"
+
+class RecaptchaResponse(object):
+ def __init__(self, is_valid, error_code=None):
+ self.is_valid = is_valid
+ self.error_code = error_code
+
+def displayhtml (public_key,
+ use_ssl = False,
+ error = None):
+ """Gets the HTML to display for reCAPTCHA
+
+ public_key -- The public api key
+ use_ssl -- Should the request be sent over ssl?
+ error -- An error message to display (from RecaptchaResponse.error_code)"""
+
+ error_param = ''
+ if error:
+ error_param = '&error=%s' % error
+
+ if use_ssl:
+ server = API_SSL_SERVER
+ else:
+ server = API_SERVER
+
+ return """<script type="text/javascript" src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s"></script>
+
+<noscript>
+ <iframe src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s" height="300" width="500" frameborder="0"></iframe><br />
+ <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+ <input type='hidden' name='recaptcha_response_field' value='manual_challenge' />
+</noscript>
+""" % {
+ 'ApiServer' : server,
+ 'PublicKey' : public_key,
+ 'ErrorParam' : error_param,
+ }
+
+
+def submit (recaptcha_challenge_field,
+ recaptcha_response_field,
+ private_key,
+ remoteip):
+ """
+ Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
+ for the request
+
+ recaptcha_challenge_field -- The value of recaptcha_challenge_field from the form
+ recaptcha_response_field -- The value of recaptcha_response_field from the form
+ private_key -- your reCAPTCHA private key
+ remoteip -- the user's ip address
+ """
+
+ if not (recaptcha_response_field and recaptcha_challenge_field and
+ len (recaptcha_response_field) and len (recaptcha_challenge_field)):
+ return RecaptchaResponse (is_valid = False, error_code = 'incorrect-captcha-sol')
+
+
+ def encode_if_necessary(s):
+ if isinstance(s, unicode):
+ return s.encode('utf-8')
+ return s
+
+ params = urllib.urlencode ({
+ 'privatekey': encode_if_necessary(private_key),
+ 'remoteip' : encode_if_necessary(remoteip),
+ 'challenge': encode_if_necessary(recaptcha_challenge_field),
+ 'response' : encode_if_necessary(recaptcha_response_field),
+ })
+
+ request = urllib2.Request (
+ url = "http://%s/verify" % VERIFY_SERVER,
+ data = params,
+ headers = {
+ "Content-type": "application/x-www-form-urlencoded",
+ "User-agent": "reCAPTCHA Python"
+ }
+ )
+
+ httpresp = urllib2.urlopen (request)
+
+ return_values = httpresp.read ().splitlines ();
+ httpresp.close();
+
+ return_code = return_values [0]
+
+ if (return_code == "true"):
+ return RecaptchaResponse (is_valid=True)
+ else:
+ return RecaptchaResponse (is_valid=False, error_code = return_values [1])
+
+
diff --git a/lib/recaptcha/client/mailhide.py b/lib/recaptcha/client/mailhide.py
new file mode 100644
index 0000000..7d9f6ef
--- /dev/null
+++ b/lib/recaptcha/client/mailhide.py
@@ -0,0 +1,68 @@
+import base64
+import cgi
+
+try:
+ from Crypto.Cipher import AES
+except:
+ raise Exception ("You need the pycrpyto library: http://cheeseshop.python.org/pypi/pycrypto/")
+
+MAIL_HIDE_BASE="http://mailhide.recaptcha.net"
+
+def asurl (email,
+ public_key,
+ private_key):
+ """Wraps an email address with reCAPTCHA mailhide and
+ returns the url. public_key is the public key from reCAPTCHA
+ (in the base 64 encoded format). Private key is the AES key, and should
+ be 32 hex chars."""
+
+ cryptmail = _encrypt_string (email, base64.b16decode (private_key, casefold=True), '\0' * 16)
+ base64crypt = base64.urlsafe_b64encode (cryptmail)
+
+ return "%s/d?k=%s&c=%s" % (MAIL_HIDE_BASE, public_key, base64crypt)
+
+def ashtml (email,
+ public_key,
+ private_key):
+ """Wraps an email address with reCAPTCHA Mailhide and
+ returns html that displays the email"""
+
+ url = asurl (email, public_key, private_key)
+ (userpart, domainpart) = _doterizeemail (email)
+
+ return """%(user)s<a href='%(url)s' onclick="window.open('%(url)s', '', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=500,height=300'); return false;" title="Reveal this e-mail address">...</a>@%(domain)s""" % {
+ 'user' : cgi.escape (userpart),
+ 'domain' : cgi.escape (domainpart),
+ 'url' : cgi.escape (url),
+ }
+
+
+def _pad_string (str, block_size):
+ numpad = block_size - (len (str) % block_size)
+ return str + numpad * chr (numpad)
+
+def _encrypt_string (str, aes_key, aes_iv):
+ if len (aes_key) != 16:
+ raise Exception ("expecting key of length 16")
+ if len (aes_iv) != 16:
+ raise Exception ("expecting iv of length 16")
+ return AES.new (aes_key, AES.MODE_CBC, aes_iv).encrypt (_pad_string (str, 16))
+
+def _doterizeemail (email):
+ """replaces part of the username with dots"""
+
+ try:
+ [user, domain] = email.split ('@')
+ except:
+ # handle invalid emails... sorta
+ user = email
+ domain = ""
+
+ if len(user) <= 4:
+ user_prefix = user[:1]
+ elif len(user) <= 6:
+ user_prefix = user[:3]
+ else:
+ user_prefix = user[:4]
+
+ return (user_prefix, domain)
diff --git a/lib/recaptcha_django/__init__.py b/lib/recaptcha_django/__init__.py
new file mode 100644
index 0000000..57591e6
--- /dev/null
+++ b/lib/recaptcha_django/__init__.py
@@ -0,0 +1,60 @@
+"""
+recaptcha-django
+
+ReCAPTCHA (Completely Automated Public Turing test to tell Computers and
+Humans Apart - while helping digitize books, newspapers, and old time radio
+shows) module for django
+"""
+
+from django.forms import Widget, Field, ValidationError
+from django.conf import settings
+from django.utils.translation import get_language
+from django.utils.html import conditional_escape
+from recaptcha.client import captcha
+
+class ReCaptchaWidget(Widget):
+ """
+ A Widget that renders a ReCAPTCHA form
+ """
+ options = ['theme', 'lang', 'custom_theme_widget', 'tabindex']
+
+ def render(self, name, value, attrs=None):
+ final_attrs = self.build_attrs(attrs)
+ error = final_attrs.get('error', None)
+ html = captcha.displayhtml(settings.RECAPTCHA_PUBLIC_KEY, error=error)
+ options = u',\n'.join([u'%s: "%s"' % (k, conditional_escape(v)) \
+ for k, v in final_attrs.items() if k in self.options])
+ return """<script type="text/javascript">
+ var RecaptchaOptions = {
+ %s
+ };
+ </script>
+ %s
+ """ % (options, html)
+
+
+ def value_from_datadict(self, data, files, name):
+ """
+ Generates Widget value from data dictionary.
+ """
+ try:
+ return {'challenge': data['recaptcha_challenge_field'],
+ 'response': data['recaptcha_response_field'],
+ 'ip': data['recaptcha_ip_field']}
+ except KeyError:
+ return None
+
+class ReCaptchaField(Field):
+ """
+ Field definition for a ReCAPTCHA
+ """
+ widget = ReCaptchaWidget
+
+ def clean(self, value):
+ resp = captcha.submit(value.get('challenge', None),
+ value.get('response', None),
+ settings.RECAPTCHA_PRIVATE_KEY,
+ value.get('ip', None))
+ if not resp.is_valid:
+ self.widget.attrs['error'] = resp.error_code
+ raise ValidationError(resp.error_code)
diff --git a/lib/recaptcha_django/middleware.py b/lib/recaptcha_django/middleware.py
new file mode 100644
index 0000000..4d592e6
--- /dev/null
+++ b/lib/recaptcha_django/middleware.py
@@ -0,0 +1,12 @@
+class ReCaptchaMiddleware(object):
+ """
+ A tiny middleware to automatically add IP address to ReCaptcha
+ POST requests
+ """
+ def process_request(self, request):
+ if request.method == 'POST' and \
+ 'recaptcha_challenge_field' in request.POST and \
+ 'recaptcha_ip_field' not in request.POST:
+ data = request.POST.copy()
+ data['recaptcha_ip_field'] = request.META['REMOTE_ADDR']
+ request.POST = data
diff --git a/local_settings.py.in b/local_settings.py.in
index 8578ce3..4329833 100644
--- a/local_settings.py.in
+++ b/local_settings.py.in
@@ -6,5 +6,10 @@ TEMPLATE_DEBUG = DEBUG
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = 'snowy.db' # Or path to database file if using sqlite3.
-OAUTH_AUTH_VIEW = "notes.views.note_oauth_auth_view"
-DOMAIN_NAME = 'localhost:8000'
\ No newline at end of file
+DOMAIN_NAME = 'localhost:8000'
+
+# Fill in this information from
+# http://recaptcha.net/api/getkey?app=snowy
+RECAPTCHA_ENABLED = False
+RECAPTCHA_PUBLIC_KEY = ''
+RECAPTCHA_PRIVATE_KEY = ''
diff --git a/settings.py b/settings.py
index e9bf1ec..1e6a429 100644
--- a/settings.py
+++ b/settings.py
@@ -19,6 +19,10 @@ DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+RECAPTCHA_ENABLED = False
+RECAPTCHA_PUBLIC_KEY = ''
+RECAPTCHA_PRIVATE_KEY = ''
+
PROJECT_NAME = 'Snowy'
# Local time zone for this installation. Choices can be found here:
@@ -85,6 +89,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.transaction.TransactionMiddleware',
'reversion.middleware.RevisionMiddleware',
+ 'recaptcha_django.middleware.ReCaptchaMiddleware',
)
ROOT_URLCONF = 'snowy.urls'
diff --git a/users/forms.py b/users/forms.py
index 71c611d..7ad6254 100644
--- a/users/forms.py
+++ b/users/forms.py
@@ -17,6 +17,8 @@
from registration.forms import RegistrationFormUniqueEmail
from django.utils.translation import ugettext_lazy as _
+from recaptcha_django import ReCaptchaField
+from django.conf import settings
from django import forms
class RegistrationFormUniqueUser(RegistrationFormUniqueEmail):
@@ -27,7 +29,25 @@ class RegistrationFormUniqueUser(RegistrationFormUniqueEmail):
username_blacklist = ['about', 'accounts', 'admin', 'api', 'blog',
'contact', 'css', 'friends', 'images', 'index.html',
'news', 'notes', 'oauth', 'pony', 'register',
- 'registration', 'site_media', 'snowy', 'tomboy' ]
+ 'registration', 'site_media', 'snowy', 'tomboy']
+
+ def __init__(self, *args, **kwargs):
+ # This must be done before we init our base class so that the field can
+ # be properly initialized in the base __init__.
+ if settings.RECAPTCHA_ENABLED:
+ self.captcha = ReCaptchaField(label=_(u'Word Verification'))
+
+ super(RegistrationFormUniqueUser, self).__init__(*args, **kwargs)
+
+ self.fields['username'].label = _(u'Username:')
+ self.fields['username'].help_text = _(u'Maximum of 30 characters in length.')
+
+ self.fields['email'].label = _(u'Email address:')
+
+ self.fields['password1'].label = _(u'Choose a password:')
+ self.fields['password1'].help_text = _(u'Minimum of 6 characters in length.')
+
+ self.fields['password2'].label = _(u'Re-enter password:')
def clean_username(self):
"""
@@ -37,3 +57,13 @@ class RegistrationFormUniqueUser(RegistrationFormUniqueEmail):
if username in self.username_blacklist:
raise forms.ValidationError(_(u'This username has been reserved. Please choose another.'))
return username
+
+ def clean_password1(self):
+ """
+ Validate that the password is at least 6 characters long.
+ """
+ password = self.cleaned_data['password1']
+ if len(password) < 6:
+ raise forms.ValidationError(_(u'Your password must be at least 6 characters long.'))
+
+ return password
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]