1 # Copyright (C) 2023 Free Software Foundation
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.
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.
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/>.
21 from fba import database
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
30 from fba.http import federation
31 from fba.http import network
33 from fba.models import blocks
34 from fba.models import instances
36 logging.basicConfig(level=logging.INFO)
37 logger = logging.getLogger(__name__)
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)
44 if not isinstance(command, str):
45 raise ValueError(f"Parameter command[]='{type(command)}' is not of type 'str'")
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")
53 logger.debug("blocked='%s' - BEFORE!", blocked)
54 blocked = utils.deobfuscate(blocked, blocker)
55 logger.debug("blocked='%s' - AFTER!", blocked)
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)
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)
66 elif not force and instances.is_recent(blocked):
67 logger.debug("blocked='%s' has been recently checked - EXIT!", blocked)
72 logger.info("Fetching instances for blocked='%s',blocker='%s',command='%s' ...", blocked, blocker, command)
73 federation.fetch_instances(blocked, blocker, None, command)
75 logger.debug("Setting processed=True for blocked='%s',blocker='%s' ...", blocked, blocker)
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)
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)
86 logger.debug("processed='%s' - EXIT!", processed)
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)
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")
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)
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)
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)
118 logger.debug("added='%s' - EXIT!", added)
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)
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'")
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")
138 logger.debug("Setting last_blocked for blocker='%s' ...", blocker)
139 instances.set_last_blocked(blocker)
144 logger.info("Fetching url='%s' for blocker='%s' ...", url, blocker)
145 response = network.fetch_url(
148 (config.get("connection_timeout"), config.get("read_timeout"))
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)
156 logger.debug("Fetched %d Bytes, parsing CSV ...", len(response.content))
157 reader = csv.DictReader(response.content.decode("utf-8").splitlines(), dialect="unix")
163 logger.debug("row[%s]='%s'", type(row), row)
164 domain = severity = reason = None
165 reject_media = reject_reports = False
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
174 logger.warning("row='%s' does not contain domain column - SKIPPED!", row)
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"])
182 logger.debug("row='%s' does not contain severity column, setting 'reject'", 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
192 logger.debug("row='%s' has no reason/comment key provided", row)
194 if "#reject_media" in row and row["#reject_media"].lower() == "true":
196 elif "reject_media" in row and row["reject_media"].lower() == "true":
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
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)
209 elif domain.endswith(".onion"):
210 logger.debug("domain='%s' is a TOR .onion domain - SKIPPED", domain)
212 elif domain.endswith(".i2p") and not config.get("allow_i2p_domain"):
213 logger.debug("domain='%s' is an I2P .onion domain - SKIPPED", domain)
215 elif domain.endswith(".arpa"):
216 logger.debug("domain='%s' is a reverse IP address - SKIPPED", domain)
218 elif domain.endswith(".tld"):
219 logger.debug("domain='%s' is a fake domain - SKIPPED", domain)
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)
226 logger.debug("Marking domain='%s' as handled", domain)
227 domains.append(domain)
229 if not validators.domain(domain):
230 logger.debug("domain='%s' is not a valid domain - SKIPPED!")
232 elif blacklist.is_blacklisted(domain):
233 logger.debug("domain='%s' is blacklisted - SKIPPED!", domain)
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)
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)
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)
250 logger.debug("reject_media='%s',reject_reports='%s'", reject_media, reject_reports)
252 block(blocker, domain, None, "reject_media")
254 block(blocker, domain, None, "reject_reports")
256 logger.debug("Invoking commit() ...")
257 database.connection.commit()
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)
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)
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)
274 logger.debug("EXIT!")