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 <alekratz@gmail.com>
This commit is contained in:
2022-06-20 15:26:35 -07:00
parent b4df8b9756
commit 28ccd7d73b
7 changed files with 190 additions and 30 deletions

View File

@@ -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"<p>{obj.post.text}</p>"
html += "</div>"
return mark_safe(html)
@admin.register(RangeBan)
class RangeBanAdmin(admin.ModelAdmin):
pass
@admin.register(Ban)
class BanAdmin(admin.ModelAdmin):
pass

View File

@@ -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):

View File

@@ -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}}
<style>
.blankcolumn {
float: left;
width: 20%;
}
.bancolumn {
float: left;
width: 60%;
}
</style>
{% endblock extrastyle %}
{% block content %}
<div class="row">
<div class="blankcolumn">&nbsp;</div>
<div class="bancolumn">
{% if bans %}
<div class="row">
<h2>You are banned.</h2>
</div>
<div class="row">
<p>
You have been banned, and you are not allowed to post until your
bans have expired.
</p>
<p>
<strong>Your IP address:</strong> {{ip}}
</p>
</div>
<hr />
{% for ban in bans %}
<div class="row">
<p>
<strong>Board:</strong>
{% if ban.board %}
/{{ ban.board.url }}/ - {{ ban.board.name }}
{% else %}
All boards
{% endif %}
</p>
<p>
<strong>Ban reason:</strong>
</p>
<p>{{ban.ban_reason}}</p>
<p><strong>
{% 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 %}
</strong></p>
</div>
<hr />
{% endfor %}
{% else %}
<div class="row">
<h2>You are not banned.</h2>
</div>
<div class="row">
<p>You have no bans recorded for your IP address.</p>
</div>
{% endif %}
</div>
<div class="blankcolumn">&nbsp;</div>
{% endblock %}

View File

@@ -7,9 +7,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block title %}{% if title %}{{title}}{% else %}Index{% endif %}{% endblock %}</title>
<link rel="stylesheet" href="{% static 'board/style.css' %}">
{% block extrastyle %}{% endblock %}
<script src="{% static 'board/jquery.js' %}"></script>
<script src="{% static 'board/winbox.bundle.js' %}"></script>
<script src="{% static 'board/post.js' %}"></script>
{% block extrajs %}{% endblock %}
</head>
<body>
<main>

View File

@@ -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)

26
board/utils.py Normal file
View File

@@ -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

View File

@@ -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