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