1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
11
from django.conf import settings
14
class CaptchaResponse(object):
15
"""Response returned from _open() in case of error"""
17
def __init__(self, code, response, tb=None):
19
self.response = response
21
self.is_error = tb is not None
28
self._data = self.response.read()
34
class Captcha(object):
36
Class to capture & abstract interaction with external captcha service.
38
Current implementation uses reCaptcha service
40
.. note: There's no possibility to actually test verification actually
41
returning true, as response, for that you need human interaction.
43
Getting new captcha is quite simple:
45
>>> captcha = Captcha.new()
46
>>> captcha.serialize() #+doctest: ELLIPSIS
47
{'captcha_id': ..., 'image_url': ...}
49
As is verifying received solution:
51
>>> captcha = Captcha('captcha-id-received-from-client')
52
>>> captcha.verify("this-is-invalid-solution")
55
Once verified solution is cached, so calling again to .verify() method is
56
very cheap (and returns same result):
58
>>> captcha.verify("this-is-invalid-solution")
61
You can also get original response from reCaptcha:
63
>>> print captcha.response.data()
71
def __init__(self, env, captcha_id, image_url=None, response=None):
73
self.captcha_id = captcha_id
74
self.image_url = image_url
75
self.response = response
82
def _setup_opener(cls):
83
if cls.opener is not None:
85
if getattr(settings, 'CAPTCHA_USE_PROXY', False):
86
proxy_handler = urllib2.ProxyHandler(settings.CAPTCHA_PROXIES)
87
opener = urllib2.build_opener(proxy_handler)
89
opener = urllib2.build_opener()
94
'captcha_id': self.captcha_id,
95
'image_url': self.image_url,
101
url = (settings.CAPTCHA_API_URL +
102
'/challenge?k=%s' % settings.CAPTCHA_PUBLIC_KEY)
103
response = cls._open(url, env)
104
if response.is_error:
105
env['oops-dump'] = True
106
logging.warning(response.traceback)
107
logging.warning("Failed to connect to reCaptcha server")
108
return cls(env, None, None)
110
data = response.data()
111
m = re.search(r"challenge\s*:\s*'(.+?)'", data, re.M | re.S)
113
captcha_id = m.group(1)
114
image_url = settings.CAPTCHA_IMAGE_URL_PATTERN % captcha_id
116
captcha_id, image_url = None, None
117
return cls(env, captcha_id, image_url, response)
120
def _open(cls, request, env):
121
default_timeout = socket.getdefaulttimeout()
122
socket.setdefaulttimeout(getattr(settings, 'CAPTCHA_TIMEOUT', 10))
125
response = cls.opener.open(request)
126
except urllib2.URLError, e:
127
# Attribute depends weather we have HTTPError or URLError
128
error_code = e.code if hasattr(e, 'code') else e.reason[0]
129
if error_code not in (111, 113, 408, 500, 502, 503, 504):
130
# 111: Connection refused
131
# 113: No route to host
132
# 408: Request Timeout
133
# 500: Internal Server Error
135
# 503: Service Unavailable
136
# 504: Gateway Timeout
138
tb = traceback.format_exc()
139
return CaptchaResponse(error_code, None, tb)
141
socket.setdefaulttimeout(default_timeout)
143
return CaptchaResponse(response.code, response, None)
145
def verify(self, captcha_solution):
146
if self._verified is not None:
147
return self._verified
149
if getattr(settings, 'DISABLE_CAPTCHA_VERIFICATION', False):
153
request_data = urllib.urlencode({
154
'privatekey': settings.CAPTCHA_PRIVATE_KEY,
155
'remoteip': self.env['REMOTE_ADDR'],
156
'challenge': self.captcha_id,
157
'response': captcha_solution,
159
request = urllib2.Request(settings.CAPTCHA_VERIFY_URL, request_data)
160
self.response = self._open(request, self.env)
162
if not self.response.is_error:
163
response_data = self.response.data()
164
self.verified, self.message = response_data.split('\n', 1)
165
self._verified = self.verified.lower() == 'true'
166
elif self.captcha_id is None:
167
self.message = 'no-challenge'
168
self._verfied = False
170
self._verified = False
171
if getattr(self.env, '__setitem__', False):
172
self.env['oops-dump'] = True
173
logging.warning(self.response.traceback)
174
logging.warning("reCaptcha connection error")
175
return self._verified