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