commit f387a0a4d4bc5eeba6d0d8c965ebe35959ff5ce7 Author: Isis Lovecruft isis@torproject.org Date: Tue Mar 4 08:28:51 2014 +0000
Add bridgedb.captcha.GimpCaptcha implementation.
* ADD GimpCaptcha class, which handles retrieving cached CAPTCHAs from a local directory, creating challenge strings from the CAPTCHA answers, and later checking a client's proposed solution against the challenge. * FIXES #10809. --- lib/bridgedb/captcha.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+)
diff --git a/lib/bridgedb/captcha.py b/lib/bridgedb/captcha.py index 07b06d4..a76303f 100644 --- a/lib/bridgedb/captcha.py +++ b/lib/bridgedb/captcha.py @@ -32,6 +32,10 @@ gimp-captcha_ and then cached locally. .. _gimp-captcha: https://github.com/isislovecruft/gimp-captcha """
+import hashlib +import logging +import random +import os import urllib2
from BeautifulSoup import BeautifulSoup @@ -46,6 +50,9 @@ class ReCaptchaKeyError(Exception): msg = 'You must supply recaptcha API keys' Exception.__init__(self, msg)
+class GimpCaptchaError(Exception): + """General exception raised when a Gimp CAPTCHA cannot be retrieved.""" + class ICaptcha(Interface): """Interface specification for CAPTCHAs.""" image = Attribute("A CAPTCHA image.") @@ -64,6 +71,7 @@ class Captcha(object): def get(self): return self.image
+ class ReCaptcha(Captcha): """A reCaptcha CAPTCHA."""
@@ -94,3 +102,69 @@ class ReCaptcha(Captcha): self.challenge = str(soup.find('input', {'name' : 'recaptcha_challenge_field'})['value']) self.image = urllib2.urlopen(imgurl).read()
+ +class GimpCaptcha(Captcha): + """A cached CAPTCHA image which was created with Gimp.""" + + def __init__(self, cacheDir=None, clientIP=None): + """Create a ``GimpCaptcha`` which retrieves images from **cacheDir**. + + :raises GimpCaptchaError: if **cacheDir** is not a directory. + """ + if not os.path.isdir(cacheDir): + raise GimpCaptchaError("Gimp captcha cache isn't a directory: %r" + % cacheDir) + + self.image = None + self.challenge = None + self.cacheDir = cacheDir + self.clientIP = clientIP + super(GimpCaptcha, self).__init__() + + @classmethod + def check(cls, challenge, answer, clientIP=None): + """Check a client's CAPTCHA solution against the **challenge**. + + :rtype: bool + :returns: True if the CAPTCHA solution was correct. + """ + logging.debug("Checking CAPTCHA solution %r against challenge %r" + % (answer, challenge)) + solution = cls.createChallenge(answer, clientIP) + if (not challenge) or (challenge != solution): + return False + return True + + @classmethod + def createChallenge(cls, answer, clientIP=None): + """Hash a CAPTCHA answer together with a **clientIP**, if given. + + :param str answer: The answer (either actual, or a client's proposed + solution) to a CAPTCHA. + :param str clientIP: The client's IP address. + """ + challenge = '\n'.join([answer, str(clientIP)]) + return hashlib.sha256(challenge).hexdigest() + + def get(self): + """Get a random CAPTCHA from the cache directory. + + :raises GimpCaptchaError: if the chosen CAPTCHA image file could not + be read. + :returns: A 2-tuple of ``(captcha, None)``, where ``captcha`` is the + image file contents. + """ + imageFilename = random.choice(os.listdir(self.cacheDir)) + imagePath = os.path.join(self.cacheDir, imageFilename) + + try: + with open(imagePath) as imageFile: + self.image = imageFile.read() + except (OSError, IOError) as err: + raise GimpCaptchaError("Could not read Gimp captcha image file: %r" + % imageFilename) + + captchaAnswer = imageFilename.rsplit(os.path.extsep, 1)[0] + self.challenge = self.createChallenge(captchaAnswer, self.clientIP) + + return (self.image, self.challenge)