]> 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 json
20 import reqto
21 import requests
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; charset=utf-8",
45 }
46
47 # Exceptions to always catch
48 exceptions = (
49     requests.exceptions.ChunkedEncodingError,
50     requests.exceptions.ConnectionError,
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 '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 'str'")
69     elif not isinstance(headers, dict):
70         raise ValueError(f"headers[]='{type(headers)}' is not '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("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
88         json_reply["json"] = json_helper.from_response(response)
89
90         logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
91         if not response.ok or response.status_code >= 300 or len(response.text.strip()) == 0:
92             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))
93             json_reply["status_code"]   = response.status_code
94             json_reply["error_message"] = response.reason
95             instances.set_last_error(domain, response)
96             del json_reply["json"]
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 '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 '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("Parsing JSON response from url='%s' ...", url)
127         json_reply["json"] = json_helper.from_response(response)
128
129         logger.debug("response.ok='%s',response.status_code='%s',response.text()=%d", response.ok, response.status_code, len(response.text))
130         if not response.ok or response.status_code >= 300 or len(response.text) == 0:
131             logger.warning("Cannot query JSON API: url='%s',response.status_code=%d,response.text()=%d", url, response.status_code, len(response.text))
132             json_reply["status_code"]   = response.status_code
133             json_reply["error_message"] = response.reason
134             del json_reply["json"]
135
136     except exceptions as exception:
137         logger.debug("Fetching url='%s' failed. exception[%s]='%s'", url, type(exception), str(exception))
138         json_reply["status_code"]   = 999
139         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
140         json_reply["exception"]     = exception
141         raise exception
142
143     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
144     return json_reply
145
146 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
147     logger.debug("domain='%s',path='%s',timeout()=%d - CALLED!", domain, path, len(timeout))
148     domain_helper.raise_on(domain)
149
150     if not isinstance(path, str):
151         raise ValueError(f"path[]='{type(path)}' is not 'str'")
152     elif path == "":
153         raise ValueError("Parameter 'path' is empty")
154     elif not isinstance(headers, dict):
155         raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
156     elif not isinstance(timeout, tuple):
157         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
158
159     json_reply = {
160         "status_code": 200,
161     }
162
163     try:
164         logger.debug("Sending GET to domain='%s',path='%s',timeout(%d)='%s'", domain, path, len(timeout), timeout)
165         response = reqto.get(
166             f"https://{domain}{path}",
167             headers={**api_headers, **headers},
168             timeout=timeout,
169             cookies=cookies.get_all(domain),
170             allow_redirects=False
171         )
172
173     except exceptions as exception:
174         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
175         json_reply["status_code"]   = 999
176         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
177         json_reply["exception"]     = exception
178         instances.set_last_error(domain, exception)
179         raise exception
180
181     logger.debug("Parsing JSON response from domain='%s',path='%s' ...", domain, path)
182     json_reply["json"] = json_helper.from_response(response)
183
184     logger.debug("response.ok='%s',response.status_code=%d,response.text()=%d", response.ok, response.status_code, len(response.text))
185     if not response.ok or response.status_code >= 300 or len(response.text) == 0:
186         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))
187         json_reply["status_code"]   = response.status_code
188         json_reply["error_message"] = response.reason
189         instances.set_last_error(domain, response)
190         del json_reply["json"]
191
192     logger.debug("Returning json_reply(%d)[]='%s' - EXIT!", len(json_reply), type(json_reply))
193     return json_reply
194
195 def send_bot_post(domain: str, blocklist: list):
196     logger.debug("domain='%s',blocklist()=%d - CALLED!", domain, len(blocklist))
197     domain_helper.raise_on(domain)
198
199     if not isinstance(blocklist, list):
200         raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'list'")
201     elif len(blocklist) == 0:
202         raise ValueError("Parameter 'blocklist' is empty")
203
204     message = f"{domain} has blocked the following instances:\n\n"
205     truncated = False
206
207     if len(blocklist) > 20:
208         truncated = True
209         blocklist = blocklist[0 : 19]
210
211     logger.debug("blocklist()=%d", len(blocklist))
212     for block in blocklist:
213         logger.debug("block[%s]='%s'", type(block), block)
214         if block["reason"] is None or block["reason"] == '':
215             message = message + block["blocked"] + " with unspecified reason\n"
216         else:
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     botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
226
227     req = reqto.post(
228         f"{config.get('bot_instance')}/api/v1/statuses",
229         data={
230             "status"      : message,
231             "visibility"  : config.get('bot_visibility'),
232             "content_type": "text/plain"
233         },
234         headers=botheaders,
235         timeout=(config.get("connection_timeout"), config.get("read_timeout")),
236         allow_redirects=False
237     ).json()
238
239     return True
240
241 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple, allow_redirects: bool = False) -> requests.models.Response:
242     logger.debug("domain='%s',path='%s',headers()=%d,timeout='%s',allow_redirects='%s' - CALLED!", domain, path, len(headers), timeout, allow_redirects)
243     domain_helper.raise_on(domain)
244
245     if not isinstance(path, str):
246         raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
247     elif path == "":
248         raise ValueError("Parameter 'path' is empty")
249     elif not isinstance(headers, dict):
250         raise ValueError(f"headers[]='{type(headers)}' is not 'dict'")
251     elif not isinstance(timeout, tuple):
252         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
253
254     try:
255         logger.debug("Sending GET request to '%s%s' ...", domain, path)
256         response = reqto.get(
257             f"https://{domain}{path}",
258             headers=headers,
259             timeout=timeout,
260             cookies=cookies.get_all(domain),
261             allow_redirects=allow_redirects
262         )
263
264     except exceptions as exception:
265         logger.debug("Fetching path='%s' from domain='%s' failed. exception[%s]='%s'", path, domain, type(exception), str(exception))
266         instances.set_last_error(domain, exception)
267         raise exception
268
269     logger.debug("response[]='%s' - EXIT!", type(response))
270     return response