]> git.mxchange.org Git - fba.git/blob - fba/http/network.py
Continued:
[fba.git] / fba / http / 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 time
22 import urllib3
23
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 json as json_helper
30
31 from fba.models import instances
32
33 logging.basicConfig(level=logging.INFO)
34 logger = logging.getLogger(__name__)
35
36 # HTTP headers for non-API requests
37 web_headers = {
38     "User-Agent": config.get("useragent"),
39 }
40
41 # HTTP headers for API requests
42 api_headers = {
43     "User-Agent"  : config.get("useragent"),
44     "Content-Type": "application/json",
45 }
46
47 # Exceptions to always catch
48 exceptions = (
49     requests.exceptions.ChunkedEncodingError,
50     requests.exceptions.ConnectionError,
51     requests.exceptions.ContentDecodingError,
52     requests.exceptions.InvalidSchema,
53     requests.exceptions.InvalidURL,
54     requests.exceptions.Timeout,
55     requests.exceptions.TooManyRedirects,
56     UnicodeEncodeError,
57     urllib3.exceptions.LocationParseError
58 )
59
60 def post_json_api(domain: str, path: str, data: str = "", headers: dict = dict()) -> dict:
61     logger.debug("domain='%s',path='%s',data='%s',headers()=%d - CALLED!", domain, path, data, len(headers))
62     domain_helper.raise_on(domain)
63
64     if not isinstance(path, str):
65         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
66     elif path == "":
67         raise ValueError("Parameter 'path' is empty")
68     elif not isinstance(data, str):
69         raise ValueError(f"data[]='{type(data)}' is not of type 'str'")
70     elif not isinstance(headers, dict):
71         raise ValueError(f"headers[]='{type(headers)}' is not of type 'list'")
72
73     json_reply = {
74         "status_code": 200,
75     }
76
77     try:
78         logger.debug("Sending POST to domain='%s',path='%s',data='%s',headers(%d)='%s'", domain, path, data, len(headers), headers)
79         start = time.perf_counter()
80         response = reqto.post(
81             f"https://{domain}{path}",
82             data=data,
83             headers={**api_headers, **headers},
84             timeout=(config.get("connection_timeout"), config.get("read_timeout")),
85             cookies=cookies.get_all(domain),
86             allow_redirects=False
87         )
88         response_time = time.perf_counter() - start
89         logger.debug("response_time=%s", response_time)
90
91         instances.set_last_response_time(domain, response_time)
92
93         logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s',response_time=%s", response.ok, response.status_code, response.reason, response_time)
94         if response.ok and response.status_code == 200:
95             logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
96             json_reply["json"] = json_helper.from_response(response)
97
98         logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
99         if not response.ok or response.status_code > 200 or len(response.text.strip()) == 0:
100             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))
101             json_reply["status_code"]   = response.status_code
102             json_reply["error_message"] = response.reason
103             instances.set_last_error(domain, response)
104
105     except exceptions as exception:
106         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
107         json_reply["status_code"]   = 999
108         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
109         json_reply["exception"]     = exception
110         instances.set_last_error(domain, exception)
111         raise exception
112
113     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
114     return json_reply
115
116 def fetch_api_url(url: str, timeout: tuple) -> dict:
117     logger.debug("url='%s',timeout()=%d - CALLED!", url, len(timeout))
118
119     if not isinstance(url, str):
120         raise ValueError(f"Parameter url[]='{type(url)}' is not of type 'str'")
121     elif url == "":
122         raise ValueError("Parameter 'url' is empty")
123     elif not isinstance(timeout, tuple):
124         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
125
126     json_reply = {
127        "status_code": 200,
128     }
129
130     try:
131         logger.debug("Fetching url='%s' ...", url)
132         response = utils.fetch_url(url, api_headers, timeout)
133
134         logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s'", response.ok, response.status_code, response.reason)
135         if response.ok and response.status_code == 200:
136             logger.debug("Parsing JSON response from url='%s' ...", url)
137             json_reply["json"] = json_helper.from_response(response)
138
139         logger.debug("response.ok='%s',response.status_code='%s',response.text()=%d", response.ok, response.status_code, len(response.text))
140         if not response.ok or response.status_code > 200 or len(response.text) == 0:
141             logger.warning("Cannot query JSON API: url='%s',response.status_code=%d,response.text()=%d", url, response.status_code, len(response.text))
142             json_reply["status_code"]   = response.status_code
143             json_reply["error_message"] = response.reason
144
145     except exceptions as exception:
146         logger.debug("Fetching url='%s' failed. exception[%s]='%s'", url, type(exception), str(exception))
147         json_reply["status_code"]   = 999
148         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
149         json_reply["exception"]     = exception
150         raise exception
151
152     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
153     return json_reply
154
155 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
156     logger.debug("domain='%s',path='%s',timeout()=%d - CALLED!", domain, path, len(timeout))
157     domain_helper.raise_on(domain)
158
159     if not isinstance(path, str):
160         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
161     elif path == "":
162         raise ValueError("Parameter 'path' is empty")
163     elif not isinstance(headers, dict):
164         raise ValueError(f"headers[]='{type(headers)}' is not of type 'list'")
165     elif not isinstance(timeout, tuple):
166         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
167
168     json_reply = {
169         "status_code": 200,
170     }
171
172     try:
173         logger.debug("Sending GET to domain='%s',path='%s',timeout(%d)='%s'", domain, path, len(timeout), timeout)
174         response = fetch_response(domain, path, {**api_headers, **headers}, timeout)
175     except exceptions as exception:
176         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
177         json_reply["status_code"]   = 999
178         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
179         json_reply["exception"]     = exception
180         instances.set_last_error(domain, exception)
181         raise exception
182
183     logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s'", response.ok, response.status_code, response.reason)
184     if response.ok and response.status_code == 200:
185         logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
186         json_reply["json"] = json_helper.from_response(response)
187
188     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
189     if not response.ok or response.status_code > 200 or len(response.text) == 0:
190         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))
191         json_reply["status_code"]   = response.status_code
192         json_reply["error_message"] = response.reason
193         instances.set_last_error(domain, response)
194
195     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
196     return json_reply
197
198 def send_bot_post(domain: str, blocklist: list):
199     logger.debug("domain='%s',blocklist()=%d - CALLED!", domain, len(blocklist))
200     domain_helper.raise_on(domain)
201
202     if not isinstance(blocklist, list):
203         raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not of type 'list'")
204     elif len(blocklist) == 0:
205         raise ValueError("Parameter 'blocklist' is empty")
206     elif config.get("bot_token") == "":
207         raise ValueError("config[bot_token] is not set")
208
209     message = f"{domain} has blocked the following instances:\n\n"
210     truncated = False
211
212     if len(blocklist) > 20:
213         truncated = True
214         blocklist = blocklist[0 : 19]
215
216     logger.debug("blocklist()=%d", len(blocklist))
217     for block in blocklist:
218         logger.debug("block[%s]='%s'", type(block), block)
219         if block["reason"] is None or block["reason"] == '':
220             logger.debug("block[blocked]='%s' is being blocked with no reason specified", block["blocked"])
221             message = message + block["blocked"] + " with unspecified reason\n"
222         else:
223             logger.debug("block[reason]()=%d", len(block["reason"]))
224             if len(block["reason"]) > 420:
225                 block["reason"] = block["reason"][0:419] + "[…]"
226
227             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
228
229     if truncated:
230         message = message + "(the list has been truncated to the first 20 entries)"
231
232     response = reqto.post(
233         f"{config.get('bot_instance')}/api/v1/statuses",
234         data={
235             "status"      : message,
236             "visibility"  : config.get("bot_visibility"),
237             "content_type": "text/plain"
238         },
239         headers={**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}},
240         timeout=(config.get("connection_timeout"), config.get("read_timeout")),
241         allow_redirects=False
242     )
243
244     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
245     return response.ok and response.status_code == 200 and response.text.strip() != ""
246
247 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple, allow_redirects: bool = False) -> requests.models.Response:
248     logger.debug("domain='%s',path='%s',headers()=%d,timeout='%s',allow_redirects='%s' - CALLED!", domain, path, len(headers), timeout, allow_redirects)
249     domain_helper.raise_on(domain)
250
251     if not isinstance(path, str):
252         raise ValueError(f"Parameter path[]='{type(path)}' is not of type 'str'")
253     elif path == "":
254         raise ValueError("Parameter 'path' is empty")
255     elif not isinstance(headers, dict):
256         raise ValueError(f"headers[]='{type(headers)}' is not of type 'dict'")
257     elif not isinstance(timeout, tuple):
258         raise ValueError(f"timeout[]='{type(timeout)}' is not of type 'tuple'")
259
260     try:
261         logger.debug("Sending GET request to '%s%s' ...", domain, path)
262         start = time.perf_counter()
263         response = reqto.get(
264             f"https://{domain}{path}",
265             headers=headers,
266             timeout=timeout,
267             cookies=cookies.get_all(domain),
268             allow_redirects=allow_redirects
269         )
270         response_time = time.perf_counter() - start
271         logger.debug("response_time=%s", response_time)
272
273         instances.set_last_response_time(domain, response_time)
274
275         logger.debug("response.ok='%s',response.status_code=%d,response.reason='%s',response_time=%s", response.ok, response.status_code, response.reason, response_time)
276
277     except exceptions as exception:
278         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
279         instances.set_last_error(domain, exception)
280         raise exception
281
282     logger.debug("response[]='%s' - EXIT!", type(response))
283     return response