Skip to content

Commit

Permalink
Merge a959fb2 into 97467c2
Browse files Browse the repository at this point in the history
  • Loading branch information
rgs258 committed May 15, 2020
2 parents 97467c2 + a959fb2 commit 8e8c717
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 39 deletions.
9 changes: 0 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,6 @@ install:

matrix:
include:
- python: "2.7"
env: TOXENV=django111-py27
- python: "2.7"
env: TOXENV=django2-py27
- python: "2.7"
env: TOXENV=django21-py27
- python: "2.7"
env: TOXENV=pycodestyle-py27

- python: "3.5"
env: TOXENV=django111-py35
- python: "3.5"
Expand Down
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ Other
* `Alexey Subbotin <https://github.com/dotsbb>`_
* `Sean Stewart <https://github.com/mindcruzer>`_
* `Rob Charlwood <https://github.com/robcharlwood>`_
* `Ryan Sullivan <https://github.com/rgs258>`_
14 changes: 13 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Requirements

Tested with:

* Python: 2.7, 3.5, 3.6, 3.7, 3.8
* Python: 3.5, 3.6, 3.7, 3.8
* Django: 1.11, 2.0, 2.1, 2.2, 3.0


Expand Down Expand Up @@ -80,6 +80,18 @@ Installation
This will change the Google JavaScript api domain as well as the client side field verification domain.

#. (OPTIONAL) When `Verify the origin of reCAPTCHA solutions` is unchecked in the reCaptcha settings, then you are required to check the hostname on your server verifying a solution as per the `reCAPTCHA Domain/Package Name Validation <https://developers.google.com/recaptcha/docs/domain_validation>`_. Set the RECAPTCHA_VALIDATE_HOSTNAME to a function that takes a str and returns True when when the domain is expected:

.. code-block:: python
def validate_hostname(hostname):
return re.compile("^.*\.valid\.com$").match(hostname)
RECAPTCHA_VALIDATE_HOSTNAME = validate_hostname
This will change the Google JavaScript api domain as well as the client side field verification domain.


Usage
-----

Expand Down
18 changes: 18 additions & 0 deletions captcha/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

from django.core.exceptions import ValidationError

class CaptchaHTTPError(ValidationError):
pass


class CaptchaValidationError(ValidationError):
pass


class CaptchaHostnameError(ValidationError):
pass


class CaptchaScoreError(ValidationError):
pass

50 changes: 30 additions & 20 deletions captcha/fields.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import logging
import os
import socket
import sys
import warnings

from captcha import client
from captcha._compat import HTTPError
from captcha.constants import TEST_PRIVATE_KEY, TEST_PUBLIC_KEY
from captcha.exceptions import CaptchaScoreError, CaptchaHostnameError, \
CaptchaValidationError, CaptchaHTTPError
from captcha.widgets import ReCaptchaV2Checkbox, ReCaptchaBase
from django import forms
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _

from captcha import client
from captcha._compat import HTTPError, urlencode
from captcha.constants import TEST_PRIVATE_KEY, TEST_PUBLIC_KEY
from captcha.widgets import ReCaptchaV2Checkbox, ReCaptchaBase, ReCaptchaV3


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -76,22 +71,36 @@ def validate(self, value):
remoteip=self.get_remote_ip(),
)

except HTTPError: # Catch timeouts, etc
raise ValidationError(
except HTTPError as e: # Catch timeouts, etc
raise CaptchaHTTPError(
self.error_messages["captcha_error"],
code="captcha_error"
)
) from e

