Skip to content

Commit

Permalink
0.1.0
Browse files Browse the repository at this point in the history
Django turnstile release
  • Loading branch information
zmh-program committed Dec 24, 2022
1 parent d3a573c commit e5ff5a1
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.idea
*.pyc
*.egg-info
build
dist
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include LICENSE
include README.rst
recursive-include turnstile/templates *
recursive-include docs *
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

109 changes: 109 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
===============
Django Turnstile
===============

Add Cloudflare Turnstile validator widget to the forms of your django project.


This project refers to github project django-hcaptcha (author: AndrejZbin)

Configuration
-------------

Add "turnstile" to your INSTALLED_APPS setting like this::

INSTALLED_APPS = [
...
'turnstile',
]

For development purposes no further configuration is required. By default, django-Turnstile will use dummy keys.

For production, you'll need to obtain your Turnstile site key and secret key and add them to you settings::

TURNSTILE_SITEKEY = '<your sitekey>'
TURNSTILE_SECRET = '<your secret key>'


You can also configure your Turnstile widget globally (`see all options <https://developers.cloudflare.com/turnstile>`_)::

TURNSTILE_DEFAULT_CONFIG = {
'onload': 'name_of_js_function',
'render': 'explicit',
'theme': 'dark', # do not use data- prefix
'size': 'compact', # do not use data- prefix
...
}

If you need to, you can also override default turnstile endpoints::


TURNSTILE_JS_API_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'

Use proxies::

TURNSTILE_PROXIES = {
'http': 'http://127.0.0.1:8000',
}

Change default verification timeout::

TURNSTILE_TIMEOUT = 5



Usage
-----------

Simply add TurnstileField to your forms::

from turnstile.fields import TurnstileField

class Forms(forms.Form):
....
turnstile = TurnstileField()
....

In your template, if you need to, you can then use `{{ form.turnstile }}` to access the field.

You can override default config by passing additional arguments::

class Forms(forms.Form):
....
turnstile = TurnstileField(theme='dark', size='compact')
....


How it Works
------------------

When a form is submitted by a user, Turnstile's JavaScript will send one POST parameter to your backend: `cf-turnstile-response`. It will be received by your app and will be used to complete the `turnstile` form field in your backend code.

When your app receives these two values, the following will happen:

- Your backend will send these values to the Cloudflare Turnstile servers
- Their servers will indicate whether the values in the fields are correct
- If so, your `turnstile` form field will validate correctly

Unit Tests
--------------
You will need to disable the Turnstile field in your unit tests, since your tests obviously cannot complete the Turnstile successfully. One way to do so might be something like:

.. code-block:: python
from unittest.mock import MagicMock, patch
from django.test import TestCase
@patch("turnstile.fields.TurnstileField.validate", return_value=True)
class ContactTest(TestCase):
test_msg = {
"name": "pandora",
"message": "xyz",
"turnstile": "xxx", # Any truthy value is fine
}
def test_something(self, mock: MagicMock) -> None:
response = self.client.post("/contact/", self.test_msg)
self.assertEqual(response.status_code, HTTP_302_FOUND)
31 changes: 31 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[metadata]
name = django-turnstile
version = 0.1.0
description = Add Cloudflare Turnstile validator widget to the forms of your django project.
long_description = file: README.rst
url = https://github.com/zmh-program
author = zmh-program
author_email = [email protected]
license = MIT
classifiers =
Environment :: Web Environment
Framework :: Django
Framework :: Django :: 2.2
Framework :: Django :: 3.2
Framework :: Django :: 4.0
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content

[options]
include_package_data = true
packages = find:
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from setuptools import setup

setup()
Empty file added turnstile/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions turnstile/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class TurnstileConfig(AppConfig):
name = 'turnstile'
64 changes: 64 additions & 0 deletions turnstile/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import inspect
import json
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import build_opener, Request, ProxyHandler

from django import forms
from django.utils.translation import gettext_lazy as _

from turnstile.settings import DEFAULT_CONFIG, PROXIES, SECRET, TIMEOUT, VERIFY_URL
from turnstile.widgets import TurnstileWidget


class TurnstileField(forms.Field):
widget = TurnstileWidget
default_error_messages = {
'error_turnstile': _('Turnstile could not be verified.'),
'invalid_turnstile': _('Turnstile could not be verified.'),
'required': _('Please prove you are a human.'),
}

def __init__(self, **kwargs):
superclass_parameters = inspect.signature(super().__init__).parameters
superclass_kwargs = {}
widget_settings = DEFAULT_CONFIG.copy()
for key, value in kwargs.items():
if key in superclass_parameters:
superclass_kwargs[key] = value
else:
widget_settings[key] = value

widget_url_settings = {}
for prop in filter(lambda p: p in widget_settings, ('onload', 'render', 'hl')):
widget_url_settings[prop] = widget_settings[prop]
del widget_settings[prop]
self.widget_settings = widget_settings

super().__init__(**superclass_kwargs)

self.widget.extra_url = widget_url_settings

def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
for key, value in self.widget_settings.items():
attrs['data-%s' % key] = value
return attrs

def validate(self, value):
super().validate(value)
opener = build_opener(ProxyHandler(PROXIES))
post_data = urlencode({
'secret': SECRET,
'response': value,
}).encode()
request = Request(VERIFY_URL, post_data)
try:
response = opener.open(request, timeout=TIMEOUT)
except HTTPError:
raise forms.ValidationError(self.error_messages['error_turnstile'], code='error_turnstile')

response_data = json.loads(response.read().decode("utf-8"))

if not response_data.get('success'):
raise forms.ValidationError(self.error_messages['invalid_turnstile'], code='invalid_turnstile')
Empty file.
9 changes: 9 additions & 0 deletions turnstile/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.conf import settings

JS_API_URL = getattr(settings, 'TURNSTILE_JS_API_URL', 'https://challenges.cloudflare.com/turnstile/v0/api.js')
VERIFY_URL = getattr(settings, 'TURNSTILE_VERIFY_URL', 'https://challenges.cloudflare.com/turnstile/v0/siteverify')
SITEKEY = getattr(settings, 'TURNSTILE_SITEKEY', '1x00000000000000000000AA')
SECRET = getattr(settings, 'TURNSTILE_SECRET', '1x0000000000000000000000000000000AA')
TIMEOUT = getattr(settings, 'TURNSTILE_TIMEOUT', 5)
DEFAULT_CONFIG = getattr(settings, 'TURNSTILE_DEFAULT_CONFIG', {})
PROXIES = getattr(settings, 'TURNSTILE_PROXIES', {})
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<script src="{{ api_url }}" async defer></script>
<div class="cf-turnstile" {% include "django/forms/widgets/attrs.html" %}></div>
26 changes: 26 additions & 0 deletions turnstile/widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from urllib.parse import urlencode
from django import forms
from turnstile.settings import JS_API_URL, SITEKEY


class TurnstileWidget(forms.Widget):
template_name = 'turnstile/forms/widgets/turnstile_widget.html'

def __init__(self, *args, **kwargs):
self.extra_url = {}
super().__init__(*args, **kwargs)

def value_from_datadict(self, data, files, name):
return data.get('cf-turnstile-response')

def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
attrs['data-sitekey'] = SITEKEY
return attrs

def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['api_url'] = JS_API_URL
if self.extra_url:
context['api_url'] += '?' + urlencode(self.extra_url)
return context

0 comments on commit e5ff5a1

Please sign in to comment.