From 28ccd7d73ba5708ee34f2dc80bce0d20341f7010 Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Mon, 20 Jun 2022 15:26:35 -0700 Subject: [PATCH] Add IP bans Users can be banned by IP address now, either by singular IP or in an IP range. If they are banned and attempt to post, they will be met with a "you are banned until X date" screen. There are a few loose threads with this, and IP bans may be obsolete if I decide to go the accounts-required-for-posting route. But I think this is a good start for 4chan style posting. Signed-off-by: Alek Ratzloff --- board/admin.py | 12 ++++- board/models.py | 54 ++++++++++++++------- board/templates/board/banned.html | 81 +++++++++++++++++++++++++++++++ board/templates/board/base.html | 2 + board/urls.py | 5 ++ board/utils.py | 26 ++++++++++ board/views.py | 40 ++++++++++----- 7 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 board/templates/board/banned.html create mode 100644 board/utils.py diff --git a/board/admin.py b/board/admin.py index 691ee72..9207029 100644 --- a/board/admin.py +++ b/board/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.utils.safestring import mark_safe -from board.models import Board, Post, ReportReason, ReportRecord +from board.models import Ban, Board, Post, RangeBan, ReportReason, ReportRecord @admin.register(Board) @@ -45,3 +45,13 @@ class ReportRecordAdmin(admin.ModelAdmin): html += f"

{obj.post.text}

" html += "" return mark_safe(html) + + +@admin.register(RangeBan) +class RangeBanAdmin(admin.ModelAdmin): + pass + + +@admin.register(Ban) +class BanAdmin(admin.ModelAdmin): + pass diff --git a/board/models.py b/board/models.py index 1666895..5b09283 100644 --- a/board/models.py +++ b/board/models.py @@ -264,32 +264,50 @@ def report_created(sender, instance, created, **kwargs): # board = -class RangeBan(models.Model): - # Starting IP address of the ban - start = models.GenericIPAddressField() - # Ending IP address of the ban - end = models.GenericIPAddressField() - # The reason for this ban - ban_reason = models.TextField(blank=False) - # The time that this ban was created. - created = models.DateTimeField(auto_now_add=True) - # Expiration date of this ban. If it is null, it is permanent. - expires = models.DateTimeField(null=True) - - -class Ban(models.Model): - # IP address of the ban - ip = models.GenericIPAddressField() +class BanCommon(models.Model): # The reason for this ban ban_reason = models.TextField(blank=False) # Board that this ban is for. If null, then all boards. # Even though this is nullable, we just want to delete all reports for a # board if that board is deleted. - board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True) + board = models.ForeignKey("Board", on_delete=models.CASCADE, null=True, blank=True) # The time that this ban was created. created = models.DateTimeField(auto_now_add=True) # Expiration date of this ban. If it is null, it is permanent. - expires = models.DateTimeField(null=True) + expires = models.DateTimeField(null=True, blank=True) + + class Meta: + abstract = True + + +class RangeBan(BanCommon): + # Starting IP address of the ban + start = models.GenericIPAddressField() + # Ending IP address of the ban + end = models.GenericIPAddressField() + + def clean(self): + import ipaddress + + start = ipaddress.ip_address(self.start) + end = ipaddress.ip_address(self.end) + if start.version != end.version: + raise ValidationError( + _("Start and end IP address must be the same protocol (IPv4 or IPv6)") + ) + if start >= end: + raise ValidationError(_("Start IP must be lower than end IP")) + + def __str__(self): + return f"Range ban for {self.start}-{self.end}" + + +class Ban(BanCommon): + # IP address of the ban + ip = models.GenericIPAddressField(unique=True) + + def __str__(self): + return f"Ban for {self.ip}" class BanTemplate(models.Model): diff --git a/board/templates/board/banned.html b/board/templates/board/banned.html new file mode 100644 index 0000000..c8dd723 --- /dev/null +++ b/board/templates/board/banned.html @@ -0,0 +1,81 @@ +{% extends "board/base.html" %} + +{% block title %} + {% if bans %} + {% with title="Banned" %} + {{ block.super }} + {% endwith %} + {% else %} + {% with title="Not banned" %} + {{ block.super }} + {% endwith %} + {% endif %} +{% endblock title %} + +{% block extrastyle %} +{{block.super}} + +{% endblock extrastyle %} + +{% block content %} +
+
 
