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