[snowy] Add reCaptcha support to the registration form



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]