1
0
mirror of https://github.com/peeringdb/peeringdb.git synced 2024-05-11 05:55:09 +00:00

Captcha fallback for where re-captcha is unavailable (#38)

This commit is contained in:
Stefan Pratter
2019-05-02 17:57:55 +00:00
parent 7b1861a58d
commit e49d7c9b68
12 changed files with 183 additions and 31 deletions

View File

@@ -51,6 +51,7 @@ django = "==1.11.20"
uwsgi = "==2.0.14"
markdown = "==2.6.7"
"twentyc.rpc" = "==0.3.5"
django-simple-captcha = "==0.5.11"
unidecode = "==1.0.23"
[requires]

View File

@@ -63,6 +63,7 @@ INSTALLED_APPS += [
'oauth2_provider',
'peeringdb_server',
'reversion',
'captcha',
]
# django_peeringdb settings

View File

@@ -29,6 +29,11 @@ urlpatterns = [
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [
url(r'^captcha/', include('captcha.urls')),
]
urlpatterns += peeringdb_server.urls.urlpatterns
handler_404 = 'peeringdb_server.views.view_http_error_404'

View File

@@ -1,9 +1,18 @@
import re
from peeringdb_server.models import User, Organization
import requests
from django.contrib.auth import forms as auth_forms
from django import forms
from django.utils import timezone
from django_namespace_perms.constants import *
from django.utils.translation import ugettext_lazy as _
from django.conf import settings as dj_settings
from captcha.fields import CaptchaField
from captcha.models import CaptchaStore
from peeringdb_server.models import User, Organization
from peeringdb_server.inet import get_client_ip
class OrgAdminUserPermissionForm(forms.Form):
@@ -90,6 +99,10 @@ class UsernameRetrieveForm(forms.Form):
class UserCreationForm(auth_forms.UserCreationForm):
recaptcha = forms.CharField(required=False)
captcha = forms.CharField(required=False)
captcha_generator = CaptchaField(required=False)
class Meta:
model = User
fields = (
@@ -100,6 +113,39 @@ class UserCreationForm(auth_forms.UserCreationForm):
)
def clean(self):
super(UserCreationForm, self).clean()
recaptcha = self.cleaned_data.get("recaptcha", "")
captcha = self.cleaned_data.get("captcha", "")
if not recaptcha and not captcha:
raise forms.ValidationError(_("Please fill out the anti-spam challenge (captcha) field"))
elif recaptcha:
cpt_params = {
"secret": dj_settings.RECAPTCHA_SECRET_KEY,
"response": recaptcha,
"remoteip": get_client_ip(self.request)
}
cpt_response = requests.post(dj_settings.RECAPTCHA_VERIFY_URL,
params=cpt_params).json()
if not cpt_response.get("success"):
raise forms.ValidationError(_("reCAPTCHA invalid"))
else:
try:
hashkey, value = captcha.split(":")
self.captcha_object = CaptchaStore.objects.get(response=value,
hashkey=hashkey,
expiration__gt=timezone.now())
except CaptchaStore.DoesNotExist:
raise forms.ValidationError(_("captcha invalid"))
def delete_captcha(self):
captcha_object = getattr(self, "captcha_object", None)
if captcha_object:
captcha_object.delete()
class UserLocaleForm(forms.Form):
locale = forms.CharField()

View File

@@ -205,3 +205,14 @@ def get_prefix_protocol(prefix):
return "IPv6"
except ipaddress.AddressValueError:
raise ValueError("Prefix invalid")
def get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip

View File

@@ -8,6 +8,9 @@ PeeringDB = {
fac : {},
ix : {}
},
recaptcha_loaded : function() {
return (typeof grecaptcha == "object");
},
init : function() {
this.InlineSearch.init_search();
@@ -25,8 +28,25 @@ PeeringDB = {
});
$('#form-create-account').on('export', function(e, data) {
if(this.recaptcha_loaded()){
data.recaptcha = grecaptcha.getResponse();
});
} else {
data.captcha = $('#id_captcha_generator_0').val()+":"+$('#id_captcha_generator_1').val()
}
}.bind(this));
setTimeout(function() {
var fallback = $('#form-create-account').find('.fallback');
if(!this.recaptcha_loaded()) {
console.log("re-captcha could not be loaded, falling back to internal captcha");
$('#form-create-account').find('.recaptcha').hide();
fallback.show();
} else {
// since re-captcha loaded successfully we don't need a requirement
// on the fallback input
fallback.find('#id_captcha_0').attr('required', false);
}
}.bind(this), 3000);
this.fix_list_offsets();

View File

@@ -1288,3 +1288,37 @@ div[data-edit-name=ipaddr6]
.ixf-errors-overview small {
color: #666;
}
/**
* captcha
*/
div.captcha {
position: relative;
min-height: 50px;
}
div.captcha .loading {
padding-top: 5px;
position: absolute;
z-index: 0;
left: 0px;
right: 0px;
top: 0px;
}
div.captcha .captcha-container {
background-color: #f1f1f1;
left: 0px;
right: 0px;
top: 0px;
position: relative;
z-index: 1;
}
div.captcha .g-recaptcha {
}
div.captcha .fallback {
display: none;
}

