]> git.mxchange.org Git - fba.git/blob - fba/http/federation.py
845d46af8e3430dfefc7280d2244898b38cc7639
[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
452         components = urlparse(response.url)
453
454         log.debug("components[]='%s'", type(components))
455         if not instances.is_registered(components.netloc):
456             logger.info("components.netloc='%s' is not registered, adding ...", components.netloc)
457             fetch_instances(components.netloc, domain, None, "fetch_generator")
458
459         message = f"Redirect from domain='{domain}' to response.url='{response.url}'"
460         instances.set_last_error(domain, message)
461         instances.set_software(domain, None)
462         instances.set_detection_mode(domain, None)
463         instances.set_nodeinfo_url(domain, None)
464
465         raise requests.exceptions.TooManyRedirects(message)
466
467     logger.debug("software[]='%s'", type(software))
468     if isinstance(software, str) and software == "":
469         logger.debug("Corrected empty string to None for software of domain='%s'", domain)
470         software = None
471     elif isinstance(software, str) and ("." in software or " " in software):
472         logger.debug("software='%s' may contain a version number, domain='%s', removing it ...", software, domain)
473         software = version.remove(software)
474
475     logger.debug("software[]='%s'", type(software))
476     if isinstance(software, str) and "powered by " in software:
477         logger.debug("software='%s' has 'powered by' in it", software)
478         software = version.remove(version.strip_powered_by(software))
479     elif isinstance(software, str) and " hosted on " in software:
480         logger.debug("software='%s' has 'hosted on' in it", software)
481         software = version.remove(version.strip_hosted_on(software))
482     elif isinstance(software, str) and " by " in software:
483         logger.debug("software='%s' has ' by ' in it", software)
484         software = version.strip_until(software, " by ")
485     elif isinstance(software, str) and " see " in software:
486         logger.debug("software='%s' has ' see ' in it", software)
487         software = version.strip_until(software, " see ")
488
489     logger.debug("software='%s' - EXIT!", software)
490     return software
491
492 def determine_software(domain: str, path: str = None) -> str:
493     logger.debug("domain(%d)='%s',path='%s' - CALLED!", len(domain), domain, path)
494     domain_helper.raise_on(domain)
495
496     if not isinstance(path, str) and path is not None:
497         raise ValueError(f"Parameter path[]='{type(path)}' is not of type 'str'")
498
499     logger.debug("Determining software for domain='%s',path='%s'", domain, path)
500     software = None
501
502     logger.debug("Fetching nodeinfo from domain='%s' ...", domain)
503     data = fetch_nodeinfo(domain, path)
504
505     logger.debug("data[%s]='%s'", type(data), data)
506     if "exception" in data:
507         # Continue raising it
508         logger.debug("data()=%d contains exception='%s' - raising ...", len(data), type(data["exception"]))
509         raise data["exception"]
510     elif "error_message" in data:
511         logger.debug("Returned error_message during fetching nodeinfo: '%s',status_code=%d", data['error_message'], data['status_code'])
512         software = fetch_generator_from_path(domain)
513         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
514     elif "json" in data:
515         logger.debug("domain='%s',path='%s',data[json] found ...", domain, path)
516         data = data["json"]
517     else:
518         logger.debug("JSON response from domain='%s' does not include [software][name], fetching / ...", domain)
519         software = fetch_generator_from_path(domain)
520         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
521
522     if "status" in data and data["status"] == "error" and "message" in data:
523         logger.warning("JSON response is an error: '%s' - Resetting detection_mode,nodeinfo_url ...", data["message"])
524         instances.set_last_error(domain, data["message"])
525         instances.set_detection_mode(domain, None)
526         instances.set_nodeinfo_url(domain, None)
527         software = fetch_generator_from_path(domain)
528         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
529     elif "software" in data and "name" in data["software"]:
530         logger.debug("Found data[json][software][name] in JSON response")
531         software = data["software"]["name"]
532         logger.debug("software[%s]='%s' - FOUND!", type(software), software)
533     elif "message" in data:
534         logger.warning("JSON response contains only a message: '%s' - Resetting detection_mode,nodeinfo_url ...", data["message"])
535         instances.set_last_error(domain, data["message"])
536         instances.set_detection_mode(domain, None)
537         instances.set_nodeinfo_url(domain, None)
538
539         logger.debug("Invoking fetch_generator_from_path(%s) ...", domain)
540         software = fetch_generator_from_path(domain)
541         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
542     elif "software" not in data or "name" not in data["software"]:
543         logger.debug("JSON response from domain='%s' does not include [software][name] - Resetting detection_mode,nodeinfo_url ...", domain)
544         instances.set_detection_mode(domain, None)
545         instances.set_nodeinfo_url(domain, None)
546
547         logger.debug("Invoking fetch_generator_from_path(%s) ...", domain)
548         software = fetch_generator_from_path(domain)
549         logger.debug("Generator for domain='%s' is: '%s'", domain, software)
550
551     logger.debug("software[%s]='%s'", type(software), software)
552     if software is None:
553         logger.debug("Returning None - EXIT!")
554         return None
555
556     logger.debug("software='%s'- BEFORE!", software)
557     software = software_helper.alias(software)
558     logger.debug("software['%s']='%s' - AFTER!", type(software), software)
559
560     if str(software) == "":
561         logger.debug("software for domain='%s' was not detected, trying generator ...", domain)
562         software = fetch_generator_from_path(domain)
563     elif len(str(software)) > 0 and ("." in software or " " in software):
564         logger.debug("software='%s' may contain a version number, domain='%s', removing it ...", software, domain)
565         software = version.remove(software)
566
567     logger.debug("software[]='%s'", type(software))
568     if isinstance(software, str) and "powered by" in software:
569         logger.debug("software='%s' has 'powered by' in it", software)
570         software = version.remove(version.strip_powered_by(software))
571
572     logger.debug("software='%s' - EXIT!", software)
573     return software
574
575 def find_domains(tag: bs4.element.Tag) -> list:
576     logger.debug("tag[]='%s' - CALLED!", type(tag))
577     if not isinstance(tag, bs4.element.Tag):
578         raise ValueError(f"Parameter tag[]='{type(tag)}' is not type of bs4.element.Tag")
579     elif len(tag.select("tr")) == 0:
580         raise KeyError("No table rows found in table!")
581
582     domains = list()
583     for element in tag.select("tr"):
584         logger.debug("element[]='%s'", type(element))
585         if not element.find("td"):
586             logger.debug("Skipping element, no <td> found")
587             continue
588
589         domain = tidyup.domain(element.find("td").text)
590         reason = tidyup.reason(element.findAll("td")[1].text)
591
592         logger.debug("domain='%s',reason='%s'", domain, reason)
593
594         if not utils.is_domain_wanted(domain):
595             logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
596             continue
597         elif domain == "gab.com/.ai, develop.gab.com":
598             logger.debug("Multiple domains detected in one row")
599             domains.append({
600                 "domain": "gab.com",
601                 "reason": reason,
602             })
603             domains.append({
604                 "domain": "gab.ai",
605                 "reason": reason,
606             })
607             domains.append({
608                 "domain": "develop.gab.com",
609                 "reason": reason,
610             })
611             continue
612         elif not validators.domain(domain.split("/")[0]):
613             logger.warning("domain='%s' is not a valid domain - SKIPPED!", domain)
614             continue
615
616         logger.debug("Adding domain='%s',reason='%s' ...", domain, reason)
617         domains.append({
618             "domain": domain,
619             "reason": reason,
620         })
621
622     logger.debug("domains()=%d - EXIT!", len(domains))
623     return domains
624
625 def add_peers(rows: dict) -> list:
626     logger.debug("rows[]='%s' - CALLED!", type(rows))
627     if not isinstance(rows, dict):
628         raise ValueError(f"Parameter rows[]='{type(rows)}' is not of type 'dict'")
629
630     peers = list()
631     for key in ["linked", "allowed", "blocked"]:
632         logger.debug("Checking key='%s'", key)
633         if key not in rows or rows[key] is None:
634             logger.debug("Cannot find key='%s' or it is NoneType - SKIPPED!", key)
635             continue
636
637         logger.debug("Adding %d peer(s) to peers list ...", len(rows[key]))
638         for peer in rows[key]:
639             logger.debug("peer[%s]='%s' - BEFORE!", type(peer), peer)
640             if peer is None or peer == "":
641                 logger.debug("peer is empty - SKIPPED")
642                 continue
643             elif isinstance(peer, dict) and "domain" in peer:
644                 logger.debug("peer[domain]='%s'", peer["domain"])
645                 peer = tidyup.domain(peer["domain"])
646             elif isinstance(peer, str):
647                 logger.debug("peer='%s'", peer)
648                 peer = tidyup.domain(peer)
649             else:
650                 raise ValueError(f"peer[]='{type(peer)}' is not supported,key='{key}'")
651
652             logger.debug("peer[%s]='%s' - AFTER!", type(peer), peer)
653             if not utils.is_domain_wanted(peer):
654                 logger.debug("peer='%s' is not wanted - SKIPPED!", peer)
655                 continue
656
657             logger.debug("Appending peer='%s' ...", peer)
658             peers.append(peer)
659
660     logger.debug("peers()=%d - EXIT!", len(peers))
661     return peers