From 3faba84bc8d51782fbbade84ad3f4557dfef57d1 Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Wed, 17 Sep 2025 20:06:57 -0300
Subject: [PATCH 1/5] =?UTF-8?q?Fix=20recibo=20proposi=C3=A7=C3=A3o=20e=20a?=
=?UTF-8?q?diciona=20rate=20limiter=20em=20mat=C3=A9ria=20e=20norma?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.dockerignore | 7 +++++++
docker/config/nginx/sapl.conf | 2 +-
docker/startup_scripts/gunicorn.conf.py | 3 ++-
docker/startup_scripts/start.sh | 5 +----
requirements/requirements.txt | 3 +--
sapl/base/views.py | 2 +-
sapl/materia/views.py | 9 +++++++-
sapl/norma/views.py | 7 +++++++
sapl/sessao/views.py | 5 +++++
sapl/utils.py | 28 ++++++++++++++-----------
scripts/gunicorn_start.sh | 4 ++--
11 files changed, 51 insertions(+), 24 deletions(-)
diff --git a/.dockerignore b/.dockerignore
index 318700ad6..4ff7fdc82 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,3 +12,10 @@ bower
.travis.yml
.env
.idea
+.DS_Store
+.coveragerc
+*.swp
+.coveragerc
+.drone.yml
+.github
+release.sh
diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf
index 18641045e..a55731d02 100644
--- a/docker/config/nginx/sapl.conf
+++ b/docker/config/nginx/sapl.conf
@@ -7,7 +7,7 @@ upstream sapl_server {
server {
listen 80;
- server_name sapl.test;
+ server_name sapl.prod;
client_max_body_size 4G;
diff --git a/docker/startup_scripts/gunicorn.conf.py b/docker/startup_scripts/gunicorn.conf.py
index 95d2f0256..6bdcacb02 100644
--- a/docker/startup_scripts/gunicorn.conf.py
+++ b/docker/startup_scripts/gunicorn.conf.py
@@ -52,7 +52,8 @@ graceful_timeout = 30
keepalive = 10
backlog = 2048
max_requests = MAX_REQUESTS
-max_requests_jitter = 100
+max_requests_jitter = 200
+worker_max_memory_per_child = 300 * 1024 * 1024 # 300 MB cap
# Environment (same as exporting before running)
raw_env = [
diff --git a/docker/startup_scripts/start.sh b/docker/startup_scripts/start.sh
index 316d73f8a..bd98bdfc0 100755
--- a/docker/startup_scripts/start.sh
+++ b/docker/startup_scripts/start.sh
@@ -4,19 +4,16 @@ IFS=$'\n\t'
APP_DIR="/var/interlegis/sapl"
DATA_DIR="/var/interlegis/sapl/data"
-MEDIA_DIR="/var/interlegis/sapl/media"
RUN_DIR="/var/interlegis/sapl/run"
ENV_FILE="$APP_DIR/.env"
SECRET_FILE="$DATA_DIR/secret.key"
chown -R root:nginx "$RUN_DIR" || true
-chown -R root:nginx "$MEDIA_DIR" || true
chmod -R g+rwX "$RUN_DIR" || true
-chmod -R g+rwX "$MEDIA_DIR" || true
# setgid bit on our writable trees (not data/)
-find "$RUN_DIR" "$MEDIA_DIR" -type d -exec chmod g+s {} + 2>/dev/null || true
+find "$RUN_DIR" -type d -exec chmod g+s {} + 2>/dev/null || true
log() { printf '[%s] %s\n' "$(date -Is)" "$*"; }
err() { printf '[%s] ERROR: %s\n' "$(date -Is)" "$*" >&2; }
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 56c2459b4..aea9f52db 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -22,8 +22,7 @@ pytz==2019.3
python-magic==0.4.15
unipath==1.1
Pillow==10.3.0
-rlPyCairo==0.3.0
-reportlab==4.2.0
+reportlab==3.6.13
WeasyPrint==66
trml2pdf==0.6
gunicorn==23.0.0
diff --git a/sapl/base/views.py b/sapl/base/views.py
index ce3a0a717..7c935d421 100644
--- a/sapl/base/views.py
+++ b/sapl/base/views.py
@@ -68,7 +68,7 @@ class IndexView(TemplateView):
@method_decorator(ratelimit(key=lambda group, request: get_client_ip(request),
- rate='20/m',
+ rate='10/m',
method=ratelimit.UNSAFE,
block=True), name='dispatch')
class LoginSapl(views.LoginView):
diff --git a/sapl/materia/views.py b/sapl/materia/views.py
index f64d8134b..ce158228b 100644
--- a/sapl/materia/views.py
+++ b/sapl/materia/views.py
@@ -24,7 +24,6 @@ from django.shortcuts import render
from django.template import loader
from django.urls import reverse
from django.utils import formats, timezone
-from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.views.generic.base import RedirectView
@@ -32,6 +31,9 @@ from django.views.generic.edit import FormView
from django_filters.views import FilterView
import weasyprint
+from ratelimit.decorators import ratelimit
+from django.utils.decorators import method_decorator
+
import sapl
from sapl.base.email_utils import do_envia_email_confirmacao
from sapl.base.models import Autor, CasaLegislativa, AppConfig as BaseAppConfig
@@ -1459,6 +1461,7 @@ class TramitacaoCrud(MasterDetailCrud):
return initial
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class ListView(MasterDetailCrud.ListView):
def get_queryset(self):
@@ -1531,6 +1534,7 @@ class TramitacaoCrud(MasterDetailCrud):
return HttpResponseRedirect(url)
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class DetailView(MasterDetailCrud.DetailView):
template_name = "materia/tramitacao_detail.html"
@@ -1908,6 +1912,7 @@ class MateriaLegislativaCrud(Crud):
def get_success_url(self):
return self.search_url
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class DetailView(Crud.DetailView):
layout_key = 'MateriaLegislativaDetail'
@@ -1920,6 +1925,7 @@ class MateriaLegislativaCrud(Crud):
pk=self.kwargs['pk'])
return context
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class ListView(Crud.ListView, RedirectView):
def get_redirect_url(self, *args, **kwargs):
@@ -2040,6 +2046,7 @@ class AcompanhamentoExcluirView(TemplateView):
return HttpResponseRedirect(self.get_success_url())
+@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
model = MateriaLegislativa
filterset_class = MateriaLegislativaFilterSet
diff --git a/sapl/norma/views.py b/sapl/norma/views.py
index 27758a1ca..488e73c90 100644
--- a/sapl/norma/views.py
+++ b/sapl/norma/views.py
@@ -19,6 +19,9 @@ from django.views.generic.edit import FormView
from django_filters.views import FilterView
import weasyprint
+from ratelimit.decorators import ratelimit
+from django.utils.decorators import method_decorator
+
from sapl import settings
import sapl
from sapl.base.models import AppConfig
@@ -147,6 +150,7 @@ class NormaRelacionadaCrud(MasterDetailCrud):
layout_key = 'NormaRelacionadaDetail'
+@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class NormaPesquisaView(MultiFormatOutputMixin, FilterView):
model = NormaJuridica
filterset_class = NormaFilterSet
@@ -232,6 +236,7 @@ class AnexoNormaJuridicaCrud(MasterDetailCrud):
initial['ano'] = self.object.ano
return initial
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class DetailView(MasterDetailCrud.DetailView):
form_class = AnexoNormaJuridicaForm
layout_key = 'AnexoNormaJuridica'
@@ -280,6 +285,7 @@ class NormaCrud(Crud):
namespace = self.model._meta.app_config.name
return reverse('%s:%s' % (namespace, 'norma_pesquisa'))
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class DetailView(Crud.DetailView):
def get(self, request, *args, **kwargs):
estatisticas_acesso_normas = AppConfig.objects.first().estatisticas_acesso_normas
@@ -337,6 +343,7 @@ class NormaCrud(Crud):
layout_key = 'NormaJuridicaCreate'
+ @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class ListView(Crud.ListView):
def get(self, request, *args, **kwargs):
diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py
index 4835a17b6..7df367379 100755
--- a/sapl/sessao/views.py
+++ b/sapl/sessao/views.py
@@ -28,6 +28,9 @@ from django.views.generic.edit import FormMixin
from django_filters.views import FilterView
import pytz
+from ratelimit.decorators import ratelimit
+from django.utils.decorators import method_decorator
+
from sapl.base.models import AppConfig as AppsAppConfig
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud,
@@ -3794,6 +3797,7 @@ class SessaoListView(ListView):
return context
+@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class PautaSessaoView(TemplateView):
model = SessaoPlenaria
template_name = "sessao/pauta_inexistente.html"
@@ -3809,6 +3813,7 @@ class PautaSessaoView(TemplateView):
reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk}))
+@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
template_name = "sessao/pauta_sessao_detail.html"
model = SessaoPlenaria
diff --git a/sapl/utils.py b/sapl/utils.py
index 9d6399c5e..6554c5aea 100644
--- a/sapl/utils.py
+++ b/sapl/utils.py
@@ -419,21 +419,25 @@ def get_base_url(request):
return "{0}://{1}".format(protocol, current_domain)
-def create_barcode(value, width=170, height=50):
- '''
- creates a base64 encoded barcode PNG image
- '''
+def create_barcode(value, width=170, height=50, dpi=72):
+ """
+ creates a base64 encoded barcode PNG image
+ """
from base64 import b64encode
from reportlab.graphics.barcode import createBarcodeDrawing
+
value_bytes = bytes(value, "ascii")
- barcode = createBarcodeDrawing('Code128',
- value=value_bytes,
- barWidth=width,
- height=height,
- fontSize=2,
- humanReadable=True)
- data = b64encode(barcode.asString('png'))
- return data.decode('utf-8')
+ barcode = createBarcodeDrawing(
+ 'Code128',
+ value=value_bytes,
+ barWidth=width,
+ height=height,
+ fontSize=2,
+ humanReadable=True
+ )
+ # Lower DPI prevents Cairo surface from blowing up
+ png_bytes = barcode.asString("png", dpi=dpi)
+ return b64encode(png_bytes).decode("utf-8")
YES_NO_CHOICES = [(True, _('Sim')), (False, _('Não'))]
diff --git a/scripts/gunicorn_start.sh b/scripts/gunicorn_start.sh
index d01503235..bd2932985 100755
--- a/scripts/gunicorn_start.sh
+++ b/scripts/gunicorn_start.sh
@@ -15,8 +15,8 @@ then
fi
NAME="SAPL" # Name of the application (*)
-DJANGODIR=$SAPL_DIR/ # Django project directory (*)
-SOCKFILE=$SAPL_DIR/run/gunicorn.sock # we will communicate using this unix socket (*)
+DJANGODIR="$SAPL_DIR/" # Django project directory (*)
+SOCKFILE="$SAPL_DIR/run/gunicorn.sock" # we will communicate using this unix socket (*)
USER=`whoami` # the user to run as (*)
GROUP=`whoami` # the group to run as (*)
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn (*)
From b63a0cec38ddef03d5885c254dd2f605b63b66c2 Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Mon, 22 Sep 2025 14:06:04 -0300
Subject: [PATCH 2/5] Release: 3.1.164-RC4
---
CHANGES.md | 6 ++++++
docker/docker-compose.yaml | 2 +-
sapl/settings.py | 2 +-
sapl/templates/base.html | 2 +-
4 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 6902a4f67..f977fe2b5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,10 @@
+3.1.164-RC4 / 2025-09-22
+========================
+
+ * Fix recibo proposição e adiciona rate limiter em matéria e norma
+ * Release: 3.1.164-RC3
+
3.1.164-RC3 / 2025-09-16
========================
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 20c75374a..cd8b7a2a1 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -33,7 +33,7 @@ services:
networks:
- sapl-net
sapl:
- image: interlegis/sapl:3.1.164-RC3
+ image: interlegis/sapl:3.1.164-RC4
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
diff --git a/sapl/settings.py b/sapl/settings.py
index 5a0fdef73..c31b6b3b9 100644
--- a/sapl/settings.py
+++ b/sapl/settings.py
@@ -43,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next='
-SAPL_VERSION = '3.1.164-RC3'
+SAPL_VERSION = '3.1.164-RC4'
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/sapl/templates/base.html b/sapl/templates/base.html
index 5679f718a..a53524bd4 100644
--- a/sapl/templates/base.html
+++ b/sapl/templates/base.html
@@ -200,7 +200,7 @@
Desenvolvido pelo Interlegis em software livre e aberto.
- Release: 3.1.164-RC3
+ Release: 3.1.164-RC4
From 12cb253e58f987821d93348fad31c077c46ee74d Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Mon, 22 Sep 2025 17:04:24 -0300
Subject: [PATCH 3/5] Hot-fix: rate limiter get_ip
---
sapl/base/views.py | 7 ++++---
sapl/materia/views.py | 27 +++++++++++++++++++++------
sapl/norma/views.py | 24 ++++++++++++++++++------
sapl/sessao/views.py | 14 ++++++++++----
sapl/utils.py | 12 ++++++++++--
5 files changed, 63 insertions(+), 21 deletions(-)
diff --git a/sapl/base/views.py b/sapl/base/views.py
index 7c935d421..68493dc04 100644
--- a/sapl/base/views.py
+++ b/sapl/base/views.py
@@ -51,7 +51,7 @@ from sapl.sessao.models import (Bancada, SessaoPlenaria)
from sapl.settings import EMAIL_SEND_USER
from sapl.utils import (gerar_hash_arquivo, intervalos_tem_intersecao, mail_service_configured,
SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, google_recaptcha_configured,
- get_client_ip, sapn_is_enabled, is_weak_password)
+ get_client_ip, sapn_is_enabled, is_weak_password, ratelimit_ip)
from .forms import (AlterarSenhaForm, CasaLegislativaForm, ConfiguracoesAppForm, EstatisticasAcessoNormasForm)
from .models import AppConfig, CasaLegislativa
@@ -67,10 +67,11 @@ class IndexView(TemplateView):
return TemplateView.get(self, request, *args, **kwargs)
-@method_decorator(ratelimit(key=lambda group, request: get_client_ip(request),
+@method_decorator(ratelimit(key=ratelimit_ip,
rate='10/m',
method=ratelimit.UNSAFE,
- block=True), name='dispatch')
+ block=True),
+ name='dispatch')
class LoginSapl(views.LoginView):
template_name = 'base/login.html'
authentication_form = LoginForm
diff --git a/sapl/materia/views.py b/sapl/materia/views.py
index ce158228b..cfd9306c5 100644
--- a/sapl/materia/views.py
+++ b/sapl/materia/views.py
@@ -56,7 +56,7 @@ from sapl.utils import (autor_label, autor_modal, gerar_hash_arquivo, get_base_u
get_client_ip, get_mime_type_from_file_extension, lista_anexados,
mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO,
show_results_filter_set, get_tempfile_dir,
- google_recaptcha_configured, MultiFormatOutputMixin)
+ google_recaptcha_configured, MultiFormatOutputMixin, ratelimit_ip)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AnexadaEmLoteFilterSet, AdicionarVariasAutoriasFilterSet,
@@ -1461,7 +1461,10 @@ class TramitacaoCrud(MasterDetailCrud):
return initial
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class ListView(MasterDetailCrud.ListView):
def get_queryset(self):
@@ -1534,7 +1537,10 @@ class TramitacaoCrud(MasterDetailCrud):
return HttpResponseRedirect(url)
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class DetailView(MasterDetailCrud.DetailView):
template_name = "materia/tramitacao_detail.html"
@@ -1912,7 +1918,10 @@ class MateriaLegislativaCrud(Crud):
def get_success_url(self):
return self.search_url
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class DetailView(Crud.DetailView):
layout_key = 'MateriaLegislativaDetail'
@@ -1925,7 +1934,10 @@ class MateriaLegislativaCrud(Crud):
pk=self.kwargs['pk'])
return context
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class ListView(Crud.ListView, RedirectView):
def get_redirect_url(self, *args, **kwargs):
@@ -2046,7 +2058,10 @@ class AcompanhamentoExcluirView(TemplateView):
return HttpResponseRedirect(self.get_success_url())
-@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+@method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class MateriaLegislativaPesquisaView(MultiFormatOutputMixin, FilterView):
model = MateriaLegislativa
filterset_class = MateriaLegislativaFilterSet
diff --git a/sapl/norma/views.py b/sapl/norma/views.py
index 488e73c90..b82d83d39 100644
--- a/sapl/norma/views.py
+++ b/sapl/norma/views.py
@@ -30,8 +30,8 @@ from sapl.compilacao.views import IntegracaoTaView
from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux,
MasterDetailCrud, make_pagination)
from sapl.materia.models import Orgao
-from sapl.utils import show_results_filter_set, get_client_ip,\
- sapn_is_enabled, MultiFormatOutputMixin
+from sapl.utils import show_results_filter_set, get_client_ip, \
+ sapn_is_enabled, MultiFormatOutputMixin, ratelimit_ip
from .forms import (AnexoNormaJuridicaForm, NormaFilterSet, NormaJuridicaForm,
NormaPesquisaSimplesForm, NormaRelacionadaForm,
@@ -150,7 +150,10 @@ class NormaRelacionadaCrud(MasterDetailCrud):
layout_key = 'NormaRelacionadaDetail'
-@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+@method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class NormaPesquisaView(MultiFormatOutputMixin, FilterView):
model = NormaJuridica
filterset_class = NormaFilterSet
@@ -236,7 +239,10 @@ class AnexoNormaJuridicaCrud(MasterDetailCrud):
initial['ano'] = self.object.ano
return initial
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class DetailView(MasterDetailCrud.DetailView):
form_class = AnexoNormaJuridicaForm
layout_key = 'AnexoNormaJuridica'
@@ -285,7 +291,10 @@ class NormaCrud(Crud):
namespace = self.model._meta.app_config.name
return reverse('%s:%s' % (namespace, 'norma_pesquisa'))
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class DetailView(Crud.DetailView):
def get(self, request, *args, **kwargs):
estatisticas_acesso_normas = AppConfig.objects.first().estatisticas_acesso_normas
@@ -343,7 +352,10 @@ class NormaCrud(Crud):
layout_key = 'NormaJuridicaCreate'
- @method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+ @method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class ListView(Crud.ListView):
def get(self, request, *args, **kwargs):
diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py
index 7df367379..63f6a39e7 100755
--- a/sapl/sessao/views.py
+++ b/sapl/sessao/views.py
@@ -48,8 +48,8 @@ from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm, OrdemExpedien
CorrespondenciaForm, CorrespondenciaEmLoteFilterSet
from sapl.sessao.models import Correspondencia
from sapl.settings import TIME_ZONE
-from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip,\
- MultiFormatOutputMixin, PautaMultiFormatOutputMixin
+from sapl.utils import show_results_filter_set, remover_acentos, get_client_ip, \
+ MultiFormatOutputMixin, PautaMultiFormatOutputMixin, ratelimit_ip
from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm,
ExpedienteForm, JustificativaAusenciaForm, OcorrenciaSessaoForm, ListMateriaForm,
@@ -3797,7 +3797,10 @@ class SessaoListView(ListView):
return context
-@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+@method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class PautaSessaoView(TemplateView):
model = SessaoPlenaria
template_name = "sessao/pauta_inexistente.html"
@@ -3813,7 +3816,10 @@ class PautaSessaoView(TemplateView):
reverse('sapl.sessao:pauta_sessao_detail', kwargs={'pk': sessao.pk}))
-@method_decorator(ratelimit(key='ip', rate='10/m', block=True), name='dispatch')
+@method_decorator(ratelimit(key=ratelimit_ip,
+ rate='10/m',
+ block=True),
+ name='dispatch')
class PautaSessaoDetailView(PautaMultiFormatOutputMixin, DetailView):
template_name = "sessao/pauta_sessao_detail.html"
model = SessaoPlenaria
diff --git a/sapl/utils.py b/sapl/utils.py
index 6554c5aea..ee97094aa 100644
--- a/sapl/utils.py
+++ b/sapl/utils.py
@@ -402,12 +402,20 @@ def xstr(s):
def get_client_ip(request):
+ from ratelimit.core import ip_mask
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
+ ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR') or '0.0.0.0'
+ return ip_mask(ip)
+
+
+def ratelimit_ip(group, request):
+ """
+ Ignore group param in django-ratelimit==3.0.1
+ """
+ return get_client_ip(request)
def get_base_url(request):
From e6970585039d3c23fefa21402f7ec850ea22fed0 Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Mon, 22 Sep 2025 17:26:47 -0300
Subject: [PATCH 4/5] Adiciona smoke test for rate limiter
---
scripts/test_ratelimiter.sh | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100755 scripts/test_ratelimiter.sh
diff --git a/scripts/test_ratelimiter.sh b/scripts/test_ratelimiter.sh
new file mode 100755
index 000000000..e67651c5c
--- /dev/null
+++ b/scripts/test_ratelimiter.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+#URL=http://localhost:8000/materia/4379
+URL=http://localhost:8000/norma/pesquisar
+#URL=http://localhost/norma/pesquisar
+
+for i in $(seq 1 6); do
+ curl -sS -o /dev/null -w "req=$i http=%{http_code} time=%{time_total}\n" "$URL"
+done
From 3f0d61bdb6b46d8c41ce54d2f9fca4183fd5c9cc Mon Sep 17 00:00:00 2001
From: Edward Oliveira
Date: Mon, 22 Sep 2025 17:27:41 -0300
Subject: [PATCH 5/5] Release: 3.1.164-RC5
---
CHANGES.md | 7 ++++++-
docker/docker-compose.yaml | 2 +-
sapl/settings.py | 2 +-
sapl/templates/base.html | 2 +-
4 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index f977fe2b5..677aea561 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,9 +1,14 @@
+3.1.164-RC5 / 2025-09-22
+========================
+
+ * Adiciona smoke test for rate limiter
+ * Hot-fix: rate limiter get ip
+
3.1.164-RC4 / 2025-09-22
========================
* Fix recibo proposição e adiciona rate limiter em matéria e norma
- * Release: 3.1.164-RC3
3.1.164-RC3 / 2025-09-16
========================
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index cd8b7a2a1..66c788241 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -33,7 +33,7 @@ services:
networks:
- sapl-net
sapl:
- image: interlegis/sapl:3.1.164-RC4
+ image: interlegis/sapl:3.1.164-RC5
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
diff --git a/sapl/settings.py b/sapl/settings.py
index c31b6b3b9..511c7f2e8 100644
--- a/sapl/settings.py
+++ b/sapl/settings.py
@@ -43,7 +43,7 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next='
-SAPL_VERSION = '3.1.164-RC4'
+SAPL_VERSION = '3.1.164-RC5'
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
diff --git a/sapl/templates/base.html b/sapl/templates/base.html
index a53524bd4..1a077f4b1 100644
--- a/sapl/templates/base.html
+++ b/sapl/templates/base.html
@@ -200,7 +200,7 @@
Desenvolvido pelo Interlegis em software livre e aberto.
- Release: 3.1.164-RC4
+ Release: 3.1.164-RC5