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