View File

@@ -76,7 +76,19 @@
data-edit-name="last_name"
data-edit-type="string" />
<div class="captcha">
<div class="loading center"><img src="{{ STATIC_URL }}loading.gif"> {% trans "Loading anti-spam challenge" %} ...</div>
<div class="captcha-container">
<div class="g-recaptcha" data-sitekey="{{ RECAPTCHA_PUBLIC_KEY }}"></div>
<div class="fallback">
{% for field in register_form.visible_fields %}
{% if field.name == "captcha_generator" %}
{{ field }}
{% endif %}
{% endfor %}
</div>
</div>
</div>
<a class="btn btn-default" data-edit-action="submit">{% trans "Create" %}</a>
</div>
@@ -86,6 +98,7 @@
</div>
</div>
<script language="javascript" type="text/javascript">
$('#form-create-account').each(function(idx) {
$(this).on("action-success:submit", function(ev, data) {
@@ -95,7 +108,9 @@ $('#form-create-account').each(function(idx) {
window.document.location.href = "/verify"
})
$(this).on("action-error:submit", function(ev, data) {
if(PeeringDB.recaptcha_loaded()) {
grecaptcha.reset()
}
});
});
</script>

View File

@@ -122,14 +122,6 @@ def make_env(**data):
return env
def get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
def view_http_error_404(request):
template = loader.get_template('site/error_404.html')
@@ -661,27 +653,18 @@ def view_registration(request):
env = BASE_ENV.copy()
env.update({
'global_stats': global_stats(),
'register_form' : UserCreationForm(),
})
return HttpResponse(template.render(env, request))
elif request.method == "POST":
form = UserCreationForm(request.POST)
form.request = request
if not form.is_valid():
return JsonResponse(form.errors, status=400)
cpt = request.POST.get("recaptcha", "")
cpt_params = {
"secret": dj_settings.RECAPTCHA_SECRET_KEY,
"response": cpt,
"remoteip": get_client_ip(request)
}
cpt_response = requests.post(dj_settings.RECAPTCHA_VERIFY_URL,
params=cpt_params).json()
if not cpt_response.get("success"):
return JsonResponse({
"non_field_errors": [_("reCAPTCHA invalid")]
}, status=400)
errors = form.errors
errors["non_field_errors"] = errors.get("__all__",[])
return JsonResponse(errors, status=400)
email = form.cleaned_data["email"]
if EmailAddress.objects.filter(email=email).count() > 0:
@@ -708,6 +691,8 @@ def view_registration(request):
user.send_email_confirmation(signup=True, request=request)
form.delete_captcha()
return JsonResponse({"status": "ok"})

View File

@@ -32,6 +32,7 @@ django-tables2==1.0.4
django-vanilla-views==1.0.2
django-cors-headers==2.1.0
django-ratelimit==1.1.0
django-simple-captcha==0.5.11
# issue #95
passlib==1.7.1

View File

@@ -27,6 +27,7 @@ settings.configure(
'dal',
'dal_select2',
'corsheaders',
'captcha',
],
CACHES={
"default": {
@@ -159,6 +160,8 @@ settings.configure(
DATA_QUALITY_MIN_PREFIXLEN_V6 = 64,
DATA_QUALITY_MAX_PREFIXLEN_V6 = 116,
TUTORIAL_MODE=False,
CAPTCHA_TEST_MODE=True,
SITE_ID=1,
RATELIMITS={
"view_affiliate_to_org_POST": "100/m",
"resend_confirmation_mail": "2/m",

View File

@@ -1,9 +1,13 @@
import pytest
import json
import re
import pytest
from django.test import Client, TestCase, RequestFactory
from django.contrib.auth.models import Group
from captcha.models import CaptchaStore
import peeringdb_server.models as models
import peeringdb_server.views as views
@@ -271,3 +275,29 @@ class UserTests(TestCase):
with self.assertRaises(KeyError):
email = c.session["username_retrieve_email"]
def test_signup(self):
"""
test user signup with captcha fallback
"""
c = Client()
response = c.get("/register")
self.assertGreater(response.content.find('name="captcha_generator_0"'), -1)
m = re.search('name="captcha_generator_0" value="([^"]+)"', response.content)
captcha_obj = CaptchaStore.objects.get(hashkey=m.group(1))
response = c.post("/register", {
"username": "signuptest",
"password1": "signuptest_123",
"password2": "signuptest_123",
"email": "signuptest@localhost",
"captcha": "{}:{}".format(captcha_obj.hashkey, captcha_obj.response)
})
self.assertEqual( json.loads(response.content), {"status":"ok"})