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