[tor-commits] [bridgedb/master] Split general and reCaptcha functionality into separate classes.

isis at torproject.org isis at torproject.org
Sun Mar 16 19:04:57 UTC 2014


commit 75f5895e729c79fcf585f7b0fc18313f4d9ea53f
Author: Isis Lovecruft <isis at torproject.org>
Date:   Sat Mar 1 03:01:35 2014 +0000

    Split general and reCaptcha functionality into separate classes.
    
     * CHANGE captcha.CaptchaProtectedResource class to only implement
       general CAPTCHA functionality.
    
     * CHANGE render_* methods in captcha.CaptchaProtectedResource to call
       subclass overridable methods for actions which are implementation
       specific, i.e. only the reCaptcha class needs to create a fake IP
       address for a client (see the new
       ReCaptchaProtectedResource.getRemoteIP() method), so this
       functionality should be in a separate method rather than in
       captcha.CaptchaProtectedResource.render_POST().  See methods
       getCaptchaImage(), extractClientSolution(), and checkSolution().
    
     * ADD new captcha.ReCaptchaProtectedResource class which is a subclass
       of captcha.CaptchaProtectedResource, and which implements
       reCaptcha-specific functionality.
    
     * CHANGE lib/bridgedb/templates/captcha.html to use HTTP parameters
       'captcha_challenge_field' and 'captcha_response_field', rather than
       'recaptcha_challenge_field' and 'recaptcha_response_field' respectively.
---
 lib/bridgedb/HTTPServer.py          |  222 +++++++++++++++++++++++++++--------
 lib/bridgedb/templates/captcha.html |   18 +--
 2 files changed, 184 insertions(+), 56 deletions(-)

diff --git a/lib/bridgedb/HTTPServer.py b/lib/bridgedb/HTTPServer.py
index 63e48c9..ae14f62 100644
--- a/lib/bridgedb/HTTPServer.py
+++ b/lib/bridgedb/HTTPServer.py
@@ -112,14 +112,12 @@ def replaceErrorPage(error, template_name=None):
 
 
 class CaptchaProtectedResource(twisted.web.resource.Resource):
-    def __init__(self, useRecaptcha=False, recaptchaPrivKey='',
-            recaptchaPubKey='', remoteip='', useForwardedHeader=False,
-            resource=None):
+    """A general resource protected by some form of CAPTCHA."""
+
+    def __init__(self, useForwardedHeader=False, resource=None):
+        twisted.web.resource.Resource.__init__(self)
         self.isLeaf = resource.isLeaf
         self.useForwardedHeader = useForwardedHeader
-        self.recaptchaPrivKey = recaptchaPrivKey
-        self.recaptchaPubKey = recaptchaPubKey
-        self.recaptchaRemoteIP = remoteip
         self.resource = resource
 
     def getClientIP(self, request):
@@ -135,84 +133,212 @@ class CaptchaProtectedResource(twisted.web.resource.Resource):
             ip = request.getClientIP()
         return ip
 
+    def getCaptchaImage(self):
+        """Get a CAPTCHA image.
+
+        :returns: A 2-tuple of ``(image, challenge)``, where ``image`` is a
+                  binary, JPEG-encoded image, and ``challenge`` is a unique
+                  string. If unable to retrieve a CAPTCHA, returns
+                  ``(None, None)``.
+        """
+        return (None, None)
+
+    def extractClientSolution(self, request):
+        """Extract the client's CAPTCHA solution from a POST request.
+
+        This is used after receiving a POST request from a client (which
+        should contain their solution to the CAPTCHA), to extract the solution
+        and challenge strings.
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object for 'bridges.html'.
+        :returns: A redirect for a request for a new CAPTCHA if there was a
+                  problem. Otherwise, returns a 2-tuple of strings, the first
+                  is the client's CAPTCHA solution from the text input area,
+                  and the second is the challenge string.
+        """
+        try:
+            challenge = request.args['captcha_challenge_field'][0]
+            response = request.args['captcha_response_field'][0]
+        except:
+            return redirectTo(request.URLPath(), request)
+        return (challenge, response)
+
+    def checkSolution(self, request):
+        """Override this method to check a client's CAPTCHA solution.
+
+        :rtype: bool
+        :returns: ``True`` if the client correctly solved the CAPTCHA;
+                  ``False`` otherwise.
+        """
+        return False
+
     def render_GET(self, request):
         """Retrieve a ReCaptcha from the API server and serve it to the client.
 
         :type request: :api:`twisted.web.http.Request`
-        :param request: A ``Request`` object for 'bridges.html'.
+        :param request: A ``Request`` object for a page which should be
+                        protected by a CAPTCHA.
         :rtype: str
         :returns: A rendered HTML page containing a ReCaptcha challenge image
                   for the client to solve.
         """
