"theme" : config.get("theme"),
})
+@router.get(config.get("base_url") + "/infos")
+def rss(request: Request, domain: str):
+ # Tidy up domain name
+ domain = tidyup.domain(domain)
+
+ if not utils.is_domain_wanted(domain):
+ raise HTTPException(status_code=500, detail=f"domain='{domain}' is not wanted")
+
+ # Fetch domain data
+ database.cursor.execute("SELECT * FROM instances WHERE domain = ? LIMIT 1", [domain])
+ domain_data = database.cursor.fetchone()
+
+ # Format timestamps
+ format = config.get("timestamp_format")
+ instance = dict()
+ for key in domain_data.keys():
+ if key in ["last_nodeinfo", "last_blocked", "first_seen", "last_updated", "last_instance_fetch"]:
+ # Timestamps
+ instance[key] = datetime.utcfromtimestamp(domain_data[key]).strftime(format) if isinstance(domain_data[key], float) else "-"
+ else:
+ # Generic
+ instance[key] = domain_data[key]
+
+ print(domain_data.keys())
+ return templates.TemplateResponse("views/infos.html", {
+ "request" : request,
+ "domain" : domain,
+ "instance": instance,
+ "theme" : config.get("theme"),
+ "slogan" : config.get("slogan"),
+ })
+
@router.get(config.get("base_url") + "/rss")
def rss(request: Request, domain: str = None):
if domain is not None:
instances.set_has_obfuscation(blocker, True)
continue
- block["blocked"] = row[0]
- origin = row[1]
- nodeinfo_url = row[2]
+ block["blocked"] = row["domain"]
+ origin = row["origin"]
+ nodeinfo_url = row["nodeinfo_url"]
elif block["blocked"].find("?") >= 0:
logger.debug("blocker='%s' uses obfuscated domains", blocker)
instances.set_has_obfuscation(blocker, True)
continue
- block["blocked"] = row[0]
- origin = row[1]
- nodeinfo_url = row[2]
+ block["blocked"] = row["domain"]
+ origin = row["origin"]
+ nodeinfo_url = row["nodeinfo_url"]
logger.debug("Looking up instance by domainm, blocked='%s'", block["blocked"])
if not utils.is_domain_wanted(block["blocked"]):
rows = database.cursor.fetchall()
logger.info("Checking %d entries ...", len(rows))
for row in rows:
- logger.debug("domain='%s'", row[0])
- if not utils.is_domain_wanted(row[0]):
- logger.debug("Domain row[0]='%s' is not wanted - SKIPPED!", row[0])
+ logger.debug("domain='%s'", row["domain"])
+ if not utils.is_domain_wanted(row["domain"]):
+ logger.debug("Domain row[domain]='%s' is not wanted - SKIPPED!", row["domain"])
continue
try:
- logger.info("Fetching instances for domain='%s',origin='%s',software='%s',nodeinfo_url='%s'", row[0], row[1], row[2], row[3])
- federation.fetch_instances(row[0], row[1], row[2], inspect.currentframe().f_code.co_name, row[3])
+ logger.info("Fetching instances for domain='%s',origin='%s',software='%s',nodeinfo_url='%s'", row["domain"], row["origin"], row["software"], row["nodeinfo_url"])
+ federation.fetch_instances(row["domain"], row["origin"], row["software"], inspect.currentframe().f_code.co_name, row["nodeinfo_url"])
except network.exceptions as exception:
- logger.warning("Exception '%s' during fetching instances (fetch_instances) from row[0]='%s'", type(exception), row[0])
- instances.set_last_error(row[0], exception)
+ logger.warning("Exception '%s' during fetching instances (fetch_instances) from row[domain]='%s'", type(exception), row["domain"])
+ instances.set_last_error(row["domain"], exception)
logger.debug("Success - EXIT!")
return 0
locking.acquire()
- if args.domain != "" and utils.is_domain_wanted(args.domain):
+ if isinstance(args.domain, str) and args.domain != "" and utils.is_domain_wanted(args.domain):
database.cursor.execute("SELECT domain, software, nodeinfo_url FROM instances WHERE has_obfuscation = 1 AND domain = ?", [args.domain])
- elif args.domain != "" and validators.domain(args.software) == args.software:
+ elif isinstance(args.software, str) and args.software != "" and validators.domain(args.software) == args.software:
database.cursor.execute("SELECT domain, software, nodeinfo_url FROM instances WHERE has_obfuscation = 1 AND software = ?", [args.software])
else:
database.cursor.execute("SELECT domain, software, nodeinfo_url FROM instances WHERE has_obfuscation = 1")
rows = database.cursor.fetchall()
logger.info("Checking %d domains ...", len(rows))
for row in rows:
- logger.debug("Fetching peers from domain='%s',software='%s',nodeinfo_url='%s' ...", row[0], row[1], row[2])
+ logger.debug("Fetching peers from domain='%s',software='%s',nodeinfo_url='%s' ...", row["domain"], row["software"], row["nodeinfo_url"])
blocking = list()
- if row[1] == "pleroma":
- logger.debug("domain='%s',software='%s'", row[0], row[1])
- blocking = pleroma.fetch_blocks(row[0], row[2])
- elif row[1] == "mastodon":
- logger.debug("domain='%s',software='%s'", row[0], row[1])
- blocking = mastodon.fetch_blocks(row[0], row[2])
- elif row[1] == "lemmy":
- logger.debug("domain='%s',software='%s'", row[0], row[1])
- blocking = lemmy.fetch_blocks(row[0], row[2])
- elif row[1] == "friendica":
- logger.debug("domain='%s',software='%s'", row[0], row[1])
- blocking = friendica.fetch_blocks(row[0])
- elif row[1] == "misskey":
- logger.debug("domain='%s',software='%s'", row[0], row[1])
- blocking = misskey.fetch_blocks(row[0])
+ if row["software"] == "pleroma":
+ logger.debug("domain='%s',software='%s'", row["domain"], row["software"])
+ blocking = pleroma.fetch_blocks(row["domain"], row["nodeinfo_url"])
+ elif row["software"] == "mastodon":
+ logger.debug("domain='%s',software='%s'", row["domain"], row["software"])
+ blocking = mastodon.fetch_blocks(row["domain"], row["nodeinfo_url"])
+ elif row["software"] == "lemmy":
+ logger.debug("domain='%s',software='%s'", row["domain"], row["software"])
+ blocking = lemmy.fetch_blocks(row["domain"], row["nodeinfo_url"])
+ elif row["software"] == "friendica":
+ logger.debug("domain='%s',software='%s'", row["domain"], row["software"])
+ blocking = friendica.fetch_blocks(row["domain"])
+ elif row["software"] == "misskey":
+ logger.debug("domain='%s',software='%s'", row["domain"], row["software"])
+ blocking = misskey.fetch_blocks(row["domain"])
else:
- logger.warning("Unknown sofware: domain='%s',software='%s'", row[0], row[1])
+ logger.warning("Unknown sofware: domain='%s',software='%s'", row["domain"], row["software"])
- logger.info("Checking %d block(s) from domain='%s' ...", len(blocking), row[0])
+ logger.info("Checking %d block(s) from domain='%s' ...", len(blocking), row["domain"])
obfuscated = 0
blockdict = list()
for block in blocking:
elif block["blocked"].find("*") >= 0 or block["blocked"].find("?") >= 0:
logger.debug("block='%s' is obfuscated.", block["blocked"])
obfuscated = obfuscated + 1
- blocked = utils.deobfuscate_domain(block["blocked"], row[0], block["hash"] if "hash" in block else None)
+ blocked = utils.deobfuscate_domain(block["blocked"], row["domain"], block["hash"] if "hash" in block else None)
elif not utils.is_domain_wanted(block["blocked"]):
logger.debug("blocked='%s' is not wanted - SKIPPED!", block["blocked"])
continue
- elif blocks.is_instance_blocked(row[0], block["blocked"]):
+ elif blocks.is_instance_blocked(row["domain"], block["blocked"]):
logger.debug("blocked='%s' is already blocked - SKIPPED!", block["blocked"])
continue
if blocked is not None and blocked != block["blocked"]:
logger.debug("blocked='%s' was deobfuscated to blocked='%s'", block["blocked"], blocked)
obfuscated = obfuscated - 1
- if blocks.is_instance_blocked(row[0], blocked):
- logger.debug("blocked='%s' is already blocked by domain='%s' - SKIPPED!", blocked, row[0])
+ if blocks.is_instance_blocked(row["domain"], blocked):
+ logger.debug("blocked='%s' is already blocked by domain='%s' - SKIPPED!", blocked, row["domain"])
continue
block["block_level"] = utils.alias_block_level(block["block_level"])
logger.info("blocked='%s' has been deobfuscated to blocked='%s', adding ...", block["blocked"], blocked)
- if utils.process_block(row[0], blocked, block["reason"], block["block_level"]) and block["block_level"] == "reject" and config.get("bot_enabled"):
- logger.debug("Appending blocked='%s',reason='%s' for blocker='%s' ...", block["blocked"], block["block_level"], row[0])
+ if utils.process_block(row["domain"], blocked, block["reason"], block["block_level"]) and block["block_level"] == "reject" and config.get("bot_enabled"):
+ logger.debug("Appending blocked='%s',reason='%s' for blocker='%s' ...", block["blocked"], block["block_level"], row["domain"])
blockdict.append({
"blocked": blocked,
"reason" : block["reason"],
})
- logger.info("domain='%s' has %d obfuscated domain(s)", row[0], obfuscated)
+ logger.info("domain='%s' has %d obfuscated domain(s)", row["domain"], obfuscated)
if obfuscated == 0 and len(blocking) > 0:
- logger.info("Block list from domain='%s' has been fully deobfuscated.", row[0])
- instances.set_has_obfuscation(row[0], False)
+ logger.info("Block list from domain='%s' has been fully deobfuscated.", row["domain"])
+ instances.set_has_obfuscation(row["domain"], False)
- if instances.has_pending(row[0]):
- logger.debug("Flushing updates for blocker='%s' ...", row[0])
- instances.update_data(row[0])
+ if instances.has_pending(row["domain"]):
+ logger.debug("Flushing updates for blocker='%s' ...", row["domain"])
+ instances.update_data(row["domain"])
logger.debug("Invoking commit() ...")
database.connection.commit()
if config.get("bot_enabled") and len(blockdict) > 0:
- logger.info("Sending bot POST for blocker='%s,blockdict()=%d ...", row[0], len(blockdict))
- network.send_bot_post(row[0], blockdict)
+ logger.info("Sending bot POST for blocker='%s,blockdict()=%d ...", row["domain"], len(blockdict))
+ network.send_bot_post(row["domain"], blockdict)
logger.debug("Success! - EXIT!")
return 0
# Connect to database
connection = sqlite3.connect("blocks.db")
+
+# Init row factory
+connection.row_factory = sqlite3.Row
+
+# Get cursor
cursor = connection.cursor()
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
+import sqlite3
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
for sub in rows:
logger.debug("Setting key='%s',sub[%s]='%s'", key, type(sub), sub)
- if isinstance(sub, tuple):
+ if isinstance(sub, sqlite3.Row):
logger.debug("Setting key='%s',sub[%s]='%s',value[]='%s'", key, type(sub), sub, type(value))
_cache[key][sub[0]] = value
else:
cookies=cookies.get_all(domain) if cookies.has(domain) else dict()
)
+ logger.info("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
json_reply["json"] = json_from_response(response)
logger.debug("response.ok='%s',response.status_code=%d,json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
logger.warning("Cannot query JSON API: domain='%s',path='%s',data()=%d,response.status_code=%d,json_reply[]='%s'", domain, path, len(data), response.status_code, type(json_reply))
json_reply["status_code"] = response.status_code
json_reply["error_message"] = response.reason
- del json_reply["json"]
instances.set_last_error(domain, response)
+ del json_reply["json"]
except exceptions as exception:
logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
logger.debug("Fetching url='%s' ...", url)
response = utils.fetch_url(url, api_headers, timeout)
+ logger.info("Parsing JSON response from url='%s' ...", url)
json_reply["json"] = json_from_response(response)
logger.debug("response.ok='%s',response.status_code='%s',json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
instances.set_last_error(domain, exception)
raise exception
+ logger.info("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
json_reply["json"] = json_from_response(response)
logger.debug("response.ok='%s',response.status_code=%d,json_reply[]='%s'", response.ok, response.status_code, type(json_reply))
try:
data = response.json()
except json.decoder.JSONDecodeError as exception:
- logger.warning("Exception '%s' during decoding JSON", type(exception))
+ logger.warning("Exception '%s' during decoding JSON from response.url='%s'", type(exception), response.url)
logger.debug("data[]='%s' - EXIT!", type(data))
return data
logger.debug("row[]='%s'", type(row))
if row is not None:
- logger.debug("domain='%s' de-obscured to '%s'", domain, row[0])
- domain = row[0]
+ logger.debug("domain='%s' de-obscured to '%s'", domain, row["domain"])
+ domain = row["domain"]
else:
logger.debug("blocker='%s' has domain that cannot be deobfuscated.", blocker)
instances.set_has_obfuscation(blocker, True)
logger.debug("row[]='%s'", type(row))
if row is not None:
- logger.debug("domain='%s' de-obscured to '%s'", domain, row[0])
- domain = row[0]
+ logger.debug("domain='%s' de-obscured to '%s'", domain, row["domain"])
+ domain = row["domain"]
else:
logger.debug("blocker='%s' has domain that cannot be deobfuscated.", blocker)
instances.set_has_obfuscation(blocker, True)
padding-bottom: 4px;
}
-table.with-rows > tbody > tr:nth-of-type(2n),
-table.with-rows > thead {
+.index-table {
+ width: 700px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.table-with-rows > tbody > tr:nth-of-type(2n),
+.table-with-rows > thead,
+.index-table > thead {
background-color: #eaeaea;
}
+.index-table h3 {
+ margin: 0px;
+}
+
+.index-table form {
+ padding: 2px;
+}
+
table td {
padding: 4px;
text-align: left;
<title>fedi-block-api - {% block title %}{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <base href="{{base_url}}" />
- <link rel="alternate" type="application/rss+xml" title="RSS Feed for latest blocked instances" href="{{base_url}}/rss" />
+ <link rel="alternate" type="application/rss+xml" title="RSS Feed for latest blocked instances" href="rss" />
{% block rss %}{% endblock %}
- <link rel="stylesheet" type="text/css" href="{{ url_for('static', path='css/' + theme + '.css') }}" media="all" />
+ <link rel="stylesheet" type="text/css" href="{{ url_for('static', path='css/' + theme + '.css') }}?v=0.0.1" media="all" />
</head>
<body>
{% block header %}<h1>Welcome to FBA</h1>{% endblock %}
{% block content %}
- <h2>Enter a Domain</h2>
- <form action="top">
- <input type="hidden" name="mode" value="domain" />
- <input type="text" name="value" placeholder="example.com" required="required" />
- <input type="submit" value="Submit" />
- </form>
+ <table class="index-table">
+ <thead>
+ <th colspan="2">
+ <h3>Choose an option</h3>
+ </th>
+ </thead>
- <h2>Enter a Reason</h2>
- <form action="top">
- <input type="hidden" name="mode" value="reason" />
- <input type="text" name="value" placeholder="free speech" required="required" />
- <input type="submit" value="Submit" />
- </form>
-
- <h2>Reverse search</h2>
- <form action="top">
- <input type="hidden" name="mode" value="reverse" />
- <input type="text" name="value" placeholder="example.com" required="required" />
- <input type="submit" value="Submit" />
- </form>
+ <tbody>
+ <tr>
+ <td>
+ <h3>Enter a domain:</h3>
+ <form action="top">
+ <input type="hidden" name="mode" value="domain" />
+ <input type="text" name="value" placeholder="example.com" required="required" />
+ <input type="submit" value="Submit" />
+ </form>
+ </td>
+ <td>
+ <h3>Enter a reason:</h3>
+ <form action="top">
+ <input type="hidden" name="mode" value="reason" />
+ <input type="text" name="value" placeholder="free speech" required="required" />
+ <input type="submit" value="Submit" />
+ </form>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <h3>Reverse search:</h3>
+ <form action="top">
+ <input type="hidden" name="mode" value="reverse" />
+ <input type="text" name="value" placeholder="example.com" required="required" />
+ <input type="submit" value="Submit" />
+ </form>
+ </td>
+ <td>
+ <h3>Info of a domain:</h3>
+ <form action="infos">
+ <input type="text" name="domain" placeholder="example.com" required="required" />
+ <input type="submit" value="Submit" />
+ </form>
+ </td>
+ </tr>
+ </tbody>
+ </table>
<h2>Scoreboards:</h2>
<ul class="nav">
- <li><a href="{{base_url}}/scoreboard?mode=blocker&amount=50">Defederating instances</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=blocked&amount=50">Defederated instances</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=reference&amount=50">Referencing instances</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=software&amount=50">Used software</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=command&amount=10">Commands</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=error_code&amount=30">Error codes</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=detection_mode&amount=10">Detection modes</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=avg_peers&amount=30">Average peers</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=obfuscator&amount=30">Obfuscating software</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=obfuscation&amount=10">Obfuscation statistics</a></li>
- <li><a href="{{base_url}}/scoreboard?mode=block_level&amount=20">Block level statistics</a></li>
+ <li><a href="scoreboard?mode=blocker&amount=50">Defederating instances</a></li>
+ <li><a href="scoreboard?mode=blocked&amount=50">Defederated instances</a></li>
+ <li><a href="scoreboard?mode=reference&amount=50">Referencing instances</a></li>
+ <li><a href="scoreboard?mode=software&amount=50">Used software</a></li>
+ <li><a href="scoreboard?mode=command&amount=10">Commands</a></li>
+ <li><a href="scoreboard?mode=error_code&amount=30">Error codes</a></li>
+ <li><a href="scoreboard?mode=detection_mode&amount=10">Detection modes</a></li>
+ <li><a href="scoreboard?mode=avg_peers&amount=30">Average peers</a></li>
+ <li><a href="scoreboard?mode=obfuscator&amount=30">Obfuscating software</a></li>
+ <li><a href="scoreboard?mode=obfuscation&amount=10">Obfuscation statistics</a></li>
+ <li><a href="scoreboard?mode=block_level&amount=20">Block level statistics</a></li>
</ul>
{% endblock %}
{% block footer %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}Infos on domain {{domain}}{% endblock %}
+
+{% block header %}<h1>Infos on {{domain}}</h1>{% endblock %}
+
+{% block content %}
+<div class="infos">
+ <table class="table-with-rows">
+ <thead>
+ <th colspan="2">
+ <h3>Instance information</h3>
+ </th>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td>Domain name:</td>
+ <td>{% with domain=instance['domain'] %}{% include "widgets/links.html" %}{% endwith %}</td>
+ </tr>
+
+ <tr>
+ <td>Domain hash:</td>
+ <td>{{instance['hash']}}</td>
+ </tr>
+
+ <tr>
+ <td>Software:</td>
+ <td>{{instance['software']}}</td>
+ </tr>
+
+ <tr>
+ <td>Originating instance:</td>
+ <td>{% if instance['origin'] != None %}{% with domain=instance['origin'] %}{% include "widgets/links.html" %}{% endwith %}{% else %}-{% endif %}</td>
+ </tr>
+
+ <tr>
+ <td>Command:</td>
+ <td><code>{{instance['command']}}</code></td>
+ </tr>
+
+ <tr>
+ <td>Detection mode:</td>
+ <td><em>{{instance['detection_mode']}}</em></td>
+ </tr>
+
+ <tr>
+ <td>NodeInfo URL:</td>
+ <td>{{instance['nodeinfo_url']}}</td>
+ </tr>
+
+ <tr>
+ <td>Total peers:</td>
+ <td>{{instance['total_peers']}}</td>
+ </tr>
+
+ <tr>
+ <td>Has obfuscated block list:</td>
+ <td>{% if instance['has_obfuscation']%}Yes{%elif not instance['has_obfuscation']%}No{%else%}-{%endif%}</td>
+ </tr>
+
+ <tr>
+ <td>First seen:</td>
+ <td>{{instance['first_seen']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last updated:</td>
+ <td>{{instance['last_updated']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last nodeinfo fetched:</td>
+ <td>{{instance['last_nodeinfo']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last blocks fetched:</td>
+ <td>{{instance['last_blocked']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last instances fetched:</td>
+ <td>{{instance['last_instance_fetch']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last status code:</td>
+ <td>{{instance['last_status_code']}}</td>
+ </tr>
+
+ <tr>
+ <td>Last error details:</td>
+ <td>{{instance['last_error_details']}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+{% endblock %}
+
+{% block footer %}
+ <a href="{{base_url}}/">Index</a> /
+ {{ super() }}
+{% endblock %}
{% block content %}
<div class="scoreboard">
- <table class="with-rows">
+ <table class="table-with-rows">
<thead>
<th>№</th>
<th>{% if mode in ('software', 'avg_peers', 'obfuscator') %}Software{% elif mode == 'obfuscation' %}Obfuscation status{% elif mode == 'detection_mode' %}Detection mode{% elif mode == 'error_code' %}Error code{% else %}Instance{% endif %}</th>
{% elif entry['domain'] == None %}
-
{% elif mode == 'block_level' %}
- <a href="{{base_url}}/top?mode=block_level&value={{entry['domain']}}&amount=50">{{entry['domain']}}</a>
+ <a href="top?mode=block_level&value={{entry['domain']}}&amount=50">{{entry['domain']}}</a>
{% else %}
{% with domain=entry['domain'] %}
{% include "widgets/links.html" %}
{% block rss %}
{{ super() }}
{% if mode == 'domain' %}
- <link rel="alternate" type="application/rss+xml" title="RSS Feed for blocked domain {{value}}" href="{{base_url}}/rss?domain={{value}}" />
+ <link rel="alternate" type="application/rss+xml" title="RSS Feed for blocked domain {{value}}" href="rss?domain={{value}}" />
{% elif mode == 'reverse' %}
- <link rel="alternate" type="application/rss+xml" title="RSS Feed for blocking domain {{value}}" href="{{base_url}}/rss?reverse={{value}}" />
+ <link rel="alternate" type="application/rss+xml" title="RSS Feed for blocking domain {{value}}" href="rss?reverse={{value}}" />
{% endif %}
{% endblock %}
{% for block_level in blocklist %}
<div class="block_level">
<h2>{{block_level}} ({{blocklist[block_level]|length}})</h2>
- <table class="with-rows">
+ <table class="table-with-rows">
<thead>
<th>Blocker</th>
<th>{% if block_level == 'accept' %}Accepted{% else %}Blocked{% endif %}</th>
-[<a class="listlink" href="{{base_url}}/top?mode=domain&value={{domain}}" title="Search {{domain}}">D</a>]
-[<a class="listlink" href="{{base_url}}/top?mode=reverse&value={{domain}}" title="Reverse search {{domain}}">R</a>]
+[<a class="listlink" href="top?mode=domain&value={{domain}}" title="Search {{domain}}">D</a>]
+[<a class="listlink" href="top?mode=reverse&value={{domain}}" title="Reverse search {{domain}}">R</a>]
+[<a class="listlink" href="infos?domain={{domain}}" title="Information on domain {{domain}}">I</a>]
<a href="https://{{domain}}" rel="nofollow noopener noreferrer">{{domain}}</a>