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