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
import tornado.httpclient
import urllib.parse
+from . import accounts
from . import base
from .misc import Object
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
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):
"""
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
{{ _("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
--- /dev/null
+{% 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 %}
--- /dev/null
+{% 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 %}‐ {{ 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 %}
# 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),
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()
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)
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)