Captcha squashed commit. Contains:
basic captcha logic. Still must exempt upgraded users finishing captcha logic for v1. Note requires redis to temporarily stash hostnames, review for storage concerns the create_app part is weird added web test fixing tests changing icon for captcha insterstitial
This commit is contained in:
parent
40d21d8490
commit
e4ce811f51
|
@ -11,4 +11,4 @@ services:
|
|||
before_script:
|
||||
- psql -c 'create database formspree_test;' -U postgres
|
||||
env:
|
||||
- REDISTOGO_URL=0.0.0.0:6379 REDIS_URL='redis://h:@localhost:6379' RATE_LIMIT='120 per hour' TEST_DATABASE_URL=postgres:///formspree_test NONCE_SECRET='y0ur_n0nc3_s3cr3t' SECRET_KEY='y0ur_s3cr3t_k3y' HASHIDS_SALT=doesntmatter STRIPE_TEST_PUBLISHABLE_KEY=pk_test_XebfAaLvLpHeO2txAgqWgJPf STRIPE_TEST_SECRET_KEY=sk_test_MLGQEdAHgWy4Rces4khaIxuc
|
||||
- TESTING=true REDISTOGO_URL=0.0.0.0:6379 REDIS_URL='redis://h:@localhost:6379' RATE_LIMIT='120 per hour' TEST_DATABASE_URL=postgres:///formspree_test NONCE_SECRET='y0ur_n0nc3_s3cr3t' SECRET_KEY='y0ur_s3cr3t_k3y' HASHIDS_SALT=doesntmatter STRIPE_TEST_PUBLISHABLE_KEY=pk_test_XebfAaLvLpHeO2txAgqWgJPf STRIPE_TEST_SECRET_KEY=sk_test_MLGQEdAHgWy4Rces4khaIxuc
|
||||
|
|
|
@ -3,14 +3,20 @@ import urlparse
|
|||
import requests
|
||||
import hashlib
|
||||
import hashids
|
||||
import uuid
|
||||
from urlparse import urljoin
|
||||
from flask import request, g
|
||||
|
||||
from formspree import settings
|
||||
from formspree.app import redis_store
|
||||
|
||||
CAPTCHA_URL = 'https://www.google.com/recaptcha/api/siteverify'
|
||||
CAPTCHA_VAL = 'g-recaptcha-response'
|
||||
|
||||
HASH = lambda x, y: hashlib.md5(x.encode('utf-8')+y.encode('utf-8')+settings.NONCE_SECRET).hexdigest()
|
||||
EXCLUDE_KEYS = {'_gotcha', '_next', '_subject', '_cc', '_format'}
|
||||
MONTHLY_COUNTER_KEY = 'monthly_{form_id}_{month}'.format
|
||||
EXCLUDE_KEYS = {'_gotcha', '_next', '_subject', '_cc', '_format', CAPTCHA_VAL, '_host_nonce'}
|
||||
REDIS_COUNTER_KEY = 'monthly_{form_id}_{month}'.format
|
||||
REDIS_HOSTNAME_KEY = 'hostname_{nonce}'.format
|
||||
HASHIDS_CODEC = hashids.Hashids(alphabet='abcdefghijklmnopqrstuvwxyz',
|
||||
min_length=8,
|
||||
salt=settings.HASHIDS_SALT)
|
||||
|
@ -94,3 +100,31 @@ def sitewide_file_check(url, email):
|
|||
|
||||
g.log.warn('Email not found in sitewide file.', contents=res.text[:100])
|
||||
return False
|
||||
|
||||
|
||||
def verify_captcha(form_data, request):
|
||||
if not CAPTCHA_VAL in form_data:
|
||||
return False
|
||||
r = requests.post(CAPTCHA_URL, data={
|
||||
'secret': settings.RECAPTCHA_SECRET,
|
||||
'response': form_data[CAPTCHA_VAL],
|
||||
'remoteip': request.remote_addr,
|
||||
}, timeout=2)
|
||||
return r.ok and r.json().get('success')
|
||||
|
||||
|
||||
def temp_store_hostname(hostname, referrer):
|
||||
nonce = uuid.uuid4()
|
||||
key = REDIS_HOSTNAME_KEY(nonce=nonce)
|
||||
redis_store.set(key, hostname+','+referrer)
|
||||
redis_store.expire(key, 300000)
|
||||
return nonce
|
||||
|
||||
|
||||
def get_temp_hostname(nonce):
|
||||
key = REDIS_HOSTNAME_KEY(nonce=nonce)
|
||||
value = redis_store.get(key)
|
||||
if value == None: raise KeyError()
|
||||
redis_store.delete(key)
|
||||
return value.split(',')
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from flask import url_for, render_template, g
|
|||
from sqlalchemy.sql.expression import delete
|
||||
from werkzeug.datastructures import ImmutableMultiDict, \
|
||||
ImmutableOrderedMultiDict
|
||||
from helpers import HASH, HASHIDS_CODEC, MONTHLY_COUNTER_KEY, \
|
||||
from helpers import HASH, HASHIDS_CODEC, REDIS_COUNTER_KEY, \
|
||||
EXCLUDE_KEYS, http_form_to_dict, referrer_to_path
|
||||
|
||||
class Form(DB.Model):
|
||||
|
@ -87,6 +87,12 @@ class Form(DB.Model):
|
|||
.filter(Form.id == self.id)
|
||||
return by_email.union(by_creation)
|
||||
|
||||
|
||||
@property
|
||||
def upgraded(self):
|
||||
upgraded_controllers = [i for i in self.controllers if i.upgraded]
|
||||
return len(upgraded_controllers) > 0
|
||||
|
||||
@classmethod
|
||||
def get_with_hashid(cls, hashid):
|
||||
try:
|
||||
|
@ -229,14 +235,14 @@ class Form(DB.Model):
|
|||
def get_monthly_counter(self, basedate=None):
|
||||
basedate = basedate or datetime.datetime.now()
|
||||
month = basedate.month
|
||||
key = MONTHLY_COUNTER_KEY(form_id=self.id, month=month)
|
||||
key = REDIS_COUNTER_KEY(form_id=self.id, month=month)
|
||||
counter = redis_store.get(key) or 0
|
||||
return int(counter)
|
||||
|
||||
def increase_monthly_counter(self, basedate=None):
|
||||
basedate = basedate or datetime.datetime.now()
|
||||
month = basedate.month
|
||||
key = MONTHLY_COUNTER_KEY(form_id=self.id, month=month)
|
||||
key = REDIS_COUNTER_KEY(form_id=self.id, month=month)
|
||||
redis_store.incr(key)
|
||||
redis_store.expireat(key, unix_time_for_12_months_from_now(basedate))
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ from formspree.app import DB
|
|||
from formspree.utils import request_wants_json, jsonerror, IS_VALID_EMAIL
|
||||
from helpers import ordered_storage, referrer_to_path, remove_www, \
|
||||
referrer_to_baseurl, sitewide_file_check, \
|
||||
verify_captcha, temp_store_hostname, get_temp_hostname, \
|
||||
HASH, EXCLUDE_KEYS
|
||||
from models import Form, Submission
|
||||
|
||||
|
@ -42,7 +43,13 @@ def send(email_or_string):
|
|||
title='Form should POST',
|
||||
text='Make sure your form has the <span class="code"><strong>method="POST"</strong></span> attribute'), 405
|
||||
|
||||
host = referrer_to_path(request.referrer)
|
||||
received_data = request.form or request.get_json() or {}
|
||||
try:
|
||||
# Get stored hostname from redis (from captcha)
|
||||
host, referrer = get_temp_hostname(received_data['_host_nonce'])
|
||||
except KeyError:
|
||||
host, referrer = referrer_to_path(request.referrer), request.referrer
|
||||
|
||||
if not host or host == 'www.google.com':
|
||||
if request_wants_json():
|
||||
return jsonerror(400, {'error': "Invalid \"Referrer\" header"})
|
||||
|
@ -130,9 +137,22 @@ def send(email_or_string):
|
|||
|
||||
# If form exists and is confirmed, send email
|
||||
# otherwise send a confirmation email
|
||||
received_data = request.form or request.get_json() or {}
|
||||
|
||||
if form.confirmed:
|
||||
status = form.send(received_data, request.referrer)
|
||||
captcha_verified = verify_captcha(received_data, request)
|
||||
needs_captcha = not (request_wants_json() or
|
||||
form.upgraded or
|
||||
captcha_verified or
|
||||
settings.TESTING)
|
||||
if needs_captcha:
|
||||
data_copy = received_data.copy()
|
||||
# Temporarily store hostname in redis while doing captcha
|
||||
data_copy['_host_nonce'] = temp_store_hostname(form.host, request.referrer)
|
||||
action = urljoin(settings.API_ROOT, email_or_string)
|
||||
return render_template('forms/captcha.html',
|
||||
data=data_copy,
|
||||
action=action)
|
||||
status = form.send(received_data, referrer)
|
||||
else:
|
||||
status = form.send_confirmation()
|
||||
|
||||
|
@ -149,7 +169,7 @@ def send(email_or_string):
|
|||
return render_template(
|
||||
'error.html',
|
||||
title='Can\'t send an empty form',
|
||||
text=u'<p>Make sure you have placed the <a href="http://www.w3schools.com/tags/att_input_name.asp" target="_blank"><code>"name"</code> attribute</a> in all your form elements. Also, to prevent empty form submissions, take a look at the <a href="http://www.w3schools.com/tags/att_input_required.asp" target="_blank"><code>"required"</code> property</a>.</p><p>This error also happens when you have an <code>"enctype"</code> attribute set in your <code><form></code>, so make sure you don\'t.</p><p><a href="{}">Return to form</a></p>'.format(request.referrer)
|
||||
text=u'<p>Make sure you have placed the <a href="http://www.w3schools.com/tags/att_input_name.asp" target="_blank"><code>"name"</code> attribute</a> in all your form elements. Also, to prevent empty form submissions, take a look at the <a href="http://www.w3schools.com/tags/att_input_required.asp" target="_blank"><code>"required"</code> property</a>.</p><p>This error also happens when you have an <code>"enctype"</code> attribute set in your <code><form></code>, so make sure you don\'t.</p><p><a href="{}">Return to form</a></p>'.format(referrer)
|
||||
), 400
|
||||
elif status['code'] == Form.STATUS_CONFIRMATION_SENT or \
|
||||
status['code'] == Form.STATUS_CONFIRMATION_DUPLICATED:
|
||||
|
@ -193,14 +213,8 @@ def resend_confirmation(email):
|
|||
g.log = g.log.bind(email=email, host=request.form.get('host'))
|
||||
g.log.info('Resending confirmation.')
|
||||
|
||||
# the first thing to do is to check the captcha
|
||||
r = requests.post('https://www.google.com/recaptcha/api/siteverify', data={
|
||||
'secret': settings.RECAPTCHA_SECRET,
|
||||
'response': request.form['g-recaptcha-response'],
|
||||
'remoteip': request.remote_addr
|
||||
})
|
||||
if r.ok and r.json().get('success'):
|
||||
# then proceed to check if this email is listed on SendGrid's bounces
|
||||
if verify_captcha(request.form, request):
|
||||
# check if this email is listed on SendGrid's bounces
|
||||
r = requests.get('https://api.sendgrid.com/api/bounces.get.json',
|
||||
params={
|
||||
'email': email,
|
||||
|
@ -255,14 +269,8 @@ def unblock_email(email):
|
|||
g.log = g.log.bind(email=email)
|
||||
g.log.info('Unblocking email on SendGrid.')
|
||||
|
||||
# check the captcha
|
||||
r = requests.post('https://www.google.com/recaptcha/api/siteverify', data={
|
||||
'secret': settings.RECAPTCHA_SECRET,
|
||||
'response': request.form['g-recaptcha-response'],
|
||||
'remoteip': request.remote_addr
|
||||
})
|
||||
if r.ok and r.json().get('success'):
|
||||
# then proceed to clear the bounce from SendGrid
|
||||
if verify_captcha(request.form, request):
|
||||
# clear the bounce from SendGrid
|
||||
r = requests.post(
|
||||
'https://api.sendgrid.com/api/bounces.delete.json',
|
||||
data={
|
||||
|
|
|
@ -8,7 +8,7 @@ if DEBUG:
|
|||
SQLALCHEMY_ECHO = True
|
||||
TESTING = os.getenv('TESTING') in ['True', 'true', '1', 'yes']
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') or os.getenv('DATABASE_URL')
|
||||
|
||||
LOG_LEVEL = os.getenv('LOG_LEVEL') or 'debug'
|
||||
|
||||
|
@ -18,7 +18,7 @@ HASHIDS_SALT = os.getenv('HASHIDS_SALT')
|
|||
|
||||
MONTHLY_SUBMISSIONS_LIMIT = int(os.getenv('MONTHLY_SUBMISSIONS_LIMIT') or 1000)
|
||||
ARCHIVED_SUBMISSIONS_LIMIT = int(os.getenv('ARCHIVED_SUBMISSIONS_LIMIT') or 100)
|
||||
REDIS_URL = os.getenv('REDISTOGO_URL') or os.getenv('REDISCLOUD_URL')
|
||||
REDIS_URL = os.getenv('REDISTOGO_URL') or os.getenv('REDISCLOUD_URL') or 'redis://localhost:6379'
|
||||
|
||||
CDN_URL = os.getenv('CDN_URL')
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
{% extends 'layouts/message.html' %}
|
||||
|
||||
{% block head_scripts %}
|
||||
<script type="text/javascript">
|
||||
|
||||
var success = function(response) {
|
||||
var form = document.querySelector('#passthrough');
|
||||
form.submit();
|
||||
};
|
||||
|
||||
var onloadCallback = function() {
|
||||
var form = document.querySelector('#passthrough');
|
||||
var data = {{ data|tojson|safe }};
|
||||
for (var key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
var input = document.createElement('input');
|
||||
input.setAttribute('name', key);
|
||||
input.setAttribute('value', data[key]);
|
||||
input.setAttribute('style', 'display:none');
|
||||
form.appendChild(input);
|
||||
}
|
||||
}
|
||||
|
||||
grecaptcha.render('recaptcha', {
|
||||
'sitekey' : {{ config.RECAPTCHA_KEY|tojson|safe }},
|
||||
'callback' : success
|
||||
});
|
||||
};
|
||||
</script>
|
||||
{% endblock head_scripts %}
|
||||
|
||||
{% block base %}
|
||||
<div class="container narrow card">
|
||||
<div class="container" id="header">
|
||||
<div class="col-1-1 header success">
|
||||
<i class="ion ion-email"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="col-1-1">
|
||||
<h1>Almost there</h1>
|
||||
<p>Please help us fight spam by following the directions below:</p>
|
||||
|
||||
<form action="{{ action }}" method="POST" id="passthrough">
|
||||
<div id="recaptcha" style="text-align:center; display:inline-block"></div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock base %}
|
||||
|
||||
{% block tail_scripts %}
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit"
|
||||
async defer>
|
||||
</script>
|
||||
{% endblock tail_scripts %}
|
|
@ -18,6 +18,7 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
|||
ga('create', 'UA-74724777-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{% block head_scripts %}{% endblock %}
|
||||
</head>
|
||||
<body class="{% block bodyclass %}{% endblock %}" id="{% block bodyid %}card{% endblock %}">
|
||||
|
||||
|
@ -73,6 +74,8 @@ ga('send', 'pageview');
|
|||
{% block footer %}{% endblock %}
|
||||
</div>
|
||||
|
||||
|
||||
{% block tail_scripts %}{% endblock %}
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/SlickNav/1.0.3/jquery.slicknav.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.1/toastr.min.js"></script>
|
||||
<script>
|
||||
|
|
|
@ -76,6 +76,12 @@ def unix_time_for_12_months_from_now(now=None):
|
|||
return calendar.timegm(start_of_next_month.utctimetuple())
|
||||
|
||||
|
||||
def unix_time_for_5_min_from_now(now=None):
|
||||
now = now or datetime.datetime.now()
|
||||
five_min_later = now + datetime.timedelta(minutes=5)
|
||||
return calendar.timegm(five_min_later.utctimetuple())
|
||||
|
||||
|
||||
def next_url(referrer=None, next=None):
|
||||
referrer = referrer if referrer is not None else ''
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from flask.ext.migrate import Migrate, MigrateCommand
|
|||
|
||||
from formspree import create_app, app
|
||||
from formspree.app import redis_store
|
||||
from formspree.forms.helpers import MONTHLY_COUNTER_KEY
|
||||
from formspree.forms.helpers import REDIS_COUNTER_KEY
|
||||
from formspree.forms.models import Form
|
||||
|
||||
forms_app = create_app()
|
||||
|
@ -82,7 +82,7 @@ def monthly_counters(email=None, host=None, id=None, month=datetime.date.today()
|
|||
return 1
|
||||
|
||||
for form in query:
|
||||
nsubmissions = redis_store.get(MONTHLY_COUNTER_KEY(form_id=form.id, month=month)) or 0
|
||||
nsubmissions = redis_store.get(REDIS_COUNTER_KEY(form_id=form.id, month=month)) or 0
|
||||
print '%s submissions for %s' % (nsubmissions, form)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<html>
|
||||
<form action="https://formspree-test.herokuapp.com/test@formspree.io" method="POST">
|
||||
<input type="text" name="email" placeholder="email" />
|
||||
<input type="text" name="message" placeholder="message" />
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<form action="http://localhost:5000/test@formspree.io" method="POST">
|
||||
<input type="text" name="email" placeholder="email" />
|
||||
<input type="text" name="message" placeholder="message" />
|
||||
<input type="hidden" name="_next" value="http://localhost:8000/thanks.html"/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
</html>
|
|
@ -0,0 +1,4 @@
|
|||
<html>
|
||||
<h1>Thanks!</h1>
|
||||
<a href="test.html">Back home</a>
|
||||
</html>
|
Loading…
Reference in New Issue