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