templates_blog_modulesdir = $(templates_blogdir)/modules
templates_dnsbl_DATA = \
- src/templates/dnsbl/index.html
+ src/templates/dnsbl/index.html \
+ src/templates/dnsbl/list.html
templates_dnsbldir = $(templatesdir)/dnsbl
import json
import logging
import pydantic
+import tornado.httpclient
import urllib.parse
from . import base
log = logging.getLogger(__name__)
class DNSBL(Object):
- async def __fetch(self, path, **kwargs):
+ async def _fetch(self, path, **kwargs):
url = urllib.parse.urljoin(
#"https://api.dnsbl.ipfire.org",
"http://dnsbl01.haj.ipfire.org:8000", path,
"""
Fetches all available lists
"""
- response = await self.__fetch("/lists")
+ response = await self._fetch("/lists")
return [List(self.backend, **list) for list in response]
+ async def get_list(self, slug):
+ try:
+ response = await self._fetch("/lists/%s" % slug)
+
+ # Return nothing if we received 404
+ except tornado.httpclient.HTTPClientError as e:
+ if e.code == 404:
+ return
+
+ raise e
+
+ # Return the list
+ return List(self.backend, **response)
+
class Model(pydantic.BaseModel):
"""
return self.slug < other.slug
return NotImplemented
+
+ # Sources
+
+ async def get_sources(self):
+ response = await self._backend.dnsbl._fetch("/lists/%s/sources" % self.slug)
+
+ return [Source(self._backend, **data) for data in response]
+
+
+class Source(Model):
+ """
+ Represents a source of a list
+ """
+ # Name
+ name : str
+
+ # URL
+ url : str
+
+ # Created At
+ created_at : datetime.datetime
+
+ # Created By
+ created_by : str
+
+ # Deleted At
+ deleted_at : datetime.datetime | None
+
+ # Deleted By
+ deleted_by : str | None
+
+ # License
+ license : str
+
+ # Updated At
+ updated_at : datetime.datetime | None
+
+ # Total Domains
+ total_domains : int
+
+ # Dead Domains
+ dead_domains : int
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.name == other.name
+
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return self.name < other.name
+
+ return NotImplemented
--- /dev/null
+{% extends "../base.html" %}
+
+{% block head %}
+ {% module OpenGraph(
+ title=_("IPFire DNSBL - %s") % list.name,
+ description=list.description,
+ ) %}
+{% end block %}
+
+{% block title %}
+ {{ _("IPFire DNSBL") }}{% if list.description %} - {{ list.description }}{% end %}
+{% end block %}
+
+{% block container %}
+ <section class="hero">
+ <div class="hero-body">
+ <div class="container">
+ <h1 class="title">
+ {{ list.name }}
+ </h1>
+
+ {% if list.description %}
+ <h6 class="subtitle">
+ {{ list.description }}
+ </h6>
+ {% end %}
+ </div>
+ </div>
+ </section>
+
+ {# Sources #}
+ {% if sources %}
+ <section>
+ <div class="container">
+ <h5 class="title is-5">{{ _("Sources") }}</h5>
+
+ <table class="table is-fullwidth is-striped is-hoverable is-narrow">
+ <thead>
+ <tr>
+ <th>
+ {{ _("Name") }}
+ </th>
+
+ <th class="has-text-right">
+ {{ _("Last Update") }}
+ </th>
+
+ <th class="has-text-right">
+ {{ _("Listed Domains") }}
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for source in sorted(sources) %}
+ <tr>
+ <th scope="row">
+ <a href="{{ source.url }}" target="_blank">
+ {{ source.name }}
+ </a>
+
+ <br>
+
+ <small>{{ source.license }}</small>
+ </th>
+
+ <td class="has-text-right">
+ {{ locale.format_date(source.updated_at, shorter=True) }}
+ </td>
+
+ <td class="has-text-right">
+ {{ format_number(source.total_domains) }}
+
+ <br>
+
+ {# Dead Domains #}
+ {% if source.total_domains and source.dead_domains %}
+ <small>
+ {{ _("Dead Domains: %s") % format_percent(source.dead_domains / source.total_domains) }}
+ </small>
+ {% end %}
+ </td>
+ </tr>
+ {% end %}
+ </tbody>
+ </table>
+ </div>
+ </section>
+ {% end %}
+{% end block %}
"format_language_name" : self.format_language_name,
"format_month_name" : self.format_month_name,
"format_number" : self.format_number,
+ "format_percent" : self.format_percent,
"format_phone_number" : self.format_phone_number,
"format_phone_number_to_e164" : self.format_phone_number_to_e164,
"format_phone_number_location" : self.format_phone_number_location,
# DNSBL
(r"/dnsbl/?", dnsbl.IndexHandler),
+ (r"/dnsbl/lists/(\w+)", dnsbl.ListHandler),
# Single-Sign-On for Discourse
(r"/sso/discourse", auth.SSODiscourse),
def format_number(self, handler, *args, **kwargs):
return babel.numbers.format_number(*args, locale=handler.locale.code, **kwargs)
+ def format_percent(self, handler, *args, **kwargs):
+ return babel.numbers.format_percent(*args, locale=handler.locale.code, **kwargs)
+
def format_phone_number(self, handler, number):
if not isinstance(number, phonenumbers.PhoneNumber):
try:
+import tornado.web
from . import base
from . import ui_modules
self.render("dnsbl/index.html", lists=lists)
+class ListHandler(base.AnalyticsMixin, base.BaseHandler):
+ async def get(self, slug):
+ # Fetch the list
+ list = await self.backend.dnsbl.get_list(slug)
+ if not list:
+ raise tornado.web.HTTPError(404, "Could not find list '%s'" % slug)
+
+ # Fetch the sources
+ sources = await list.get_sources()
+
+ # Render the page
+ self.render("dnsbl/list.html", list=list, sources=sources)
+
+
class ListsModule(ui_modules.UIModule):
def render(self, lists):
return self.render_string("dnsbl/modules/lists.html", lists=lists)