commit 75f5895e729c79fcf585f7b0fc18313f4d9ea53f Author: Isis Lovecruft isis@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>
tor-commits@lists.torproject.org