[tor-commits] [bridgedb/develop] Twisted implementation of reCaptcha's submit(); use SSL for CAPTCHA verify.
isis at torproject.org
isis at torproject.org
Sun Mar 16 16:38:46 UTC 2014
commit fa8bf463adbf7bc5e308527e29959d57f1754185
Author: Isis Lovecruft <isis at torproject.org>
Date: Tue Mar 4 05:26:01 2014 +0000
Twisted implementation of reCaptcha's submit(); use SSL for CAPTCHA verify.
* ADD module bridgedb.txrecaptcha.
* FIXES #11127
---
lib/bridgedb/txrecaptcha.py | 215 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 215 insertions(+)
diff --git a/lib/bridgedb/txrecaptcha.py b/lib/bridgedb/txrecaptcha.py
new file mode 100644
index 0000000..96c813b
--- /dev/null
+++ b/lib/bridgedb/txrecaptcha.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_txrecaptcha -*-
+#
+# This file is part of BridgeDB, a Tor bridge distribution system.
+#
+# :authors: Isis Lovecruft 0xA3ADB67A2CDB8B35 <isis at torproject.org>
+# :copyright: (c) 2013-2014, Isis Lovecruft
+# (c) 2007-2014, The Tor Project, Inc.
+# :license: 3-Clause BSD, see LICENSE for licensing information
+
+
+import logging
+import urllib
+
+from recaptcha.client.captcha import API_SSL_SERVER
+from recaptcha.client.captcha import RecaptchaResponse
+from recaptcha.client.captcha import displayhtml
+
+from twisted.internet import defer
+from twisted.internet import protocol
+from twisted.internet import reactor
+from twisted.python import failure
+from twisted.web import client
+from twisted.web.http_headers import Headers
+from twisted.web.iweb import IBodyProducer
+
+from zope.interface import implements
+
+from bridgedb.crypto import SSLVerifyingContextFactory
+
+
+API_SERVER = API_SSL_SERVER
+API_SSL_VERIFY_URL = "%s/verify" % API_SSL_SERVER
+
+_pool = client.HTTPConnectionPool(reactor, persistent=False)
+_pool.maxPersistentPerHost = 5
+_pool.cachedConnectionTimeout = 30
+_agent = client.Agent(reactor, pool=_pool)
+
+
+def _setAgent(agent):
+ """Set the global :attr:`agent`.
+
+ :param agent: An :api:`twisted.web.client.Agent` for issuing requests.
+ """
+ global _agent
+ _agent = agent
+
+def _getAgent(reactor=reactor, url=API_SSL_VERIFY_URL, pool=_pool,
+ connectTimeout=30, **kwargs):
+ """Create a :api:`twisted.web.client.Agent` which will verify the
+ certificate chain and hostname for the given **url**.
+
+ :param reactor: A provider of the
+ :api:`twisted.internet.interface.IReactorTCP` interface.
+ :param str url: The full URL which will be requested with the
+ ``Agent``. (default: :attr:`API_SSL_VERIFY_URL`)
+ :param pool: An :api:`twisted.web.client.HTTPConnectionPool`
+ instance. (default: :attr:`_pool`)
+ :type connectTimeout: None or int
+ :param connectTimeout: If not ``None``, the timeout passed to
+ :api:`twisted.internet.reactor.connectTCP` or
+ :api:`twisted.internet.reactor.connectSSL` for specifying the
+ connection timeout. (default: ``30``)
+ """
+ return client.Agent(reactor,
+ contextFactory=SSLVerifyingContextFactory(url),
+ connectTimeout=connectTimeout,
+ pool=pool,
+ **kwargs)
+
+_setAgent(_getAgent())
+
+
+class RecaptchaResponseError(ValueError):
+ """There was an error with the reCaptcha API server's response."""
+
+
+class RecaptchaResponseProtocol(protocol.Protocol):
+ """HTML parser which creates a :class:`RecaptchaResponse` from the body of
+ the reCaptcha API server's response.
+ """
+ def __init__(self, finished):
+ """Create a protocol for creating :class:`RecaptchaResponse`s.
+
+ :type finished: :api:`~twisted.internet.defer.Deferred`
+ :param finished: A deferred which will have its ``callback()`` called
+ with a :class:`RecaptchaResponse`.
+ """
+ self.finished = finished
+ self.remaining = 1024 * 10
+ self.response = ''
+
+ def dataReceived(self, data):
+ """Called when some data is received from the connection."""
+ if self.remaining:
+ received = data[:self.remaining]
+ self.response += received
+ self.remaining -= len(received)
+
+ def connectionLost(self, reason):
+ """Called when the connection was closed.
+
+ :type reason: :api:`twisted.python.failure.Failure`
+ :param reason: A string explaning why the connection was closed,
+ wrapped in a ``Failure`` instance.
+
+ :raises: A :api:`twisted.internet.error.ConnectError` if the
+ """
+ valid = False
+ error = reason.getErrorMessage()
+ try:
+ (valid, error) = self.response.strip().split('\n', 1)
+ except ValueError:
+ error = "Couldn't parse response from reCaptcha API server"
+
+ valid = bool(valid == "true")
+ result = RecaptchaResponse(is_valid=valid, error_code=error)
+ logging.debug(
+ "ReCaptcha API server response: %s(is_valid=%s, error_code=%s)"
+ % (result.__class__.__name__, valid, error))
+ self.finished.callback(result)
+
+
+class _BodyProducer(object):
+ """I write a string into the HTML body of an open request."""
+ implements(IBodyProducer)
+
+ def __init__(self, body):
+ self.body = body
+ self.length = len(body)
+
+ def startProducing(self, consumer):
+ """Start writing the HTML body."""
+ consumer.write(self.body)
+ return defer.succeed(None)
+
+ def pauseProducing(self):
+ pass
+
+ def stopProducing(self):
+ pass
+
+ def resumeProducing(self):
+ pass
+
+
+def _cbRequest(response):
+ """Callback for a :api:`twisted.web.client.Agent.request` which delivers
+ the result to a :class:`RecaptchaResponseProtocol`.
+
+ :returns: A :api:`~twisted.internet.defer.Deferred` which will callback
+ with a ``recaptcha.RecaptchaResponse`` for the request.
+ """
+ finished = defer.Deferred()
+ response.deliverBody(RecaptchaResponseProtocol(finished))
+ return finished
+
+def _ebRequest(fail):
+ """Errback for a :api:`twisted.web.client.Agent.request`.
+
+ :param fail: A :api:`twisted.python.failure.Failure` which occurred during
+ the request.
+ """
+ logging.debug("txrecaptcha._ebRequest() called with %r" % fail)
+ error = fail.getErrorMessage() or "possible problem in _ebRequest()"
+ return RecaptchaResponse(is_valid=False, error_code=error)
+
+def _encodeIfNecessary(string):
+ """Encode unicode objects in utf-8 if necessary."""
+ if isinstance(string, unicode):
+ return string.encode('utf-8')
+ return string
+
+def submit(recaptcha_challenge_field, recaptcha_response_field,
+ private_key, remoteip, agent=_agent):
+ """Submits a reCaptcha request for verification. This function is a patched
+ version of the ``recaptcha.client.captcha.submit()`` function in
+ reCaptcha's Python API.
+
+ It does two things differently:
+ 1. It uses Twisted for everything.
+ 2. It uses SSL/TLS for everything.
+
+ This function returns a :api:`~twisted.internet.defer.Deferred`. If you
+ need a ``recaptcha.client.captcha.RecaptchaResponse`` to be returned, use
+ the :func:`submit` function, which is an ``@inlineCallbacks`` wrapper for
+ this function.
+
+ :param str recaptcha_challenge_field: The value of the HTTP POST
+ ``recaptcha_challenge_field`` argument from the form.
+ :param recaptcha_response_field: The value of the HTTP POST
+ ``recaptcha_response_field`` argument from the form.
+ :param private_key: The reCAPTCHA API private key.
+ :param remoteip: An IP address to give to the reCaptcha API server.
+ :returns: A :api:`~twisted.internet.defer.Deferred` which will callback
+ with a ``recaptcha.RecaptchaResponse`` for the request.
+ """
+ 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')
+
+ params = urllib.urlencode({
+ 'privatekey': _encodeIfNecessary(private_key),
+ 'remoteip': _encodeIfNecessary(remoteip),
+ 'challenge': _encodeIfNecessary(recaptcha_challenge_field),
+ 'response': _encodeIfNecessary(recaptcha_response_field)})
+ body = _BodyProducer(params)
+ headers = Headers({"Content-type": ["application/x-www-form-urlencoded"],
+ "User-agent": ["reCAPTCHA Python"]})
+ d = agent.request('POST', API_SSL_VERIFY_URL, headers, body)
+ d.addCallbacks(_cbRequest, _ebRequest)
+ return d
More information about the tor-commits
mailing list