+
+ {% if bans %} +
+

You are banned.

+
+
+

+ You have been banned, and you are not allowed to post until your + bans have expired. +

+

+ Your IP address: {{ip}} +

+
+
+ {% for ban in bans %} +
+

+ Board: + {% if ban.board %} + /{{ ban.board.url }}/ - {{ ban.board.name }} + {% else %} + All boards + {% endif %} +

+

+ Ban reason: +

+

{{ban.ban_reason}}

+

+ {% if ban.expires %} + This ban will expire on {{ban.expires|date:"M dS"}} at {{ban.expires|date:"P e"}} + {% else %} + This ban will not expire. + {% endif %} +

+
+
+ {% endfor %} + {% else %} +
+

You are not banned.

+
+
+

You have no bans recorded for your IP address.

+
+ {% endif %} +
+
 
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/base.html b/board/templates/board/base.html index c16db4d..74064ce 100644 --- a/board/templates/board/base.html +++ b/board/templates/board/base.html @@ -7,9 +7,11 @@ {% block title %}{% if title %}{{title}}{% else %}Index{% endif %}{% endblock %} + {% block extrastyle %}{% endblock %} + {% block extrajs %}{% endblock %}
diff --git a/board/urls.py b/board/urls.py index 5085fd4..c584be4 100644 --- a/board/urls.py +++ b/board/urls.py @@ -16,6 +16,11 @@ urlpatterns = [ TemplateView.as_view(template_name="board/report_success.html"), name="report_success", ), + path( + "banned", + BannedView.as_view(), + name="banned", + ), ] # TODO - make this conditional so we can serve images up with whatever server we want urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/board/utils.py b/board/utils.py new file mode 100644 index 0000000..44cd221 --- /dev/null +++ b/board/utils.py @@ -0,0 +1,26 @@ +from board.models import Ban, RangeBan +import ipaddress + + +def get_client_ip(request): + "Get the IP address of a client-side request. Shamelessly copy/pasted from StackOverflow." + 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 get_ip_bans(ip: str) -> list: + bans = list(Ban.objects.filter(ip=ip)) + + ip_addr = ipaddress.ip_address(ip) + for rangeban in RangeBan.objects.all(): + start = ipaddress.ip_address(rangeban.start) + end = ipaddress.ip_address(rangeban.end) + if ip_addr.version != start.version or ip_addr.version != end.version: + continue + if start <= ip_addr <= end: # type: ignore + bans += [rangeban] + return bans diff --git a/board/views.py b/board/views.py index a42b856..3ecaab7 100644 --- a/board/views.py +++ b/board/views.py @@ -1,23 +1,27 @@ from django.conf import settings from django.http import Http404, HttpResponseRedirect -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import get_object_or_404 from django.views.generic import DetailView +from django.views.generic.base import TemplateView from django.views.generic.edit import CreateView from django.urls import reverse, reverse_lazy -from board.models import Post, Board, Report, ReportRecord from board.forms import PostForm, ReplyForm, ReportForm +from board.models import Ban, Post, Board, Report +from board.utils import get_client_ip, get_ip_bans -__all__ = ("BoardView", "PostView", "ReportView") +__all__ = ("BannedView", "BoardView", "PostView", "ReportView") -def get_client_ip(request): - "Get the IP address of a client-side request. Shamelessly copy/pasted from StackOverflow." - 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 +class BannedView(TemplateView): + template_name = "board/banned.html" + + def get_context_data(self, **kwargs): + context = super(TemplateView, self).get_context_data(**kwargs) + ip = get_client_ip(self.request) + bans = get_ip_bans(ip) + context["bans"] = bans + context["ip"] = ip + return context class CreatePostView(CreateView): @@ -46,6 +50,20 @@ class CreatePostView(CreateView): kwargs["ip"] = get_client_ip(self.request) return kwargs + def dispatch(self, request, *args, **kwargs): + self._set_board(kwargs["url"]) + if request.method == "POST": + ip = get_client_ip(request) + bans = [ + ban + for ban in get_ip_bans(ip) + if ban.board == self.board or not ban.board + ] + if bans: + return HttpResponseRedirect(reverse("board:banned")) + + return super(CreatePostView, self).dispatch(request, *args, **kwargs) + class BoardView(CreatePostView): model = Post