Projet

Général

Profil

0001-misc-add-rate-limiting-to-tracking-code-URLs-35395.patch

Frédéric Péters, 14 août 2019 11:51

Télécharger (9,08 ko)

Voir les différences:

Subject: [PATCH] misc: add rate limiting to tracking code URLs (#35395)

 combo/apps/wcs/views.py | 69 +++++++++++++++++++++++++++++++----------
 combo/settings.py       |  3 ++
 debian/control          |  1 +
 setup.py                |  1 +
 tests/conftest.py       |  9 ++++++
 tests/test_wcs.py       | 34 ++++++++++++++++++--
 6 files changed, 98 insertions(+), 19 deletions(-)
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
-