-        c = captcha.ReCaptcha(self.recaptchaPubKey, self.recaptchaPrivKey)
-
-        try:
-            c.get()
-        except Exception as error:
-            logging.fatal("Connection to Recaptcha server failed: %s" % error)
-
-        if c.image is None:
-            logging.warn("No CAPTCHA image received from ReCaptcha server!")
+        image, challenge = self.getCaptchaImage()
 
         try:
             # TODO: this does not work for versions of IE < 8.0
-            imgstr = 'data:image/jpeg;base64,%s' % base64.b64encode(c.image)
+            imgstr = 'data:image/jpeg;base64,%s' % base64.b64encode(image)
             template = lookup.get_template('captcha.html')
             rendered = template.render(imgstr=imgstr,
-                                       challenge_field=c.challenge)
+                                       challenge_field=challenge)
         except Exception as err:
             rendered = replaceErrorPage(err, 'captcha.html')
 
         return rendered
 
     def render_POST(self, request):
-        """Process a client CAPTCHA by sending it to the ReCaptcha server.
+        """Process a client's CAPTCHA solution.
 
-        The client's IP address is not sent to the ReCaptcha server; instead,
-        a completely random IP is generated and sent instead.
+        If the client's CAPTCHA solution is valid (according to
+        :meth:`checkSolution`), process and serve their original
+        request. Otherwise, redirect them back to a new CAPTCHA page.
 
         :type request: :api:`twisted.web.http.Request`
-        :param request: A ``Request`` object containing the POST arguments
+        :param request: A ``Request`` object, including POST arguments which
                         should include two key/value pairs: one key being
-                        ``'recaptcha_challange_field'``, and the other,
-                        ``'recaptcha_response_field'``. These POST arguments
+                        ``'captcha_challenge_field'``, and the other,
+                        ``'captcha_response_field'``. These POST arguments
                         should be obtained from :meth:`render_GET`.
