]> git.mxchange.org Git - fba.git/blob - network.py
2b1a9bbac8a2b3ee29c6d5db5e59a244dbd68c50
[fba.git] / network.py
1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17 import logging
18
19 import reqto
20 import requests
21 import urllib3
22
23 from fba import utils
24
25 from fba.helpers import config
26 from fba.helpers import cookies
27 from fba.helpers import domain as domain_helper
28 from fba.helpers import json as json_helper
29
30 from fba.models import instances
31
32 logging.basicConfig(level=logging.INFO)
33 logger = logging.getLogger(__name__)
34
35 # HTTP headers for non-API requests
36 web_headers = {
37     "User-Agent": config.get("useragent"),
38 }
39
40 # HTTP headers for API requests
41 api_headers = {
42     "User-Agent"  : config.get("useragent"),
43     "Content-Type": "application/json",
44 }
45
46 # Exceptions to always catch
47 exceptions = (
48     requests.exceptions.ChunkedEncodingError,
49     requests.exceptions.ConnectionError,
50     requests.exceptions.InvalidSchema,
51     requests.exceptions.InvalidURL,
52     requests.exceptions.Timeout,
53     requests.exceptions.TooManyRedirects,
54     UnicodeEncodeError,
55     urllib3.exceptions.LocationParseError
56 )
57
58 def post_json_api(domain: str, path: str, data: str = "", headers: dict = dict()) -> dict:
59     logger.debug("domain='%s',path='%s',data='%s',headers()=%d - CALLED!", domain, path, data, len(headers))
60     domain_helper.raise_on(domain)
61
62     if not isinstance(path, str):
63         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
64     elif path == "":
65         raise ValueError("Parameter 'path' is empty")
66     elif not isinstance(data, str):
67         raise ValueError(f"data[]='{type(data)}' is not of type 'str'")
68     elif not isinstance(headers, dict):
69         raise ValueError(f"headers[]='{type(headers)}' is not of type 'list'")
70
71     json_reply = {
72         "status_code": 200,
73     }
74
75     try:
76         logger.debug("Sending POST to domain='%s',path='%s',data='%s',headers(%d)='%s'", domain, path, data, len(headers), headers)
77         response = reqto.post(
78             f"https://{domain}{path}",
79             data=data,
80             headers={**api_headers, **headers},
81             timeout=(config.get("connection_timeout"), config.get("read_timeout")),
82             cookies=cookies.get_all(domain),
83             allow_redirects=False
84         )
85
86         logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s'", response.ok, response.status_code, response.reason)
87         if response.ok and response.status_code == 200:
88             logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
89             json_reply["json"] = json_helper.from_response(response)
90
91         logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
92         if not response.ok or response.status_code >= 300 or len(response.text.strip()) == 0:
93             logger.debug("Cannot query JSON API: domain='%s',path='%s',data()=%d,response.status_code=%d,response.text()=%d", domain, path, len(data), response.status_code, len(response.text))
94             json_reply["status_code"]   = response.status_code
95             json_reply["error_message"] = response.reason
96             instances.set_last_error(domain, response)
97
98     except exceptions as exception:
99         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
100         json_reply["status_code"]   = 999
101         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
102         json_reply["exception"]     = exception
103         instances.set_last_error(domain, exception)
104         raise exception
105
106     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
107     return json_reply
108
109 def fetch_api_url(url: str, timeout: tuple) -> dict:
110     logger.debug("url='%s',timeout()=%d - CALLED!", url, len(timeout))
111     if not isinstance(url, str):
112         raise ValueError(f"Parameter url[]='{type(url)}' is not of type 'str'")
113     elif url == "":
114         raise ValueError("Parameter 'url' is empty")
115     elif not isinstance(timeout, tuple):
116         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
117
118     json_reply = {
119        "status_code": 200,
120     }
121
122     try:
123         logger.debug("Fetching url='%s' ...", url)
124         response = utils.fetch_url(url, api_headers, timeout)
125
126         logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s'", response.ok, response.status_code, response.reason)
127         if response.ok and response.status_code == 200:
128             logger.debug("Parsing JSON response from url='%s' ...", url)
129             json_reply["json"] = json_helper.from_response(response)
130
131         logger.debug("response.ok='%s',response.status_code='%s',response.text()=%d", response.ok, response.status_code, len(response.text))
132         if not response.ok or response.status_code >= 300 or len(response.text) == 0:
133             logger.warning("Cannot query JSON API: url='%s',response.status_code=%d,response.text()=%d", url, response.status_code, len(response.text))
134             json_reply["status_code"]   = response.status_code
135             json_reply["error_message"] = response.reason
136
137     except exceptions as exception:
138         logger.debug("Fetching url='%s' failed. exception[%s]='%s'", url, type(exception), str(exception))
139         json_reply["status_code"]   = 999
140         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
141         json_reply["exception"]     = exception
142         raise exception
143
144     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
145     return json_reply
146
147 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
148     logger.debug("domain='%s',path='%s',timeout()=%d - CALLED!", domain, path, len(timeout))
149     domain_helper.raise_on(domain)
150
151     if not isinstance(path, str):
152         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
153     elif path == "":
154         raise ValueError("Parameter 'path' is empty")
155     elif not isinstance(headers, dict):
156         raise ValueError(f"headers[]='{type(headers)}' is not of type 'list'")
157     elif not isinstance(timeout, tuple):
158         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
159
160     json_reply = {
161         "status_code": 200,
162     }
163
164     try:
165         logger.debug("Sending GET to domain='%s',path='%s',timeout(%d)='%s'", domain, path, len(timeout), timeout)
166         response = fetch_response(domain, path, {**api_headers, **headers}, timeout)
167     except exceptions as exception:
168         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
169         json_reply["status_code"]   = 999
170         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
171         json_reply["exception"]     = exception
172         instances.set_last_error(domain, exception)
173         raise exception
174
175     logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s'", response.ok, response.status_code, response.reason)
176     if response.ok and response.status_code == 200:
177         logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
178         json_reply["json"] = json_helper.from_response(response)
179
180     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
181     if not response.ok or response.status_code >= 300 or len(response.text) == 0:
182         logger.debug("Cannot query JSON API: domain='%s',path='%s',response.status_code=%d,response.text()=%d", domain, path, response.status_code, len(response.text))
183         json_reply["status_code"]   = response.status_code
184         json_reply["error_message"] = response.reason
185         instances.set_last_error(domain, response)
186
187     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
188     return json_reply
189
190 def send_bot_post(domain: str, blocklist: list):
191     logger.debug("domain='%s',blocklist()=%d - CALLED!", domain, len(blocklist))
192     domain_helper.raise_on(domain)
193
194     if not isinstance(blocklist, list):
195         raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not of type 'list'")
196     elif len(blocklist) == 0:
197         raise ValueError("Parameter 'blocklist' is empty")
198     elif config.get("bot_token") == "":
199         raise ValueError("config[bot_token] is not set")
200
201     message = f"{domain} has blocked the following instances:\n\n"
202     truncated = False
203
204     if len(blocklist) > 20:
205         truncated = True
206         blocklist = blocklist[0 : 19]
207
208     logger.debug("blocklist()=%d", len(blocklist))
209     for block in blocklist:
210         logger.debug("block[%s]='%s'", type(block), block)
211         if block["reason"] is None or block["reason"] == '':
212             logger.debug("block[blocked]='%s' is being blocked with no reason specified", block["blocked"])
213             message = message + block["blocked"] + " with unspecified reason\n"
214         else:
215             logger.debug("block[reason]()=%d", len(block["reason"]))
216             if len(block["reason"]) > 420:
217                 block["reason"] = block["reason"][0:419] + "[…]"
218
219             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
220
221     if truncated:
222         message = message + "(the list has been truncated to the first 20 entries)"
223
224     response = reqto.post(
225         f"{config.get('bot_instance')}/api/v1/statuses",
226         data={
227             "status"      : message,
228             "visibility"  : config.get("bot_visibility"),
229             "content_type": "text/plain"
230         },
231         headers={**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}},
232         timeout=(config.get("connection_timeout"), config.get("read_timeout")),
233         allow_redirects=False
234     )
235
236     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
237     return response.ok and response.status_code < 300 and response.text.strip() != ""
238
239 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple, allow_redirects: bool = False) -> requests.models.Response:
240     logger.debug("domain='%s',path='%s',headers()=%d,timeout='%s',allow_redirects='%s' - CALLED!", domain, path, len(headers), timeout, allow_redirects)
241     domain_helper.raise_on(domain)
242
243     if not isinstance(path, str):
244         raise ValueError(f"Parameter path[]='{type(path)}' is not of type 'str'")
245     elif path == "":
246         raise ValueError("Parameter 'path' is empty")
247     elif not isinstance(headers, dict):
248         raise ValueError(f"headers[]='{type(headers)}' is not of type 'dict'")
249     elif not isinstance(timeout, tuple):
250         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
251
252     try:
253         logger.debug("Sending GET request to '%s%s' ...", domain, path)
254         response = reqto.get(
255             f"https://{domain}{path}",
256             headers=headers,
257             timeout=timeout,
258             cookies=cookies.get_all(domain),
259             allow_redirects=allow_redirects
260         )
261
262     except exceptions as exception:
263         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
264         instances.set_last_error(domain, exception)
265         raise exception
266
267     logger.debug("response[]='%s' - EXIT!", type(response))
268     return response