0001-misc-add-rate-limiting-to-tracking-code-URLs-35395.patch
combo/apps/wcs/views.py | ||
---|---|---|
17 | 17 |
import re |
18 | 18 | |
19 | 19 |
from django.contrib import messages |
20 |
from django.conf import settings |
|
21 |
from django.core.exceptions import PermissionDenied |
|
20 | 22 |
from django.core.urlresolvers import reverse |
21 | 23 |
from django.http import JsonResponse, HttpResponseRedirect, HttpResponseBadRequest |
22 | 24 |
from django.utils.http import urlquote |
... | ... | |
25 | 27 |
from django.views.decorators.csrf import csrf_exempt |
26 | 28 |
from django.views.generic import View |
27 | 29 | |
30 |
import ratelimit.utils |
|
31 | ||
28 | 32 |
from .models import TrackingCodeInputCell |
29 | 33 |
from .utils import get_wcs_services |
30 | 34 | |
... | ... | |
41 | 45 |
return super(TrackingCodeView, self).dispatch(*args, **kwargs) |
42 | 46 | |
43 | 47 |
@classmethod |
44 |
def search(self, code, wcs_site=None): |
|
48 |
def search(self, code, request, wcs_site=None):
|
|
45 | 49 |
code = code.strip().upper() |
46 | 50 |
if wcs_site: |
47 | 51 |
wcs_sites = [get_wcs_services().get(wcs_site)] |
48 | 52 |
else: |
49 | 53 |
wcs_sites = get_wcs_services().values() |
50 | 54 | |
55 |
rate_limit_option = settings.WCS_TRACKING_CODE_RATE_LIMIT |
|
56 |
if rate_limit_option and rate_limit_option != 'none': |
|
57 |
for rate_limit in rate_limit_option.split(): |
|
58 |
ratelimited = ratelimit.utils.is_ratelimited( |
|
59 |
request=request, |
|
60 |
group='trackingcode', |
|
61 |
key='ip', |
|
62 |
rate=rate_limit, |
|
63 |
increment=True) |
|
64 |
if ratelimited: |
|
65 |
raise PermissionDenied('rate limit reached (%s)' % rate_limit) |
|
66 | ||
51 | 67 |
for wcs_site in wcs_sites: |
52 | 68 |
response = requests.get('/api/code/' + urlquote(code), |
53 | 69 |
remote_service=wcs_site, log_errors=False) |
... | ... | |
65 | 81 |
return HttpResponseBadRequest('Missing code') |
66 | 82 |
code = request.POST['code'] |
67 | 83 | |
68 |
url = self.search(code, wcs_site=cell.wcs_site) |
|
69 |
if url: |
|
70 |
return HttpResponseRedirect(url) |
|
71 | ||
72 | 84 |
next_url = request.POST.get('url') or '/' |
73 | 85 |
next_netloc = urlparse.urlparse(next_url).netloc |
74 |
if not (next_netloc and next_netloc != urlparse.urlparse(request.build_absolute_uri()).netloc): |
|
75 |
messages.error(self.request, |
|
76 |
_(u'The tracking code could not been found.')) |
|
86 |
redirect_to_other_domain = bool( |
|
87 |
next_netloc and next_netloc != urlparse.urlparse(request.build_absolute_uri()).netloc) |
|
88 | ||
89 |
try: |
|
90 |
url = self.search(code, request, wcs_site=cell.wcs_site) |
|
91 |
except PermissionDenied: |
|
92 |
if redirect_to_other_domain: |
|
93 |
raise |
|
94 |
else: |
|
95 |
messages.error(self.request, |
|
96 |
_(u'Looking up tracking code is currently rate limited.')) |
|
77 | 97 |
else: |
78 |
if '?' in next_url: |
|
79 |
next_url += '&' |
|
98 |
if url: |
|
99 |
return HttpResponseRedirect(url) |
|
100 |
if redirect_to_other_domain: |
|
101 |
if '?' in next_url: |
|
102 |
next_url += '&' |
|
103 |
else: |
|
104 |
next_url += '?' |
|
105 |
next_url += 'unknown-tracking-code' |
|
80 | 106 |
else: |
81 |
next_url += '?'
|
|
82 |
next_url += 'unknown-tracking-code'
|
|
107 |
messages.error(self.request,
|
|
108 |
_(u'The tracking code could not been found.'))
|
|
83 | 109 | |
84 | 110 |
return HttpResponseRedirect(next_url) |
85 | 111 | |
86 | 112 | |
87 | 113 |
def tracking_code_search(request): |
88 | 114 |
hits = [] |
115 |
response = {'data': hits, 'err': 0} |
|
89 | 116 |
query = request.GET.get('q') or '' |
90 | 117 |
query = query.strip().upper() |
91 | 118 |
if re.match(r'^[BCDFGHJKLMNPQRSTVWXZ]{8}$', query): |
92 |
url = TrackingCodeView.search(query) |
|
93 |
if url: |
|
119 |
try: |
|
120 |
url = TrackingCodeView.search(query, request) |
|
121 |
except PermissionDenied: |
|
122 |
response['err'] = 1 |
|
94 | 123 |
hits.append({ |
95 |
'text': _('Use tracking code %s') % query,
|
|
96 |
'url': url,
|
|
124 |
'text': _('Looking up tracking code is currently rate limited.'),
|
|
125 |
'url': '#',
|
|
97 | 126 |
}) |
98 |
return JsonResponse({'data': hits}) |
|
127 |
else: |
|
128 |
if url: |
|
129 |
hits.append({ |
|
130 |
'text': _('Use tracking code %s') % query, |
|
131 |
'url': url, |
|
132 |
}) |
|
133 |
return JsonResponse(response) |
combo/settings.py | ||
---|---|---|
310 | 310 |
# default duration of notifications (in days) |
311 | 311 |
COMBO_DEFAULT_NOTIFICATION_DURATION = 3 |
312 | 312 | |
313 |
# tracking code thorttling |
|
314 |
WCS_TRACKING_CODE_RATE_LIMIT = '3/s 1500/d' |
|
315 | ||
313 | 316 |
# predefined slots for assets |
314 | 317 |
# example: {'banner': {'label': 'Banner image'}} |
315 | 318 |
COMBO_ASSET_SLOTS = {} |
debian/control | ||
---|---|---|
22 | 22 |
python-xstatic-roboto-fontface (<< 0.5.0.0), |
23 | 23 |
python-eopayment (>= 1.35), |
24 | 24 |
python-django-haystack (>= 2.4.0), |
25 |
python-django-ratelimit, |
|
25 | 26 |
python-sorl-thumbnail, |
26 | 27 |
python-pil, |
27 | 28 |
python-pywebpush, |
setup.py | ||
---|---|---|
161 | 161 |
'python-dateutil', |
162 | 162 |
'djangorestframework>=3.3, <3.7', |
163 | 163 |
'django-haystack', |
164 |
'django-ratelimit<3', |
|
164 | 165 |
'whoosh', |
165 | 166 |
'sorl-thumbnail', |
166 | 167 |
'Pillow', |
tests/conftest.py | ||
---|---|---|
50 | 50 |
except User.DoesNotExist: |
51 | 51 |
user = User.objects.create_superuser('admin', email=None, password='admin') |
52 | 52 |
return user |
53 | ||
54 | ||
55 |
@pytest.fixture |
|
56 |
def nocache(settings): |
|
57 |
settings.CACHES = { |
|
58 |
'default': { |
|
59 |
'BACKEND': 'django.core.cache.backends.dummy.DummyCache', |
|
60 |
} |
|
61 |
} |
tests/test_wcs.py | ||
---|---|---|
611 | 611 | |
612 | 612 | |
613 | 613 |
@wcs_present |
614 |
def test_tracking_code_cell(app): |
|
614 |
def test_tracking_code_cell(app, nocache):
|
|
615 | 615 |
Page.objects.all().delete() |
616 | 616 |
page = Page(title='One', slug='index', template_name='standard') |
617 | 617 |
page.save() |
... | ... | |
713 | 713 |
assert u'>Picture — form title (test)<' in resp.text |
714 | 714 | |
715 | 715 |
@wcs_present |
716 |
def test_tracking_code_search(app): |
|
716 |
def test_tracking_code_search(app, nocache):
|
|
717 | 717 |
assert len(app.get('/api/search/tracking-code/').json.get('data')) == 0 |
718 |
assert app.get('/api/search/tracking-code/').json.get('err') == 0 |
|
718 | 719 |
assert len(app.get('/api/search/tracking-code/?q=123').json.get('data')) == 0 |
719 | 720 |
assert len(app.get('/api/search/tracking-code/?q=BBCCDFF').json.get('data')) == 0 |
720 | 721 |
assert len(app.get('/api/search/tracking-code/?q=BBCCDDFF').json.get('data')) == 0 |
... | ... | |
722 | 723 |
assert len(app.get('/api/search/tracking-code/?q=BBCCDDFFG').json.get('data')) == 0 |
723 | 724 |
assert len(app.get('/api/search/tracking-code/?q= cnphntfb').json.get('data')) == 1 |
724 | 725 | |
726 |
@wcs_present |
|
727 |
def test_tracking_code_search_rate_limit(app): |
|
728 |
for i in range(3): |
|
729 |
assert app.get('/api/search/tracking-code/?q=BBCCDDFF').json.get('err') == 0 |
|
730 |
assert app.get('/api/search/tracking-code/?q=BBCCDDFF').json.get('err') == 1 |
|
731 | ||
732 |
Page.objects.all().delete() |
|
733 |
page = Page(title='One', slug='index', template_name='standard') |
|
734 |
page.save() |
|
735 |
cell = TrackingCodeInputCell(page=page, placeholder='content', order=0) |
|
736 |
cell.save() |
|
737 | ||
738 |
resp = app.get('/') |
|
739 |
for i in range(3): # make sure we hit ratelimit |
|
740 |
app.get('/api/search/tracking-code/?q=BBCCDDFF') |
|
741 |
resp.form['code'] = 'FOOBAR' |
|
742 |
resp = resp.form.submit() |
|
743 |
assert resp.status_code == 302 |
|
744 |
resp = resp.follow() |
|
745 |
assert '<li class="error">Looking up tracking code is currently rate limited.</li>' in resp.text |
|
746 | ||
747 |
resp = app.get('/') |
|
748 |
for i in range(3): # make sure we hit ratelimit |
|
749 |
app.get('/api/search/tracking-code/?q=BBCCDDFF') |
|
750 |
resp.form['code'] = 'FOOBAR' |
|
751 |
resp.form['url'] = 'http://example.net/' |
|
752 |
resp = resp.form.submit(status=403) |
|
753 | ||
754 | ||
725 | 755 |
@wcs_present |
726 | 756 |
def test_wcs_search_engines(app): |
727 | 757 |
search_engines = engines.get_engines() |
728 |
- |