]> git.mxchange.org Git - fba.git/blob - fba/helpers/processing.py
Continued:
[fba.git] / fba / helpers / processing.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 csv
17 import logging
18
19 import validators
20
21 from fba import database
22 from fba import utils
23
24 from fba.helpers import blacklist
25 from fba.helpers import blocklists
26 from fba.helpers import config
27 from fba.helpers import domain as domain_helper
28 from fba.helpers import tidyup
29
30 from fba.http import federation
31 from fba.http import network
32
33 from fba.models import blocks
34 from fba.models import instances
35
36 logging.basicConfig(level=logging.INFO)
37 logger = logging.getLogger(__name__)
38
39 def instance(blocked: str, blocker: str, command: str) -> bool:
40     logger.debug("blocked='%s',blocker='%s',command='%s' - CALLED!", blocked, blocker, command)
41     domain_helper.raise_on(blocked)
42     domain_helper.raise_on(blocker)
43
44     if not isinstance(command, str):
45         raise ValueError(f"Parameter command[]='{type(command)}' is not of type 'str'")
46     elif command == "":
47         raise ValueError("Parameter 'command' is empty")
48     elif blacklist.is_blacklisted(blocked):
49         raise ValueError(f"blocked='{blocked}' is blacklisted but function was invoked")
50     elif blacklist.is_blacklisted(blocker):
51         raise ValueError(f"blocker='{blocker}' is blacklisted but function was invoked")
52
53     logger.debug("blocked='%s' - BEFORE!", blocked)
54     blocked = utils.deobfuscate(blocked, blocker)
55     logger.debug("blocked='%s' - AFTER!", blocked)
56
57     logger.debug("Checking if blocker='%s' has pending data ...", blocker)
58     if instances.has_pending(blocker):
59         logger.debug("Flushing updates for blocker='%s' ...", blocker)
60         instances.update(blocker)
61
62     logger.debug("Checking blocked='%s' if wanted and recent ...", blocked)
63     if not domain_helper.is_wanted(blocked):
64         logger.debug("blocked='%s' is not wanted - SKIPPED!", blocked)
65         return False
66     elif instances.is_recent(blocked):
67         logger.debug("blocked='%s' has been recently checked - SKIPPED!", blocked)
68         return False
69
70     processed = False
71     try:
72         logger.info("Fetching instances for blocked='%s',blocker='%s',command='%s' ...", blocked, blocker, command)
73         federation.fetch_instances(blocked, blocker, None, command)
74         processed = True
75     except network.exceptions as exception:
76         logger.warning("Exception '%s' during fetching instances (%s) from blocked='%s'", type(exception), command, blocked)
77         instances.set_last_error(blocked, exception)
78
79     logger.debug("Checking if blocked='%s' has pending updates ...", blocked)
80     if instances.has_pending(blocked):
81         logger.debug("Flushing updates for blocked='%s' ...", blocked)
82         instances.update(blocked)
83
84     logger.debug("processed='%s' - EXIT!", processed)
85     return processed
86
87 def block(blocker: str, blocked: str, reason: str, block_level: str) -> bool:
88     logger.debug("blocker='%s',blocked='%s',reason='%s',block_level='%s' - CALLED!", blocker, blocked, reason, block_level)
89     domain_helper.raise_on(blocker)
90     domain_helper.raise_on(blocked)
91
92     if not isinstance(reason, str) and reason is not None:
93         raise ValueError(f"Parameter reason[]='{type(reason)}' is not of type 'str'")
94     elif not isinstance(block_level, str):
95         raise ValueError(f"Parameter block_level[]='{type(block_level)}' is not of type 'str'")
96     elif block_level == "":
97         raise ValueError("Parameter block_level is empty")
98     elif blacklist.is_blacklisted(blocker):
99         raise ValueError(f"blocker='{blocker}' is blacklisted but function was invoked")
100     elif blacklist.is_blacklisted(blocked):
101         raise ValueError(f"blocked='{blocked}' is blacklisted but function was invoked")
102
103     added = False
104     if not blocks.is_instance_blocked(blocker, blocked, block_level):
105         logger.debug("Invoking blocks.add(%s, %s, %s, %s) ...", blocker, blocked, reason, block_level)
106         blocks.add(blocker, blocked, reason, block_level)
107         added = True
108     else:
109         logger.debug("Updating last_seen for blocker='%s',blocked='%s',block_level='%s' ...", blocker, blocked, block_level)
110         blocks.update_last_seen(blocker, blocked, block_level)
111
112     logger.debug("added='%s' - EXIT!", added)
113     return added
114
115 def csv_block(blocker: str, url: str, command: str):
116     logger.debug("blocker='%s',url='%s',command='%s' - CALLED!", blocker, url, command)
117     domain_helper.raise_on(blocker)
118
119     if not isinstance(url, str):
120         raise ValueError(f"url[]='{url}' is not of type 'str'")
121     elif url == "":
122         raise ValueError("Parameter 'url' is empty")
123     elif not validators.url(url):
124         raise ValueError(f"Parameter url='{url}' is not a valid URL")
125     elif not isinstance(command, str):
126         raise ValueError(f"command[]='{command}' is not of type 'str'")
127     elif command == "":
128         raise ValueError("Parameter 'command' is empty")
129     elif blacklist.is_blacklisted(blocker):
130         raise ValueError(f"blocker='{blocker}' is blacklisted but function was invoked")
131
132     logger.debug("Setting last_blocked for blocker='%s' ...", blocker)
133     instances.set_last_blocked(blocker)
134
135     domains = list()
136
137     # Fetch this URL
138     logger.info("Fetching url='%s' for blocker='%s' ...", url, blocker)
139     response = utils.fetch_url(
140         url,
141         network.web_headers,
142         (config.get("connection_timeout"), config.get("read_timeout"))
143     )
144
145     logger.debug("response.ok='%s',response.status_code=%d,response.content()=%d", response.ok, response.status_code, len(response.content))
146     if not response.ok or response.status_code > 200 or response.content == "":
147         logger.warning("Could not fetch url='%s' for blocker='%s' - EXIT!", url, blocker)
148         return
149
150     logger.debug("Fetched %d Bytes, parsing CSV ...", len(response.content))
151     reader = csv.DictReader(response.content.decode("utf-8").splitlines(), dialect="unix")
152
153     blockdict = list()
154
155     cnt = 0
156     for row in reader:
157         logger.debug("row[%s]='%s'", type(row), row)
158         domain = severity = reason = None
159         reject_media = reject_reports = False
160
161         if "#domain" in row:
162             domain = tidyup.domain(row["#domain"]) if row["#domain"] not in [None, ""] else None
163         elif "domain" in row:
164             domain = tidyup.domain(row["domain"]) if row["domain"] not in [None, ""] else None
165         elif "Domain" in row:
166             domain = tidyup.domain(row["Domain"]) if row["Domain"] not in [None, ""] else None
167         else:
168             logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
169             continue
170
171         if "#severity" in row:
172             severity = blocks.alias_block_level(row["#severity"])
173         elif "severity" in row:
174             severity = blocks.alias_block_level(row["severity"])
175         else:
176             logger.debug("row='%s' does not contain severity column, setting 'reject'", row)
177             severity = "reject"
178
179         if "reason" in row:
180             reason = tidyup.reason(row["reason"]) if row["reason"] not in [None, ""] else None
181         elif "comment" in row:
182             reason = tidyup.reason(row["comment"]) if row["comment"] not in [None, ""] else None
183         elif "Comment" in row:
184             reason = tidyup.reason(row["Comment"]) if row["Comment"] not in [None, ""] else None
185         else:
186             logger.debug("row='%s' has no reason/comment key provided", row)
187
188         if "#reject_media" in row and row["#reject_media"].lower() == "true":
189             reject_media = True
190         elif "reject_media" in row and row["reject_media"].lower() == "true":
191             reject_media = True
192
193         if "#reject_reports" in row and row["#reject_reports"].lower() == "true":
194             reject_reports = True
195         elif "reject_reports" in row and row["reject_reports"].lower() == "true":
196             reject_reports = True
197
198         cnt = cnt + 1
199         logger.debug("domain='%s',severity='%s',reject_media='%s',reject_reports='%s'", domain, severity, reject_media, reject_reports)
200         if domain in [None, ""]:
201             logger.debug("domain='%s' is empty - SKIPPED!", domain)
202             continue
203         elif domain.endswith(".onion"):
204             logger.debug("domain='%s' is a TOR .onion domain - SKIPPED", domain)
205             continue
206         elif domain.endswith(".i2p") and config.get("allow_i2p_domain") == "true":
207             logger.debug("domain='%s' is an I2P .onion domain - SKIPPED", domain)
208             continue
209         elif domain.endswith(".arpa"):
210             logger.debug("domain='%s' is a reverse IP address - SKIPPED", domain)
211             continue
212         elif domain.endswith(".tld"):
213             logger.debug("domain='%s' is a fake domain - SKIPPED", domain)
214             continue
215         elif domain.find("*") >= 0 or domain.find("?") >= 0:
216             logger.debug("domain='%s' is obfuscated - Invoking utils.deobfuscate(%s, %s) ...", domain, domain, blocker)
217             domain = utils.deobfuscate(domain, blocker)
218             logger.debug("domain='%s' - AFTER!", domain)
219
220         logger.debug("Marking domain='%s' as handled", domain)
221         domains.append(domain)
222
223         if not validators.domain(domain):
224             logger.debug("domain='%s' is not a valid domain - SKIPPED!")
225             continue
226         elif blacklist.is_blacklisted(domain):
227             logger.warning("domain='%s' is blacklisted - SKIPPED!", domain)
228             continue
229         elif blocks.is_instance_blocked(blocker, domain, severity):
230             logger.debug("blocker='%s' has already blocked domain='%s' with severity='%s' - SKIPPED!", blocker, domain, severity)
231             continue
232
233         logger.debug("Processing domain='%s',blocker='%s',command='%s' ...", domain, blocker, command)
234         processed = instance(domain, blocker, command)
235         logger.debug("processed='%s'", processed)
236
237         if block(blocker, domain, reason, severity) and config.get("bot_enabled"):
238             logger.debug("Appending blocked='%s',reason='%s' for blocker='%s' ...", domain, reason, blocker)
239             blockdict.append({
240                 "blocked": domain,
241                 "reason" : reason,
242             })
243
244         logger.debug("reject_media='%s',reject_reports='%s'", reject_media, reject_reports)
245         if reject_media:
246             block(blocker, domain, None, "reject_media")
247         if reject_reports:
248             block(blocker, domain, None, "reject_reports")
249
250         logger.debug("Invoking commit() ...")
251         database.connection.commit()
252
253     logger.debug("blocker='%s'", blocker)
254     if blocklists.has(blocker):
255         logger.debug("Invoking instances.set_total_blocks(%s, domains()=%d) ...", blocker, len(domains))
256         instances.set_total_blocks(blocker, domains)
257
258     logger.debug("Checking if blocker='%s' has pending updates ...", blocker)
259     if instances.has_pending(blocker):
260         logger.debug("Flushing updates for blocker='%s' ...", blocker)
261         instances.update(blocker)
262
263     logger.debug("config.get(bot_enabled)='%s',blockdict()=%d", config.get("bot_enabled"), len(blockdict))
264     if config.get("bot_enabled") and len(blockdict) > 0:
265         logger.info("Sending bot POST for blocker='%s',blockdict()=%d ...", blocker, len(blockdict))
266         network.send_bot_post(blocker, blockdict)
267
268     logger.debug("EXIT!")