+        :rtype: str
+        :returns: A rendered HTML page containing a ReCaptcha challenge image
+                  for the client to solve.
         """
-        try:
-            challenge = request.args['recaptcha_challenge_field'][0]
-            response = request.args['recaptcha_response_field'][0]
-        except:
-            return redirectTo(request.URLPath(), request)
-
-        if self.recaptchaRemoteIP:
-            remote_ip = self.recaptchaRemoteIP
-        else:
-            # generate a random IP for the captcha submission
-            remote_ip = '%d.%d.%d.%d' % (randint(1,255),randint(1,255),
-                                         randint(1,255),randint(1,255))
-
-        recaptcha_response = recaptcha.submit(challenge, response,
-                                            self.recaptchaPrivKey, remote_ip)
-        logging.debug("Captcha from client with masked IP %r. Parameters:\n%r"
-                      % (remote_ip, request.args))
-
-        if recaptcha_response.is_valid:
-            logging.info("Valid recaptcha from client with masked IP %r."
-                         % remote_ip)
+        if self.checkSolution(request):
             try:
                 rendered = self.resource.render(request)
             except Exception as err:
                 rendered = replaceErrorPage(err)
             return rendered
-        else:
-            logging.info("Invalid recaptcha from client with masked IP %r: %r"
-                         % (remote_ip, recaptcha_response.error_code))
 
         logging.debug("Client failed a recaptcha; returning redirect to %s"
                       % request.uri)
         return redirectTo(request.uri, request)
 
 
+class ReCaptchaProtectedResource(CaptchaProtectedResource):
+    """A web resource which uses the reCaptcha_ service.
+
+    .. _reCaptcha: http://www.google.com/recaptcha
+    """
+
+    def __init__(self, recaptchaPrivKey='', recaptchaPubKey='', remoteip='',
+                 useForwardedHeader=False, resource=None):
+        CaptchaProtectedResource.__init__(self, useForwardedHeader, resource)
+        self.recaptchaPrivKey = recaptchaPrivKey
+        self.recaptchaPubKey = recaptchaPubKey
+        self.recaptchaRemoteIP = remoteip
+
+    def getCaptchaImage(self):
+        """Get a CAPTCHA image from the remote reCaptcha server.
+
+        :returns: A 2-tuple of ``(image, challenge)``, where::
+            - ``image`` is a string holding a binary, JPEG-encoded image.
+            - ``challenge`` is a unique string associated with the request.
+        """
+        c = captcha.ReCaptcha(self.recaptchaPubKey, self.recaptchaPrivKey)
+
+        try:
+            c.get()
+        except Exception as error:
+            logging.fatal("Connection to Recaptcha server failed: %s" % error)
+
+        if c.image is None:
+            logging.warn("No CAPTCHA image received from ReCaptcha server!")
+
+        return (c.image, c.challenge)
+
+    def getRemoteIP(self):
+        """Mask the client's real IP address with a faked one.
+
+        The fake client IP address is sent to the reCaptcha server, and it is
+        either the public IP address of bridges.torproject.org (if
+        ``RECAPTCHA_REMOTE_IP`` is configured), or a random IP.
+
+        :rtype: str
+        :returns: A fake IP address to report to the reCaptcha API server.
+        """
+        if self.recaptchaRemoteIP:
+            remoteIP = self.recaptchaRemoteIP
+        else:
+            # generate a random IP for the captcha submission
+            remoteIP = '%d.%d.%d.%d' % (randint(1,255),randint(1,255),
+                                        randint(1,255),randint(1,255))
+        return remoteIP
+
+    def checkSolution(self, request):
+        """Process a solved CAPTCHA by sending it to the ReCaptcha server.
+
+        The client's IP address is not sent to the ReCaptcha server; instead,
+        a completely random IP is generated and sent instead.
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object, including POST arguments which
+                        should include two key/value pairs: one key being
+                        ``'captcha_challenge_field'``, and the other,
+                        ``'captcha_response_field'``. These POST arguments
+                        should be obtained from :meth:`render_GET`.
+        :rtupe: bool
+        :returns: XXX
+        """
+        challenge, response = self.extractClientSolution(request)
+        clientIP = self.getClientIP(request)
+        remoteIP = self.getRemoteIP()
+        solution = recaptcha.submit(challenge, response,
+                                    self.recaptchaPrivKey, remoteIP)
+        logging.debug("Captcha from %r. Parameters: %r"
+                      % (Util.logSafely(clientIP), request.args))
+
+        if solution.is_valid:
+            logging.info("Valid CAPTCHA solution from %r."
+                         % Util.logSafely(clientIP))
+            return True
+        else:
+            logging.info("Invalid CAPTCHA solution from %r: %r"
+                         % (Util.logSafely(clientIP), solution.error_code))
+            return False
+
+    def render_GET(self, request):
+        """Retrieve a ReCaptcha from the API server and serve it to the client.
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object for 'bridges.html'.
+        :rtype: str
+        :returns: A rendered HTML page containing a ReCaptcha challenge image
+                  for the client to solve.
+        """
+        return CaptchaProtectedResource.render_GET(self, request)
+
+    def render_POST(self, request):
+        """Process a client's CAPTCHA solution.
+
+        If the client's CAPTCHA solution is valid (according to
+        :meth:`checkSolution`), process and serve their original
+        request. Otherwise, redirect them back to a new CAPTCHA page.
+
+        :type request: :api:`twisted.web.http.Request`
+        :param request: A ``Request`` object, including POST arguments which
+                        should include two key/value pairs: one key being
+                        ``'captcha_challenge_field'``, and the other,
+                        ``'captcha_response_field'``. These POST arguments
+                        should be obtained from :meth:`render_GET`.
+        :rtype: str
+        :returns: A rendered HTML page containing a ReCaptcha challenge image
+                  for the client to solve.
+        """
+        return CaptchaProtectedResource.render_POST(self, request)
+
+
+
 class WebResourceOptions(twisted.web.resource.Resource):
     """This resource is used by Twisted Web to give a web page with
        additional options that the user may use to specify the criteria
diff --git a/lib/bridgedb/templates/captcha.html b/lib/bridgedb/templates/captcha.html
index dcb8adb..a670072 100644
--- a/lib/bridgedb/templates/captcha.html
+++ b/lib/bridgedb/templates/captcha.html
@@ -1,14 +1,16 @@
-## -*- coding: utf-8 -*-
+## -*- coding: utf-8 -*-
 <%inherit file="base.html"/>
 <div class="captcha">
 <form action="" method="POST">
-  <input type="hidden" name="recaptcha_challenge_field"
-    id="recaptcha_challenge_field" value="${challenge_field}">
-  <img width="300" height="57" alt="${_('Upgrade your browser to Firefox')}" src="${imgstr}">
-  <div class="recaptcha_input_area">
-    <label for="recaptcha_response_field">${_("Type the two words")}</label></div>
-  <input name="recaptcha_response_field" id="recaptcha_response_field"
-    type="text" autocomplete="off">
+  <input type="hidden" name="captcha_challenge_field"
+    id="captcha_challenge_field" value="${challenge_field}">
+  <img width="300" height="57"
+       alt="${_('Your browser is not displaying images properly.')}"
+       src="${imgstr}">
+  <div class="captcha_input_area">
+    <label for="captcha_response_field">${_("Type the two words")}</label></div>
+  <input name="captcha_response_field" id="captcha_response_field"
+         type="text" autocomplete="off">
   <input class="btn btn-success" type="submit" name="submit" value="I am human">
 </form>
 </div>





More information about the tor-commits mailing list