]> git.ipfire.org Git - ipfire.org.git/commitdiff
dnsbl: Implement submitting a report
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 30 Dec 2025 11:27:24 +0000 (11:27 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 30 Dec 2025 11:27:24 +0000 (11:27 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/backend/dnsbl.py
src/templates/base.html
src/templates/dnsbl/report-submitted.html [new file with mode: 0644]
src/templates/dnsbl/report.html [new file with mode: 0644]
src/web/__init__.py
src/web/dnsbl.py

index f13fff0919041a6c607c134ede5c127180f872ed..a6f3ff7ba112d8d6083209aa5ddbcb3764cb5b5c 100644 (file)
@@ -185,7 +185,9 @@ templates_blog_modulesdir = $(templates_blogdir)/modules
 
 templates_dnsbl_DATA = \
        src/templates/dnsbl/index.html \
-       src/templates/dnsbl/list.html
+       src/templates/dnsbl/list.html \
+       src/templates/dnsbl/report.html \
+       src/templates/dnsbl/report-submitted.html
 
 templates_dnsbldir = $(templatesdir)/dnsbl
 
index 6244106a362a07243a651a9219cdfca6d34b0906..8ce3274a7e088f6b3241c04d2fc0e60d7607bc28 100644 (file)
@@ -7,6 +7,7 @@ import pydantic
 import tornado.httpclient
 import urllib.parse
 
+from . import accounts
 from . import base
 from .misc import Object
 from .decorators import *
@@ -15,15 +16,32 @@ from .decorators import *
 log = logging.getLogger(__name__)
 
 class DNSBL(Object):
-       async def _fetch(self, path, **kwargs):
+       async def _fetch(self, path, headers=None, body=None, **kwargs):
+               if headers is None:
+                       headers = {}
+
                url = urllib.parse.urljoin(
                        #"https://api.dnsbl.ipfire.org",
                        "http://dnsbl01.haj.ipfire.org:8000", path,
                )
 
+               # Authenticate
+               headers |= {
+                       "X-API-Key" : self.backend.settings.get("dnsbl-api-key", ""),
+               }
+
+               # Serialize any content
+               if body:
+                       headers |= {
+                               "Content-Type" : "application/json",
+                       }
+
+                       # Serialize the body
+                       body = json.dumps(body)
+
                # Send the request
                response = await self.backend.http_client.fetch(
-                       url, **kwargs,
+                       url, headers=headers, body=body, **kwargs,
                )
 
                # Decode the response
@@ -115,6 +133,31 @@ class List(Model):
 
                return [Source(self._backend, **data) for data in response]
 
+       # Report!
+       async def report(self, name, reported_by, comment=None, block=True):
+               """
+                       Submits a report
+               """
+               # Only submit the UID of the user
+               if isinstance(reported_by, accounts.Account):
+                       reported_by = reported_by.uid
+
+               # Compose the request body
+               body = {
+                       "name"        : name,
+                       "reported_by" : reported_by,
+                       "comment"     : comment,
+                       "block"       : block
+               }
+
+               # Submit the report
+               response = await self._backend.dnsbl._fetch(
+                       "/lists/%s/reports" % self.slug, method="POST", body=body,
+               )
+
+               # Return the report
+               return Report(self._backend, **response)
+
 
 class Source(Model):
        """
@@ -161,3 +204,35 @@ class Source(Model):
                        return self.name < other.name
 
                return NotImplemented
+
+
+class Report(Model):
+       """
+               Represents a report
+       """
+       # ID
+       id : int
+
+       # Name
+       name : str
+
+       # Reported At
+       reported_at : datetime.datetime
+
+       # Reported By
+       reported_by : str
+
+       # Closed At
+       closed_at : datetime.datetime | None = None
+
+       # Closed By
+       closed_by : str | None = None
+
+       # Comment
+       comment : str = ""
+
+       # Block?
+       block : bool = True
+
+       # Accepted?
+       accepted : bool = False
index 0e3f687c5cbc9755b273849a70bce6efb3922156..211ae8d66cdb7e2ece7e91154d3c606293a94490 100644 (file)
                                                                                {{ _("How To Use?") }}
                                                                        </a>
 
+                                                                       <a class="navbar-item is-tab
+                                                                                       {% if request.path.startswith("/dnsbl/report") %}is-active{% end %}"
+                                                                                       href="/dnsbl/report">
+                                                                               {{ _("Report") }}
+                                                                       </a>
+
                                                                {# Location #}
                                                                {% elif request.path.startswith("/location") %}
                                                                        <a class="navbar-item is-tab
diff --git a/src/templates/dnsbl/report-submitted.html b/src/templates/dnsbl/report-submitted.html
new file mode 100644 (file)
index 0000000..63eafb4
--- /dev/null
@@ -0,0 +1,23 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Thank You!") }}{% end block %}
+
+{% block container %}
+       <section class="hero is-success is-fullheight-with-navbar">
+               <div class="hero-body">
+                       <div class="container">
+                               <h1 class="title">
+                                       {{ _("Thank You") }}
+                               </h1>
+
+                               <p class="subtitle">
+                                       {{ _("Your report has been submitted") }}
+                               </p>
+
+                               <a class="button is-white" href="/dnsbl/reports/{{ report.id }}">
+                                       {{ _("View Report") }}
+                               </a>
+                       </div>
+               </div>
+       </section>
+{% end block %}
diff --git a/src/templates/dnsbl/report.html b/src/templates/dnsbl/report.html
new file mode 100644 (file)
index 0000000..93a623c
--- /dev/null
@@ -0,0 +1,162 @@
+{% extends "../base.html" %}
+
+{% block title %}
+       {{ _("IPFire DNSBL") }} {{ _("Report A Domain") }}
+{% end block %}
+
+{% block head %}
+       {% module OpenGraph(
+               title=_("Report A Domain"),
+               description=_("Help Us To Improve IPFire DNSBL"),
+       ) %}
+{% end block %}
+
+{% block container %}
+       <section class="hero is-primary">
+               <div class="hero-body">
+                       <div class="container">
+                               <h1 class="title">
+                                       {{ _("Help Us To Improve IPFire DNSBL") }}
+                               </h1>
+
+                               <p class="subtitle">
+                                       {{ _("Report anything that we have missed") }}
+                               </p>
+                       </div>
+               </div>
+       </section>
+
+       <section class="section">
+               <div class="container">
+                       {# Show a note to users that are not logged in #}
+                       {% if not current_user %}
+                               <div class="notification">
+                                       <strong>{{ _("Please log in to submit a report") }}</strong>
+
+                                       <br>
+
+                                       To keep the IPFire DNSBL accurate and trustworthy, domain reports
+                                       can only be submitted by logged-in users.
+                                       This helps us prevent spam and abuse, and ensures that every report
+                                       comes from a real, accountable community member.
+                               </div>
+                       {% end %}
+
+                       <form action="" method="POST">
+                               {% raw xsrf_form_html() %}
+
+                               <fieldset {% if not current_user %}disabled{% end %}>
+                                       {# List #}
+                                       <div class="field is-horizontal">
+                                               <div class="field-label is-normal">
+                                                       <label class="label">{{ _("List") }}</label>
+                                               </div>
+
+                                               <div class="field-body">
+                                                       <div class="field">
+                                                               <div class="control">
+                                                                       <div class="select is-fullwidth">
+                                                                               <select name="list" required>
+                                                                                       <option value="">- {{ _("Choose One") }} -</option>
+
+                                                                                       {% for l in sorted(lists) %}
+                                                                                               <option value="{{ l.slug }}">
+                                                                                                       {{ l.name }} {% if l.description %}&dash; {{ l.description }}{% end %}
+                                                                                               </option>
+                                                                                       {% end %}
+                                                                               </select>
+                                                                       </div>
+                                                               </div>
+                                                       </div>
+                                               </div>
+                                       </div>
+
+                                       {# Name #}
+                                       <div class="field is-horizontal">
+                                               <div class="field-label is-normal">
+                                                       <label class="label">{{ _("Domain") }}</label>
+                                               </div>
+
+                                               <div class="field-body">
+                                                       <div class="field">
+                                                               <p class="control">
+                                                                       <input class="input" name="name" type="text" placeholder="{{ _("Domain") }}" required
+                                                                               pattern="^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$" />
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       </div>
+
+                                       {# Block? #}
+                                       <div class="field is-horizontal">
+                                               <div class="field-label is-normal">
+                                                       <label class="label">{{ _("What is wrong?") }}</label>
+                                               </div>
+
+                                               <div class="field-body">
+                                                       <div class="field">
+                                                               <div class="control">
+                                                                       <div class="select is-fullwidth">
+                                                                               <select name="block" required>
+                                                                                       <option value="yes">
+                                                                                               {{ _("This domain should be blocked, but isn't") }}
+                                                                                       </option>
+
+                                                                                       <option value="no">
+                                                                                               {{ _("This domain should not be blocked, but currently is") }}
+                                                                                       </option>
+                                                                               </select>
+                                                                       </div>
+                                                               </div>
+                                                       </div>
+                                               </div>
+                                       </div>
+
+                                       {# Comment #}
+                                       <div class="field is-horizontal">
+                                               <div class="field-label is-normal">
+                                                       <label class="label">{{ _("Additional Information") }}</label>
+                                               </div>
+
+                                               <div class="field-body">
+                                                       <div class="field">
+                                                               <p class="control">
+                                                                       <textarea class="textarea" name="comment" rows="4"></textarea>
+                                                               </p>
+
+                                                               <p class="help">
+                                                                       Please provide any additional context that may help us review this domain.
+                                                                       For example, where you encountered it or why you believe it should be (de-)listed.
+                                                               </p>
+
+                                                               <p class="help">
+                                                                       By submitting your report, you grant the IPFire Project the right to store,
+                                                                       process, and publish this information as part of its security services,
+                                                                       including the IPFire DNSBL under the terms of the respective list.
+                                                                       Submissions may be reviewed, modified, or removed at our discretion.
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       </div>
+
+                                       {# Submit! #}
+                                       <div class="field is-horizontal">
+                                               <div class="field-label">
+                                                       <!-- Left empty for spacing -->
+                                               </div>
+
+                                               <div class="field-body">
+                                                       <div class="field">
+                                                               <div class="control">
+                                                                       <button type="submit" class="button is-primary">
+                                                                               {{ _("Submit Report") }}
+                                                                       </button>
+                                                               </div>
+                                                       </div>
+                                               </div>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </section>
+{% end block %}
index 0a50a39d947a406d718e911fe470d40415781142..725c038b6c76a36c24ab44231fdc99e80cdeb396 100644 (file)
@@ -216,6 +216,7 @@ class Application(tornado.web.Application):
                        # DNSBL
                        (r"/dnsbl/?", dnsbl.IndexHandler),
                        (r"/dnsbl/lists/(\w+)", dnsbl.ListHandler),
+                       (r"/dnsbl/report", dnsbl.ReportHandler),
 
                        # Single-Sign-On for Discourse
                        (r"/sso/discourse", auth.SSODiscourse),
index a8a4e21d965e1d971aa4bc34ab2f5bf2a3bec8eb..12dd9eac3da1a9a83fd52affd1ab238fc1425d32 100644 (file)
@@ -4,7 +4,15 @@ import tornado.web
 from . import base
 from . import ui_modules
 
-class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
+class BaseHandler(base.BaseHandler):
+       async def get_list(self, *args, **kwargs):
+               slug = self.get_argument(*args, **kwargs)
+
+               # Fetch the list
+               return await self.backend.dnsbl.get_list(slug)
+
+
+class IndexHandler(base.AnalyticsMixin, BaseHandler):
        async def get(self):
                # Fetch all lists
                lists = await self.backend.dnsbl.get_lists()
@@ -13,7 +21,7 @@ class IndexHandler(base.AnalyticsMixin, base.BaseHandler):
                self.render("dnsbl/index.html", lists=lists)
 
 
-class ListHandler(base.AnalyticsMixin, base.BaseHandler):
+class ListHandler(base.AnalyticsMixin, BaseHandler):
        async def get(self, slug):
                # Fetch the list
                list = await self.backend.dnsbl.get_list(slug)
@@ -27,6 +35,32 @@ class ListHandler(base.AnalyticsMixin, base.BaseHandler):
                self.render("dnsbl/list.html", list=list, sources=sources)
 
 
+class ReportHandler(base.AnalyticsMixin, BaseHandler):
+       async def get(self):
+               # Fetch all lists
+               lists = await self.backend.dnsbl.get_lists()
+
+               # Render the page
+               self.render("dnsbl/report.html", lists=lists)
+
+       @tornado.web.authenticated
+       #@base.ratelimit(minutes=60, requests=10)
+       async def post(self):
+               # Fetch the list
+               list = await self.get_list("list")
+
+               # Create the report
+               report = await list.report(
+                       name        = self.get_argument("name"),
+                       reported_by = self.current_user,
+                       comment     = self.get_argument("comment", ""),
+                       block       = self.get_argument("block", "off") == "on",
+               )
+
+               # Render a result page
+               self.render("dnsbl/report-submitted.html", report=report)
+
+
 class ListsModule(ui_modules.UIModule):
        def render(self, lists):
                return self.render_string("dnsbl/modules/lists.html", lists=lists)