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