]> git.mxchange.org Git - fba.git/blob - fba/http/federation.py
c17e52d749033d74023cbe95a258c90c93770a6c
[fba.git] / fba / http / federation.py
1 # Copyright (C) 2023 Free Software Foundation
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License as published
5 # by the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU Affero General Public License for more details.
12 #
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16 import logging
17
18 from urllib.parse import urlparse
19
20 import bs4
21 import requests
22 import validators
23
24 from fba.helpers import config
25 from fba.helpers import cookies
26 from fba.helpers import domain as domain_helper
27 from fba.helpers import software as software_helper
28 from fba.helpers import tidyup
29 from fba.helpers import version
30
31 from fba.http import csrf
32 from fba.http import network
33 from fba.http import nodeinfo
34
35 from fba.models import blocks
36 from fba.models import instances
37
38 from fba.networks import lemmy
39 from fba.networks import misskey
40 from fba.networks import peertube
41
42 # Depth counter, being raised and lowered
43 _DEPTH = 0
44
45 logging.basicConfig(level=logging.INFO)
46 logger = logging.getLogger(__name__)
47
48 def fetch_instances(domain: str, origin: str, software: str, command: str, path: str = None):
49     global _DEPTH
50     logger.debug("domain='%s',origin='%s',software='%s',command='%s',path='%s',_DEPTH=%d - CALLED!", domain, origin, software, command, path, _DEPTH)
51     domain_helper.raise_on(domain)
52
53     if not isinstance(origin, str) and origin is not None:
54         raise ValueError(f"Parameter origin[]='{type(origin)}' is not of type 'str'")
55     elif not isinstance(command, str):
56         raise ValueError(f"Parameter command[]='{type(command)}' is not of type 'str'")
57     elif command == "":
58         raise ValueError("Parameter 'command' is empty")
59     elif command in ["fetch_blocks", "fetch_cs", "fetch_bkali", "fetch_relays", "fetch_fedipact", "fetch_joinmobilizon", "fetch_joinmisskey", "fetch_joinfediverse", "fetch_relaylist"] and origin is None:
60         raise ValueError(f"Parameter command='{command}' but origin is None, please fix invoking this function.")
61     elif not isinstance(path, str) and path is not None:
62         raise ValueError(f"Parameter path[]='{type(path)}' is not of type 'str'")
63     elif _DEPTH > 0 and instances.is_recent(domain, "last_instance_fetch"):
64         raise ValueError(f"domain='{domain}' has recently been fetched but function was invoked")
65     elif software is None and not instances.is_recent(domain, "last_nodeinfo"):
66         try:
67             logger.debug("Software for domain='%s' is not set, determining ...", domain)
68             software = determine_software(domain, path)
69         except network.exceptions as exception:
70             logger.warning("Exception '%s' during determining software type", type(exception))
71             instances.set_last_error(domain, exception)
72
73         logger.debug("Determined software='%s' for domain='%s'", software, domain)
74     elif software is None:
75         logger.debug("domain='%s' has unknown software or nodeinfo has recently being fetched", domain)
76     elif not isinstance(software, str):
77         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
78
79     # Increase depth
80     _DEPTH = _DEPTH + 1
81
82     logger.debug("Checking if domain='%s' is registered ...", domain)
83     if not instances.is_registered(domain):
84         logger.debug("Adding new domain='%s',origin='%s',command='%s',path='%s',software='%s'", domain, origin, command, path, software)
85         instances.add(domain, origin, command, path, software)
86
87         logger.debug("software='%s'", software)
88         if software_helper.is_relay(software):
89             logger.debug("software='%s' is a relay software - EXIT!", software)
90             _DEPTH = _DEPTH - 1
91             return
92
93     logger.debug("Updating last_instance_fetch for domain='%s' ...", domain)
94     instances.set_last_instance_fetch(domain)
95
96     peerlist = list()
97     logger.debug("software='%s'", software)
98     if software is not None:
99         try:
100             logger.debug("Fetching instances for domain='%s',software='%s',origin='%s'", domain, software, origin)
101             peerlist = fetch_peers(domain, software, origin)
102         except network.exceptions as exception:
103             logger.warning("Cannot fetch peers from domain='%s',software='%s': '%s'", domain, software, type(exception))
104
105     logger.debug("peerlist[]='%s'", type(peerlist))
106     if isinstance(peerlist, list):
107         logger.debug("Invoking instances.set_total_peerlist(%s,%d) ...", domain, len(peerlist))
108         instances.set_total_peers(domain, peerlist)
109
110     logger.debug("Invoking cookies.clear(%s) ...", domain)
111     cookies.clear(domain)
112
113     logger.debug("peerlist[]='%s'", type(peerlist))
114     if peerlist is None:
115         logger.warning("Cannot fetch peers: domain='%s',software='%s'", domain, software)
116
117         if instances.has_pending(domain):
118             logger.debug("Flushing updates for domain='%s' ...", domain)
119             instances.update(domain)
120
121         _DEPTH = _DEPTH - 1
122         logger.debug("EXIT!")
123         return
124     elif len(peerlist) == 0:
125         logger.info("domain='%s' returned an empty peer list.", domain)
126
127         if instances.has_pending(domain):
128             logger.debug("Flushing updates for domain='%s' ...", domain)
129             instances.update(domain)
130
131         _DEPTH = _DEPTH - 1
132         logger.debug("domain='%s',software='%s' has an empty peer list returned - EXIT!", domain, software)
133         return
134
135     logger.info("Checking %d instance(s) from domain='%s',software='%s',depth=%d ...", len(peerlist), domain, software, _DEPTH)
136     for instance in peerlist:
137         logger.debug("instance='%s'", instance)
138         if instance is None or instance == "":
139             logger.debug("instance[%s]='%s' is either None or empty - SKIPPED!", type(instance), instance)
140             continue
141
142         logger.debug("instance='%s' - BEFORE!", instance)
143         instance = tidyup.domain(instance) if isinstance(instance, str) and instance != "" else None
144         logger.debug("instance='%s' - AFTER!", instance)
145
146         if instance is None or instance == "":
147             logger.warning("instance='%s' is empty after tidyup.domain(), domain='%s'", instance, domain)
148             continue
149         elif ".." in instance:
150             logger.warning("instance='%s' contains double-dot, removing ...", instance)
151             instance = instance.replace("..", ".")
152
153         logger.debug("instance='%s' - BEFORE!", instance)
154         instance = instance.encode("idna").decode("utf-8")
155         logger.debug("instance='%s' - AFTER!", instance)
156
157         if not domain_helper.is_wanted(instance):
158             logger.debug("instance='%s' is not wanted - SKIPPED!", instance)
159             continue
160         elif instance.find("/profile/") > 0 or instance.find("/users/") > 0 or (instances.is_registered(instance.split("/")[0]) and instance.find("/c/") > 0):
161             logger.debug("instance='%s' is a link to a single user profile - SKIPPED!", instance)
162             continue
163         elif instance.find("/tag/") > 0:
164             logger.debug("instance='%s' is a link to a tag - SKIPPED!", instance)
165             continue
166         elif not instances.is_registered(instance):
167             logger.debug("Checking if domain='%s' has pending updates ...", domain)
168             if instances.has_pending(domain):
169                 logger.debug("Flushing updates for domain='%s' ...", domain)
170                 instances.update(domain)
171
172             logger.debug("instance='%s',origin='%s',_DEPTH=%d reached!", instance, origin, _DEPTH)
173             if _DEPTH <= config.get("max_crawl_depth") and len(peerlist) >= config.get("min_peers_length"):
174                 logger.debug("Fetching instance='%s',origin='%s',command='%s',path='%s',_DEPTH=%d ...", instance, domain, command, path, _DEPTH)
175                 fetch_instances(instance, domain, None, command, path)
176             else:
177                 logger.debug("Adding instance='%s',domain='%s',command='%s',_DEPTH=%d ...", instance, domain, command, _DEPTH)
178                 instances.add(instance, domain, command)
179
180     logger.debug("Checking if domain='%s' has pending updates ...", domain)
181     if instances.has_pending(domain):
182         logger.debug("Flushing updates for domain='%s' ...", domain)
183         instances.update(domain)
184
185     _DEPTH = _DEPTH - 1
186     logger.debug("EXIT!")
187
188 def fetch_peers(domain: str, software: str, origin: str) -> list:
189     logger.debug("domain='%s',software='%s',origin='%s' - CALLED!", domain, software, origin)
190     domain_helper.raise_on(domain)
191
192     if not isinstance(software, str) and software is not None:
193         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
194     elif isinstance(software, str) and software == "":
195         raise ValueError("Parameter 'software' is empty")
196     elif software_helper.is_relay(software):
197         raise ValueError(f"domain='{domain}' is of software='{software}' and isn't supported here.")
198     elif not isinstance(origin, str) and origin is not None:
199         raise ValueError(f"Parameter origin[]='{type(origin)}' is not of type 'str'")
200     elif isinstance(origin, str) and origin == "":
201         raise ValueError("Parameter 'origin' is empty")
202
203     if software == "misskey":
204         logger.debug("Invoking misskey.fetch_peers(%s) ...", domain)
205         return misskey.fetch_peers(domain)
206     elif software == "lemmy":
207         logger.debug("Invoking lemmy.fetch_peers(%s,%s) ...", domain, origin)
208         return lemmy.fetch_peers(domain, origin)
209     elif software == "peertube":
210         logger.debug("Invoking peertube.fetch_peers(%s) ...", domain)
211         return peertube.fetch_peers(domain)
212
213     # No CSRF by default, you don't have to add network.api_headers by yourself here
214     headers = tuple()
215
216     try:
217         logger.debug("Checking CSRF for domain='%s'", domain)
218         headers = csrf.determine(domain, dict())
219     except network.exceptions as exception:
220         logger.warning("Exception '%s' during checking CSRF (fetch_peers,%s)", type(exception), __name__)
221         instances.set_last_error(domain, exception)
222
223         logger.debug("Returning empty list ... - EXIT!")
224         return list()
225
226     paths = [
227         "/api/v1/instance/peers",
228         "/api/v3/site",
229     ]
230
231     # Init peers variable
232     peers = list()
233
234     logger.debug("Checking %d paths ...", len(paths))
235     for path in paths:
236         logger.debug("Fetching path='%s' from domain='%s',software='%s' ...", path, domain, software)
237         data = network.get_json_api(
238             domain,
239             path,
240             headers=headers,
241             timeout=(config.get("connection_timeout"), config.get("read_timeout"))
242         )
243
244         logger.debug("data[]='%s'", type(data))
245         if "error_message" in data:
246             logger.debug("Was not able to fetch peers from path='%s',domain='%s' ...", path, domain)
247             instances.set_last_error(domain, data)
248         elif "json" in data and len(data["json"]) > 0:
249             logger.debug("Querying API path='%s' was successful: domain='%s',data[json][%s]()=%d", path, domain, type(data['json']), len(data['json']))
250             peers = data["json"]
251
252             logger.debug("Marking domain='%s' as successfully handled ...", domain)
253             instances.set_success(domain)
254             break
255
256     if not isinstance(peers, list):
257         logger.warning("peers[]='%s' is not of type 'list', maybe bad API response?", type(peers))
258         peers = list()
259
260     logger.debug("Invoking instances.set_total_peers(%s,%d) ...", domain, len(peers))
261     instances.set_total_peers(domain, peers)
262
263     logger.debug("peers()=%d - EXIT!", len(peers))
264     return peers
265
266 def fetch_generator_from_path(domain: str, path: str = "/") -> str:
267     logger.debug("domain='%s',path='%s' - CALLED!", domain, path)
268     domain_helper.raise_on(domain)
269
270     if not isinstance(path, str):
271         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
272     elif path == "":
273         raise ValueError("Parameter 'path' is empty")
274
275     software = None
276
277     logger.debug("Fetching path='%s' from domain='%s' ...", path, domain)
278     response = network.fetch_response(
279         domain,
280         path,
281         headers=network.web_headers,
282         timeout=(config.get("connection_timeout"), config.get("read_timeout")),
283         allow_redirects=True
284     )
285
286     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
287     if ((response.ok and response.status_code == 200) or response.status_code == 410) and response.text.find("<html") > 0 and domain_helper.is_in_url(domain, response.url):
288         logger.debug("Parsing response.text()=%d Bytes ...", len(response.text))
289         doc = bs4.BeautifulSoup(response.text, "html.parser")
290
291         logger.debug("doc[]='%s'", type(doc))
292         platform  = doc.find("meta", {"property": "og:platform"})
293         generator = doc.find("meta", {"name"    : "generator"})
294         site_name = doc.find("meta", {"property": "og:site_name"})
295         app_name  = doc.find("meta", {"name"    : "application-name"})
296
297         logger.debug("generator[]='%s',site_name[]='%s',platform[]='%s',app_name[]='%s'", type(generator), type(site_name), type(platform), type(app_name))
298         if isinstance(platform, bs4.element.Tag) and isinstance(platform.get("content"), str) and platform.get("content") != "":
299             logger.debug("Found property=og:platform, domain='%s'", domain)
300             software = tidyup.domain(platform.get("content"))
301
302             logger.debug("software[%s]='%s'", type(software), software)
303             if software is not None and software != "":
304                 logger.debug("domain='%s' has og:platform='%s' - Setting detection_mode=PLATFORM ...", domain, software)
305                 instances.set_detection_mode(domain, "PLATFORM")
306         elif isinstance(generator, bs4.element.Tag) and isinstance(generator.get("content"), str) and generator.get("content") != "":
307             logger.debug("Found generator meta tag: domain='%s'", domain)
308             software = tidyup.domain(generator.get("content"))
309
310             logger.debug("software[%s]='%s'", type(software), software)
311             if software is not None and software != "":
312                 logger.info("domain='%s' is generated by software='%s' - Setting detection_mode=GENERATOR ...", domain, software)
313                 instances.set_detection_mode(domain, "GENERATOR")
314         elif isinstance(app_name, bs4.element.Tag) and isinstance(app_name.get("content"), str) and app_name.get("content") != "":
315             logger.debug("Found property=og:app_name, domain='%s'", domain)
316             software = tidyup.domain(app_name.get("content"))
317
318             logger.debug("software[%s]='%s'", type(software), software)
319             if software is not None and software != "":
320                 logger.debug("domain='%s' has application-name='%s' - Setting detection_mode=app_name ...", domain, software)
321                 instances.set_detection_mode(domain, "APP_NAME")
322         elif isinstance(site_name, bs4.element.Tag) and isinstance(site_name.get("content"), str) and site_name.get("content") != "":
323             logger.debug("Found property=og:site_name, domain='%s'", domain)
324             software = tidyup.domain(site_name.get("content"))
325
326             logger.debug("software[%s]='%s'", type(software), software)
327             if software is not None and software != "":
328                 logger.debug("domain='%s' has og:site_name='%s' - Setting detection_mode=SITE_NAME ...", domain, software)
329                 instances.set_detection_mode(domain, "SITE_NAME")
330     elif not domain_helper.is_in_url(domain, response.url):
331         logger.warning("domain='%s' doesn't match response.url='%s', maybe redirect to other domain?", domain, response.url)
332
333         components = urlparse(response.url)
334         domain2 = components.netloc.lower().split(":")[0]
335
336         logger.debug("domain2='%s'", domain2)
337         if not domain_helper.is_wanted(domain2):
338             logger.debug("domain2='%s' is not wanted - EXIT!", domain2)
339             return None
340         elif not instances.is_registered(domain2):
341             logger.info("components.netloc='%s' is not registered, adding ...", components.netloc)
342             instances.add(domain2, domain, "redirect_target")
343
344         message = f"Redirect from domain='{domain}' to response.url='{response.url}'"
345         instances.set_last_error(domain, message)
346         instances.set_software(domain, None)
347         instances.set_detection_mode(domain, None)
348         instances.set_nodeinfo_url(domain, None)
349
350         raise requests.exceptions.TooManyRedirects(message)
351
352     logger.debug("software[]='%s'", type(software))
353     if isinstance(software, str) and software == "":
354         logger.debug("Corrected empty string to None for software of domain='%s'", domain)
355         software = None
356     elif isinstance(software, str) and ("." in software or " " in software):
357         logger.debug("software='%s' may contain a version number, domain='%s', removing it ...", software, domain)
358         software = version.remove(software)
359
360     logger.debug("software[]='%s'", type(software))
361     if isinstance(software, str) and "powered by " in software:
362         logger.debug("software='%s' has 'powered by' in it", software)
363         software = version.remove(software_helper.strip_powered_by(software))
364     elif isinstance(software, str) and " hosted on " in software:
365         logger.debug("software='%s' has 'hosted on' in it", software)
366         software = version.remove(software_helper.strip_hosted_on(software))
367     elif isinstance(software, str) and " by " in software:
368         logger.debug("software='%s' has ' by ' in it", software)
369         software = software_helper.strip_until(software, " by ")
370     elif isinstance(software, str) and " see " in software:
371         logger.debug("software='%s' has ' see ' in it", software)
372         software = software_helper.strip_until(software, " see ")
373
374     logger.debug("software='%s' - EXIT!", software)
375     return software
376
377 def determine_software(domain: str, path: str = None) -> str:
378     logger.debug("domain='%s',path='%s' - CALLED!", domain, path)
379     domain_helper.raise_on(domain)
380
381     if not isinstance(path, str) and path is not None:
382         raise ValueError(f"Parameter path[]='{type(path)}' is not of type 'str'")
383
384     logger.debug("Fetching nodeinfo from domain='%s',path='%s' ...", domain, path)
385     data = nodeinfo.fetch(domain, path)
386     software = None
387
388     logger.debug("data[%s]='%s'", type(data), data)
389     if "exception" in data:
390         # Continue raising it
391         logger.debug("data()=%d contains exception='%s' - raising ...", len(data), type(data["exception"]))
392         raise data["exception"]
393     elif "error_message" in data:
394         logger.debug("Returned error_message during fetching nodeinfo: '%s',status_code=%d", data['error_message'], data['status_code'])
395         software = fetch_generator_from_path(domain)
396         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
397     elif "json" in data:
398         logger.debug("domain='%s',path='%s',data[json] found ...", domain, path)
399         data = data["json"]
400     else:
401         logger.debug("Auto-detection for domain='%s' was failing, fetching / ...", domain)
402         software = fetch_generator_from_path(domain)
403         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
404
405     if "status" in data and data["status"] == "error" and "message" in data:
406         logger.warning("JSON response is an error: '%s' - Resetting detection_mode,nodeinfo_url ...", data["message"])
407         instances.set_last_error(domain, data["message"])
408         instances.set_detection_mode(domain, None)
409         instances.set_nodeinfo_url(domain, None)
410         software = fetch_generator_from_path(domain)
411         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
412     elif "software" in data and "name" in data["software"]:
413         logger.debug("Found data[json][software][name] in JSON response")
414         software = data["software"]["name"]
415         logger.debug("software[%s]='%s' - FOUND!", type(software), software)
416     elif "message" in data:
417         logger.warning("JSON response contains only a message: '%s' - Resetting detection_mode,nodeinfo_url ...", data["message"])
418         instances.set_last_error(domain, data["message"])
419         instances.set_detection_mode(domain, None)
420         instances.set_nodeinfo_url(domain, None)
421
422         logger.debug("Invoking fetch_generator_from_path(%s) ...", domain)
423         software = fetch_generator_from_path(domain)
424         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
425     elif "server" in data and "software" in data["server"]:
426         logger.debug("Found data[server][software]='%s' for domain='%s'", data["server"]["software"].lower(), domain)
427         software = data["server"]["software"].lower()
428         logger.debug("Detected software for domain='%s' is: '%s'", domain, software)
429     elif "software" not in data or "name" not in data["software"]:
430         logger.debug("JSON response from domain='%s' does not include [software][name] - Resetting detection_mode,nodeinfo_url ...", domain)
431         instances.set_detection_mode(domain, None)
432         instances.set_nodeinfo_url(domain, None)
433
434         logger.debug("Invoking fetch_generator_from_path(%s) ...", domain)
435         software = fetch_generator_from_path(domain)
436         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
437
438     logger.debug("software[%s]='%s'", type(software), software)
439     if software is None or software == "":
440         logger.debug("Returning None - EXIT!")
441         return None
442
443     logger.debug("software='%s'- BEFORE!", software)
444     software = software_helper.alias(software)
445     logger.debug("software['%s']='%s' - AFTER!", type(software), software)
446
447     if str(software) == "":
448         logger.debug("software for domain='%s' was not detected, trying generator ...", domain)
449         software = fetch_generator_from_path(domain)
450     elif len(str(software)) > 0 and ("." in software or " " in software):
451         logger.debug("software='%s' may contain a version number, domain='%s', removing it ...", software, domain)
452         software = version.remove(software)
453
454     logger.debug("software[]='%s'", type(software))
455     if isinstance(software, str) and "powered by" in software:
456         logger.debug("software='%s' has 'powered by' in it", software)
457         software = version.remove(software_helper.strip_powered_by(software))
458
459     software = software.strip()
460
461     logger.debug("software='%s' - EXIT!", software)
462     return software
463
464 def find_domains(tag: bs4.element.Tag) -> list:
465     logger.debug("tag[]='%s' - CALLED!", type(tag))
466     if not isinstance(tag, bs4.element.Tag):
467         raise ValueError(f"Parameter tag[]='{type(tag)}' is not type of bs4.element.Tag")
468     elif len(tag.select("tr")) == 0:
469         raise KeyError("No table rows found in table!")
470
471     domains = list()
472     for element in tag.select("tr"):
473         logger.debug("element[]='%s'", type(element))
474         if not element.find("td"):
475             logger.debug("Skipping element, no <td> found")
476             continue
477
478         domain = tidyup.domain(element.find("td").text)
479         reason = tidyup.reason(element.findAll("td")[1].text)
480
481         logger.debug("domain='%s',reason='%s'", domain, reason)
482
483         if not domain_helper.is_wanted(domain):
484             logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
485             continue
486         elif domain == "gab.com/.ai, develop.gab.com":
487             logger.debug("Multiple domains detected in one row")
488             domains.append({
489                 "domain": "gab.com",
490                 "reason": reason,
491             })
492             domains.append({
493                 "domain": "gab.ai",
494                 "reason": reason,
495             })
496             domains.append({
497                 "domain": "develop.gab.com",
498                 "reason": reason,
499             })
500             continue
501         elif not validators.domain(domain.split("/")[0]):
502             logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
503             continue
504
505         logger.debug("Adding domain='%s',reason='%s' ...", domain, reason)
506         domains.append({
507             "domain": domain,
508             "reason": reason,
509         })
510
511     logger.debug("domains()=%d - EXIT!", len(domains))
512     return domains
513
514 def add_peers(rows: dict) -> list:
515     logger.debug("rows[]='%s' - CALLED!", type(rows))
516     if not isinstance(rows, dict):
517         raise ValueError(f"Parameter rows[]='{type(rows)}' is not of type 'dict'")
518
519     peers = list()
520     for key in ["linked", "allowed", "blocked"]:
521         logger.debug("Checking key='%s'", key)
522         if key not in rows or rows[key] is None:
523             logger.debug("Cannot find key='%s' or it is NoneType - SKIPPED!", key)
524             continue
525
526         logger.debug("Adding %d peer(s) to peers list ...", len(rows[key]))
527         for peer in rows[key]:
528             logger.debug("peer[%s]='%s' - BEFORE!", type(peer), peer)
529             if peer is None or peer == "":
530                 logger.debug("peer is empty - SKIPPED")
531                 continue
532             elif isinstance(peer, dict) and "domain" in peer:
533                 logger.debug("peer[domain]='%s'", peer["domain"])
534                 peer = tidyup.domain(peer["domain"])
535             elif isinstance(peer, str):
536                 logger.debug("peer='%s'", peer)
537                 peer = tidyup.domain(peer)
538             else:
539                 raise ValueError(f"peer[]='{type(peer)}' is not supported,key='{key}'")
540
541             logger.debug("peer[%s]='%s' - AFTER!", type(peer), peer)
542             if not domain_helper.is_wanted(peer):
543                 logger.debug("peer='%s' is not wanted - SKIPPED!", peer)
544                 continue
545
546             logger.debug("Appending peer='%s' ...", peer)
547             peers.append(peer)
548
549     logger.debug("peers()=%d - EXIT!", len(peers))
550     return peers
551
552 def fetch_blocks(domain: str) -> list:
553     logger.debug("domain='%s' - CALLED!", domain)
554     domain_helper.raise_on(domain)
555
556     if not instances.is_registered(domain):
557         raise Exception(f"domain='{domain}' is not registered but function is invoked.")
558
559     # Init block list
560     blocklist = list()
561
562     # No CSRF by default, you don't have to add network.api_headers by yourself here
563     headers = tuple()
564
565     try:
566         logger.debug("Checking CSRF for domain='%s'", domain)
567         headers = csrf.determine(domain, dict())
568     except network.exceptions as exception:
569         logger.warning("Exception '%s' during checking CSRF (fetch_blocks,%s)", type(exception), __name__)
570         instances.set_last_error(domain, exception)
571
572         logger.debug("Returning empty list ... - EXIT!")
573         return list()
574
575     try:
576         # json endpoint for newer mastodongs
577         logger.info("Fetching domain_blocks from domain='%s' ...", domain)
578         data = network.get_json_api(
579             domain,
580             "/api/v1/instance/domain_blocks",
581             headers=headers,
582             timeout=(config.get("connection_timeout"), config.get("read_timeout"))
583         )
584         rows = list()
585
586         logger.debug("data[]='%s'", type(data))
587         if "error_message" in data:
588             logger.debug("Was not able to fetch domain_blocks from domain='%s': status_code=%d,error_message='%s'", domain, data['status_code'], data['error_message'])
589             instances.set_last_error(domain, data)
590             return blocklist
591         elif "json" in data and "error" in data["json"]:
592             logger.warning("JSON API returned error message: '%s'", data["json"]["error"])
593             instances.set_last_error(domain, data)
594             return blocklist
595         else:
596             # Getting blocklist
597             rows = data["json"]
598
599             logger.debug("Marking domain='%s' as successfully handled ...", domain)
600             instances.set_success(domain)
601
602         logger.debug("rows[%s]()=%d", type(rows), len(rows))
603         if len(rows) > 0:
604             logger.debug("Checking %d entries from domain='%s' ...", len(rows), domain)
605             for block in rows:
606                 # Check type
607                 logger.debug("block[]='%s'", type(block))
608                 if not isinstance(block, dict):
609                     logger.debug("block[]='%s' is of type 'dict' - SKIPPED!", type(block))
610                     continue
611                 elif "domain" not in block:
612                     logger.warning("block()=%d does not contain element 'domain' - SKIPPED!", len(block))
613                     continue
614                 elif "severity" not in block:
615                     logger.warning("block()=%d does not contain element 'severity' - SKIPPED!", len(block))
616                     continue
617                 elif block["severity"] in ["accept", "accepted"]:
618                     logger.debug("block[domain]='%s' has unwanted severity level '%s' - SKIPPED!", block["domain"], block["severity"])
619                     continue
620                 elif "digest" in block and not validators.hashes.sha256(block["digest"]):
621                     logger.warning("block[domain]='%s' has invalid block[digest]='%s' - SKIPPED!", block["domain"], block["digest"])
622                     continue
623
624                 reason = tidyup.reason(block["comment"]) if "comment" in block and block["comment"] is not None and block["comment"] != "" else None
625
626                 logger.debug("Appending blocker='%s',blocked='%s',reason='%s',block_level='%s'", domain, block["domain"], reason, block["severity"])
627                 blocklist.append({
628                     "blocker"    : domain,
629                     "blocked"    : block["domain"],
630                     "digest"     : block["digest"] if "digest" in block else None,
631                     "reason"     : reason,
632                     "block_level": blocks.alias_block_level(block["severity"]),
633                 })
634         else:
635             logger.debug("domain='%s' has no block list", domain)
636
637     except network.exceptions as exception:
638         logger.warning("domain='%s',exception[%s]='%s'", domain, type(exception), str(exception))
639         instances.set_last_error(domain, exception)
640
641     logger.debug("blocklist()=%d - EXIT!", len(blocklist))
642     return blocklist