if not check_captcha.is_valid:
logger.error(
"ReCAPTCHA validation failed due to: %s" %
check_captcha.error_codes
logger.log(
getattr(settings, "RECAPTCHA_LOG_LEVEL_VALIDATE", logging.ERROR),
"ReCAPTCHA validation failed due to: %s" % check_captcha.error_codes
)
raise ValidationError(
raise CaptchaValidationError(
self.error_messages["captcha_invalid"],
code="captcha_invalid"
)

validate_hostname = self.widget.attrs.get("validate_hostname")
if validate_hostname:
hostname = check_captcha.extra_data.get("hostname")
if not validate_hostname(hostname):
logger.log(
getattr(settings, "RECAPTCHA_LOG_LEVEL_HOSTNAME", logging.ERROR),
"ReCAPTCHA validation failed because hostname %s rejected" %
hostname
)
raise CaptchaHostnameError(
self.error_messages["captcha_invalid"],
code="captcha_invalid"
)

required_score = self.widget.attrs.get("required_score")
if required_score:
# Our score values need to be floats, as that is the expected
Expand All @@ -108,11 +117,12 @@ def validate(self, value):
score = float(check_captcha.extra_data.get("score", 0))

if required_score > score:
logger.error(
logger.log(
getattr(settings, "RECAPTCHA_LOG_LEVEL_SCORE", logging.ERROR),
"ReCAPTCHA validation failed due to its score of %s"
" being lower than the required amount." % score
)
raise ValidationError(
raise CaptchaScoreError(
self.error_messages["captcha_invalid"],
code="captcha_invalid"
)
2 changes: 1 addition & 1 deletion captcha/templates/captcha/widget_v2_checkbox.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% include "captcha/includes/js_v2_checkbox.html" %}
<div class="g-recaptcha"
<div
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
>
</div>
2 changes: 1 addition & 1 deletion captcha/templates/captcha/widget_v2_invisible.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% include "captcha/includes/js_v2_invisible.html" %}
<div class="g-recaptcha"
<div
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
>
</div>
2 changes: 1 addition & 1 deletion captcha/templates/captcha/widget_v3.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% include "captcha/includes/js_v3.html" %}
<input class="g-recaptcha"
<input
type="hidden"
name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
Expand Down
1 change: 0 additions & 1 deletion captcha/tests/requirements/py27.txt

This file was deleted.

65 changes: 65 additions & 0 deletions captcha/tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import uuid
import warnings

Expand All @@ -20,6 +21,16 @@ class DefaultForm(forms.Form):
captcha = fields.ReCaptchaField()


def validate_hostname(hostname):
"""
Validates a hostname against a pattern
:param hostname: a str that is a hostname that should match the pattern
:return: True if the hostname matches the pattern
"""
pattern = r"^.*\.valid\.com$"
return re.compile(pattern).match(hostname)


class TestFields(TestCase):

@patch("captcha.fields.client.submit")
Expand Down Expand Up @@ -91,6 +102,60 @@ def test_field_captcha_errors(self, mocked_response):
["Error verifying reCAPTCHA, please try again."]
)

@patch("captcha.fields.client.submit")
def test_validate_hostname_success_using_attr(self, mocked_submit):
mocked_submit.return_value = RecaptchaResponse(
is_valid=True,
extra_data={'hostname': 'example.valid.com'}
)
form_params = {"g-recaptcha-response": "PASSED"}

class HostnameForm(forms.Form):
captcha = fields.ReCaptchaField(
widget=widgets.ReCaptchaBase(
attrs={
"validate_hostname": validate_hostname,
},
)
)

form = HostnameForm(form_params)
self.assertTrue(form.is_valid())

@patch("captcha.fields.client.submit")
@override_settings(RECAPTCHA_VALIDATE_HOSTNAME=validate_hostname)
def test_validate_hostname_success(self, mocked_submit):
mocked_submit.return_value = RecaptchaResponse(
is_valid=True,
extra_data={'hostname': 'example.valid.com'}
)
form_params = {"g-recaptcha-response": "PASSED"}

class HostnameForm(forms.Form):
captcha = fields.ReCaptchaField(
widget=widgets.ReCaptchaBase()
)

form = HostnameForm(form_params)
self.assertTrue(form.is_valid())

@patch("captcha.fields.client.submit")
@override_settings(RECAPTCHA_VALIDATE_HOSTNAME=validate_hostname)
def test_validate_hostname_failure(self, mocked_submit):
mocked_submit.return_value = RecaptchaResponse(
is_valid=True,
extra_data={'hostname': 'example.invalid.com'}
)
form_params = {"g-recaptcha-response": "PASSED"}

class HostnameForm(forms.Form):
captcha = fields.ReCaptchaField(
widget=widgets.ReCaptchaBase()
)

form = HostnameForm(form_params)
self.assertFalse(form.is_valid())


class TestWidgets(TestCase):
@patch("captcha.widgets.uuid.UUID.hex", new_callable=PropertyMock)
Expand Down
8 changes: 8 additions & 0 deletions captcha/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def __init__(self, api_params=None, *args, **kwargs):
self.uuid = uuid.uuid4().hex
self.api_params = api_params or {}

if not self.attrs.get("validate_hostname", None):
self.attrs["validate_hostname"] = getattr(
settings, "RECAPTCHA_VALIDATE_HOSTNAME", None
)
if not self.attrs.get("class", None):
self.attrs["class"] = "g-recaptcha"


def value_from_datadict(self, data, files, name):
return data.get(self.recaptcha_response_name, None)

Expand Down
2 changes: 0 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
Expand Down
5 changes: 2 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tox]
envlist =
pycodestyle-{py27,py37,py38}
django{111,2,21}-{py27,py35,py36,py37}
pycodestyle-{py37,py38}
django{111,2,21}-{py35,py36,py37}
django{22}-{py35,py36,py37}
django{3}-{py36,py37,py38}

Expand All @@ -13,7 +13,6 @@ deps =
django21: Django<2.2
django22: Django<3.0
django3: Django<4.0
py27: -rcaptcha/tests/requirements/py27.txt
pycodestyle: pycodestyle
commands =
django{111,2,21,22,3}: coverage run manage.py test
Expand Down

0 comments on commit 8e8c717

Please sign in to comment.