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