]> 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 json
18 import reqto
19 import requests
20 import urllib3
21 import validators
22
23 from fba import fba
24
25 from fba.helpers import config
26 from fba.helpers import cookies
27
28 from fba.models import instances
29
30 # HTTP headers for non-API requests
31 web_headers = {
32     "User-Agent": config.get("useragent"),
33 }
34
35 # HTTP headers for API requests
36 api_headers = {
37     "User-Agent"  : config.get("useragent"),
38     "Content-Type": "application/json",
39 }
40
41 # Exceptions to always catch
42 exceptions = (
43     requests.exceptions.ChunkedEncodingError,
44     requests.exceptions.ConnectionError,
45     requests.exceptions.InvalidSchema,
46     requests.exceptions.InvalidURL,
47     requests.exceptions.Timeout,
48     requests.exceptions.TooManyRedirects,
49     UnicodeEncodeError,
50     urllib3.exceptions.LocationParseError
51 )
52
53 def post_json_api(domain: str, path: str, data: str = "", headers: dict = {}) -> dict:
54     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',data='{data}',headers()={len(headers)} - CALLED!")
55     if not isinstance(domain, str):
56         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
57     elif domain == "":
58         raise ValueError("Parameter 'domain' is empty")
59     elif not validators.domain(domain.split("/")[0]):
60         raise ValueError(f"domain='{domain}' is not a valid domain")
61     elif domain.endswith(".arpa"):
62         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
63     elif domain.endswith(".tld"):
64         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
65     elif not isinstance(path, str):
66         raise ValueError(f"path[]='{type(path)}' is not 'str'")
67     elif path == "":
68         raise ValueError("Parameter 'path' cannot be empty")
69     elif not isinstance(data, str):
70         raise ValueError(f"data[]='{type(data)}' is not 'str'")
71     elif not isinstance(headers, dict):
72         raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
73
74     json_reply = {
75         "status_code": 200,
76     }
77
78     try:
79         # DEBUG: print(f"DEBUG: Sending POST to domain='{domain}',path='{path}',data='{data}',headers({len(headers)})={headers}")
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) if cookies.has(domain) else {}
86         )
87
88         json_reply["json"] = json_from_response(response)
89
90         # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
91         if not response.ok or response.status_code >= 400:
92             print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',data()={len(data)},response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
93             json_reply["status_code"]   = response.status_code
94             json_reply["error_message"] = response.reason
95             del json_reply["json"]
96             instances.set_last_error(domain, response)
97
98     except exceptions as exception:
99         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{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     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
107     return json_reply
108
109 def fetch_api_url(url: str, timeout: tuple) -> dict:
110     # DEBUG: print(f"DEBUG: url='{url}',timeout()={len(timeout)} - CALLED!")
111     if not isinstance(url, str):
112         raise ValueError(f"Parameter url[]='{type(url)}' is not 'str'")
113     elif not isinstance(timeout, tuple):
114         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
115
116     json_reply = {
117        "status_code": 200,
118     }
119
120     try:
121         # DEBUG: print(f"DEBUG: Fetching url='{url}' ...")
122         response = fba.fetch_url(url, api_headers, timeout)
123
124         json_reply["json"] = json_from_response(response)
125
126         # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
127         if not response.ok or response.status_code >= 400:
128             print(f"WARNING: Cannot query JSON API: url='{url}',response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
129             json_reply["status_code"]   = response.status_code
130             json_reply["error_message"] = response.reason
131             del json_reply["json"]
132
133     except exceptions as exception:
134         # DEBUG: print(f"DEBUG: Fetching '{url}' failed. exception[{type(exception)}]='{str(exception)}'")
135         json_reply["status_code"]   = 999
136         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
137         json_reply["exception"]     = exception
138         raise exception
139
140     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
141     return json_reply
142
143 def get_json_api(domain: str, path: str, headers: dict, timeout: tuple) -> dict:
144     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',timeout()={len(timeout)} - CALLED!")
145     if not isinstance(domain, str):
146         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
147     elif domain == "":
148         raise ValueError("Parameter 'domain' is empty")
149     elif not validators.domain(domain.split("/")[0]):
150         raise ValueError(f"domain='{domain}' is not a valid domain")
151     elif domain.endswith(".arpa"):
152         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
153     elif domain.endswith(".tld"):
154         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
155     elif not isinstance(path, str):
156         raise ValueError(f"path[]='{type(path)}' is not 'str'")
157     elif path == "":
158         raise ValueError("Parameter 'path' cannot be empty")
159     elif not isinstance(headers, dict):
160         raise ValueError(f"headers[]='{type(headers)}' is not 'list'")
161     elif not isinstance(timeout, tuple):
162         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
163
164     json_reply = {
165         "status_code": 200,
166     }
167
168     try:
169         # DEBUG: print(f"DEBUG: Sending GET to domain='{domain}',path='{path}',timeout({len(timeout)})={timeout}")
170         response = reqto.get(
171             f"https://{domain}{path}",
172             headers={**api_headers, **headers},
173             timeout=timeout,
174             cookies=cookies.get_all(domain) if cookies.has(domain) else {}
175         )
176
177     except exceptions as exception:
178         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(exception)}]='{str(exception)}'")
179         json_reply["status_code"]   = 999
180         json_reply["error_message"] = f"exception['{type(exception)}']='{str(exception)}'"
181         json_reply["exception"]     = exception
182         instances.set_last_error(domain, exception)
183         raise exception
184
185     json_reply["json"] = json_from_response(response)
186
187     # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},json_reply[]='{type(json_reply)}'")
188     if not response.ok or response.status_code >= 400:
189         print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',response.status_code='{response.status_code}',json_reply[]='{type(json_reply)}'")
190         json_reply["status_code"]   = response.status_code
191         json_reply["error_message"] = response.reason
192         del json_reply["json"]
193         instances.set_last_error(domain, response)
194
195     # DEBUG: print(f"DEBUG: Returning json_reply({len(json_reply)})=[]:{type(json_reply)}")
196     return json_reply
197
198 def send_bot_post(domain: str, blocklist: dict):
199     # DEBUG: print(f"DEBUG: domain='{domain}',blocklist()={len(blocklist)} - CALLED!")
200     if not isinstance(domain, str):
201         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
202     elif domain == "":
203         raise ValueError("Parameter 'domain' is empty")
204     elif not validators.domain(domain.split("/")[0]):
205         raise ValueError(f"domain='{domain}' is not a valid domain")
206     elif domain.endswith(".arpa"):
207         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
208     elif domain.endswith(".tld"):
209         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
210     elif not isinstance(blocklist, dict):
211         raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'dict'")
212
213     message = f"{domain} has blocked the following instances:\n\n"
214     truncated = False
215
216     if len(blocklist) > 20:
217         truncated = True
218         blocklist = blocklist[0 : 19]
219
220     # DEBUG: print(f"DEBUG: blocklist()={len(blocklist)}")
221     for block in blocklist:
222         # DEBUG: print(f"DEBUG: block['{type(block)}']={block}")
223         if block["reason"] is None or block["reason"] == '':
224             message = message + block["blocked"] + " with unspecified reason\n"
225         else:
226             if len(block["reason"]) > 420:
227                 block["reason"] = block["reason"][0:419] + "[…]"
228
229             message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
230
231     if truncated:
232         message = message + "(the list has been truncated to the first 20 entries)"
233
234     botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
235
236     req = reqto.post(
237         f"{config.get('bot_instance')}/api/v1/statuses",
238         data={
239             "status"      : message,
240             "visibility"  : config.get('bot_visibility'),
241             "content_type": "text/plain"
242         },
243         headers=botheaders,
244         timeout=10
245     ).json()
246
247     return True
248
249 def fetch_response(domain: str, path: str, headers: dict, timeout: tuple) -> requests.models.Response:
250     # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',headers()={len(headers)},timeout={timeout} - CALLED!")
251     if not isinstance(domain, str):
252         raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
253     elif domain == "":
254         raise ValueError("Parameter 'domain' is empty")
255     elif not validators.domain(domain.split("/")[0]):
256         raise ValueError(f"domain='{domain}' is not a valid domain")
257     elif domain.endswith(".arpa"):
258         raise ValueError(f"domain='{domain}' is a domain for reversed IP addresses, please don't crawl them!")
259     elif domain.endswith(".tld"):
260         raise ValueError(f"domain='{domain}' is a fake domain, please don't crawl them!")
261     elif not isinstance(path, str):
262         raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
263     elif path == "":
264         raise ValueError("Parameter 'path' is empty")
265     elif not isinstance(headers, dict):
266         raise ValueError(f"headers[]='{type(headers)}' is not 'dict'")
267     elif not isinstance(timeout, tuple):
268         raise ValueError(f"timeout[]='{type(timeout)}' is not 'tuple'")
269
270     try:
271         # DEBUG: print(f"DEBUG: Sending GET request to '{domain}{path}' ...")
272         response = reqto.get(
273             f"https://{domain}{path}",
274             headers=headers,
275             timeout=timeout,
276             cookies=cookies.get_all(domain) if cookies.has(domain) else {}
277         )
278
279     except exceptions as exception:
280         # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(exception)}]='{str(exception)}'")
281         instances.set_last_error(domain, exception)
282         raise exception
283
284     # DEBUG: print(f"DEBUG: response[]='{type(response)}' - EXXIT!")
285     return response
286
287 def json_from_response(response: requests.models.Response) -> list:
288     # DEBUG: print(f"DEBUG: response[]='{type(response)}' - CALLED!")
289     if not isinstance(response, requests.models.Response):
290         raise ValueError(f"Parameter response[]='{type(response)}' is not type of 'Response'")
291
292     data = list()
293     if response.text.strip() != "":
294         # DEBUG: print(f"DEBUG: response.text()={len(response.text)} is not empty, invoking response.json() ...")
295         try:
296             data = response.json()
297         except json.decoder.JSONDecodeError:
298             pass
299
300     # DEBUG: print(f"DEBUG: data[]='{type(data)}' - EXIT!")
301     return data