Coverage for an_website/patches/__init__.py: 89.571%
163 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-16 19:56 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-16 19:56 +0000
1# This program is free software: you can redistribute it and/or modify
2# it under the terms of the GNU Affero General Public License as
3# published by the Free Software Foundation, either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU Affero General Public License for more details.
10#
11# You should have received a copy of the GNU Affero General Public License
12# along with this program. If not, see <https://www.gnu.org/licenses/>.
13# pylint: disable=protected-access
15"""Patches that improve everything."""
17from __future__ import annotations
19import asyncio
20import http.client
21import json as stdlib_json # pylint: disable=preferred-module
22import logging
23import os
24import sys
25from collections.abc import Callable
26from configparser import RawConfigParser
27from contextlib import suppress
28from importlib import import_module
29from pathlib import Path
30from threading import Thread
31from types import MethodType
32from typing import Any
33from urllib.parse import urlsplit
35import certifi
36import defusedxml # type: ignore[import-untyped]
37import jsonpickle # type: ignore[import-untyped]
38import orjson
39import pycurl
40import tornado.httputil
41import yaml
42from emoji import EMOJI_DATA
43from pillow_jxl import JpegXLImagePlugin # noqa: F401
44from setproctitle import setthreadtitle
45from tornado.httpclient import AsyncHTTPClient, HTTPRequest
46from tornado.httputil import HTTPFile, HTTPHeaders, HTTPServerRequest
47from tornado.log import gen_log
48from tornado.web import GZipContentEncoding, RedirectHandler, RequestHandler
50from .. import CA_BUNDLE_PATH, MEDIA_TYPES
51from . import braille, json # noqa: F401 # pylint: disable=reimported
54def apply() -> None:
55 """Improve."""
56 patch_asyncio()
57 patch_certifi()
58 patch_configparser()
59 patch_emoji()
60 patch_http()
61 patch_json()
62 patch_jsonpickle()
63 patch_threading()
64 patch_xml()
66 patch_tornado_418()
67 patch_tornado_arguments()
68 patch_tornado_gzip()
69 patch_tornado_httpclient()
70 patch_tornado_logs()
71 patch_tornado_redirect()
74def patch_asyncio() -> None:
75 """Make stuff faster."""
76 if os.environ.get("DISABLE_UVLOOP") not in {
77 "y", "yes", "t", "true", "on", "1" # fmt: skip
78 }:
79 with suppress(ModuleNotFoundError):
80 asyncio.set_event_loop_policy(
81 import_module("uvloop").EventLoopPolicy()
82 )
85def patch_certifi() -> None:
86 """Make everything use our CA bundle."""
87 certifi.where = lambda: CA_BUNDLE_PATH
88 certifi.contents = lambda: Path(certifi.where()).read_text("ASCII")
91def patch_configparser() -> None:
92 """Make configparser funky."""
93 RawConfigParser.BOOLEAN_STATES.update( # type: ignore[attr-defined]
94 {
95 "sure": True,
96 "nope": False,
97 "accept": True,
98 "reject": False,
99 "enabled": True,
100 "disabled": False,
101 }
102 )
105def patch_emoji() -> None:
106 """Add cool new emoji."""
107 EMOJI_DATA["🐱\u200D💻"] = {
108 "de": ":hacker_katze:",
109 "en": ":hacker_cat:",
110 "status": 2,
111 "E": 1,
112 }
113 for de_name, en_name, rect in (
114 ("rot", "red", "🟥"),
115 ("blau", "blue", "🟦"),
116 ("orang", "orange", "🟧"),
117 ("gelb", "yellow", "🟨"),
118 ("grün", "green", "🟩"),
119 ("lilan", "purple", "🟪"),
120 ("braun", "brown", "🟫"),
121 ):
122 EMOJI_DATA[f"🫙\u200D{rect}"] = {
123 "de": f":{de_name}es_glas:",
124 "en": f":{en_name}_jar:",
125 "status": 2,
126 "E": 14,
127 }
128 EMOJI_DATA[f"🏳\uFE0F\u200D{rect}"] = {
129 "de": f":{de_name}e_flagge:",
130 "en": f":{en_name}_flag:",
131 "status": 2,
132 "E": 11,
133 }
134 EMOJI_DATA[f"\u2691\uFE0F\u200D{rect}"] = {
135 "de": f":tief{de_name}e_flagge:",
136 "en": f":deep_{en_name}_flag:",
137 "status": 2,
138 "E": 11,
139 }
142def patch_http() -> None:
143 """Add response code 420."""
144 http.client.responses[420] = "Enhance Your Calm"
147def patch_json() -> None:
148 """Replace json with orjson."""
149 if getattr(stdlib_json, "_omegajson", False) or sys.version_info < (3, 12):
150 return
151 stdlib_json.dumps = json.dumps
152 stdlib_json.dump = json.dump # type: ignore[assignment]
153 stdlib_json.loads = json.loads # type: ignore[assignment]
154 stdlib_json.load = json.load
157def patch_jsonpickle() -> None:
158 """Make jsonpickle return bytes."""
159 jsonpickle.load_backend("orjson")
160 jsonpickle.set_preferred_backend("orjson")
161 jsonpickle.enable_fallthrough(False)
164def patch_threading() -> None:
165 """Set thread names."""
166 _bootstrap = Thread._bootstrap # type: ignore[attr-defined]
168 def bootstrap(self: Thread) -> None:
169 with suppress(Exception):
170 setthreadtitle(self.name)
171 _bootstrap(self)
173 Thread._bootstrap = bootstrap # type: ignore[attr-defined]
176def patch_tornado_418() -> None:
177 """Add support for RFC 7168."""
178 RequestHandler.SUPPORTED_METHODS += ( # type: ignore[assignment]
179 "PROPFIND",
180 "BREW",
181 "WHEN",
182 )
183 _ = RequestHandler._unimplemented_method
184 RequestHandler.propfind = _ # type: ignore[attr-defined]
185 RequestHandler.brew = _ # type: ignore[attr-defined]
186 RequestHandler.when = _ # type: ignore[attr-defined]
189def patch_tornado_arguments() -> None: # noqa: C901
190 """Improve argument parsing."""
191 # pylint: disable=too-complex
193 def ensure_bytes(value: Any) -> bytes:
194 """Return the value as bytes."""
195 if isinstance(value, bool):
196 return b"true" if value else b"false"
197 if isinstance(value, bytes):
198 return value
199 return str(value).encode("UTF-8")
201 def parse_body_arguments(
202 content_type: str,
203 body: bytes,
204 arguments: dict[str, list[bytes]],
205 files: dict[str, list[HTTPFile]],
206 headers: None | HTTPHeaders = None,
207 *,
208 _: Callable[..., None] = tornado.httputil.parse_body_arguments,
209 ) -> None:
210 # pylint: disable=too-many-branches
211 if content_type.startswith("application/json"):
212 if headers and "Content-Encoding" in headers:
213 gen_log.warning(
214 "Unsupported Content-Encoding: %s",
215 headers["Content-Encoding"],
216 )
217 return
218 try:
219 spam = orjson.loads(body)
220 except Exception as exc: # pylint: disable=broad-except
221 gen_log.warning("Invalid JSON body: %s", exc)
222 else:
223 for key, value in spam.items():
224 if value is not None:
225 arguments.setdefault(key, []).append(
226 ensure_bytes(value)
227 )
228 elif content_type.startswith("application/yaml"):
229 if headers and "Content-Encoding" in headers:
230 gen_log.warning(
231 "Unsupported Content-Encoding: %s",
232 headers["Content-Encoding"],
233 )
234 return
235 try:
236 spam = yaml.safe_load(body)
237 except Exception as exc: # pylint: disable=broad-except
238 gen_log.warning("Invalid YAML body: %s", exc)
239 else:
240 for key, value in spam.items():
241 if value is not None:
242 arguments.setdefault(key, []).append(
243 ensure_bytes(value)
244 )
245 else:
246 _(content_type, body, arguments, files, headers)
248 parse_body_arguments.__doc__ = tornado.httputil.parse_body_arguments.__doc__
250 tornado.httputil.parse_body_arguments = parse_body_arguments
253def patch_tornado_gzip() -> None:
254 """Use gzip for more content types."""
255 GZipContentEncoding.CONTENT_TYPES = {
256 type for type, data in MEDIA_TYPES.items() if data.get("compressible")
257 }
260def patch_tornado_httpclient() -> None: # fmt: off
261 """Make requests quick."""
262 BACON = 0x75800 # noqa: N806 # pylint: disable=invalid-name
263 EGGS = 1 << 25 # noqa: N806 # pylint: disable=invalid-name
265 AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
267 def prepare_curl_callback(self: HTTPRequest, curl: pycurl.Curl) -> None:
268 # pylint: disable=c-extension-no-member, useless-suppression
269 if urlsplit(self.url).scheme == "https": # noqa: SIM102
270 if (ver := pycurl.version_info())[2] >= BACON and ver[4] & EGGS:
271 curl.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_3) # type: ignore[attr-defined] # noqa: B950
273 original_request_init = HTTPRequest.__init__
275 def request_init(self: HTTPRequest, *args: Any, **kwargs: Any) -> None:
276 if len(args) < 18: # there are too many positional arguments here
277 prepare_curl_method = MethodType(prepare_curl_callback, self)
278 kwargs.setdefault("prepare_curl_callback", prepare_curl_method)
279 original_request_init(self, *args, **kwargs)
281 request_init.__doc__ = HTTPRequest.__init__.__doc__
283 HTTPRequest.__init__ = request_init # type: ignore[method-assign]
286def patch_tornado_logs() -> None:
287 """Anonymize Tornado logs."""
288 # pylint: disable=import-outside-toplevel
289 from ..utils.utils import SUS_PATHS, anonymize_ip
291 RequestHandler._request_summary = ( # type: ignore[method-assign]
292 lambda self: "%s %s (%s)" # pylint: disable=consider-using-f-string
293 % (
294 self.request.method,
295 self.request.uri,
296 (
297 self.request.remote_ip
298 if self.request.path == "/robots.txt"
299 or self.request.path.lower() in SUS_PATHS
300 else anonymize_ip(self.request.remote_ip, ignore_invalid=True)
301 ),
302 )
303 )
305 HTTPServerRequest.__repr__ = ( # type: ignore[method-assign]
306 lambda self: "%s(%s)" # pylint: disable=consider-using-f-string
307 % (
308 self.__class__.__name__,
309 ", ".join(
310 [
311 "%s=%r" # pylint: disable=consider-using-f-string
312 % (
313 n,
314 getattr(self, n),
315 )
316 for n in ("protocol", "host", "method", "uri", "version")
317 ]
318 ),
319 )
320 )
323def patch_tornado_redirect() -> None:
324 """Use modern redirect codes and support HEAD requests."""
326 def redirect(
327 self: RequestHandler,
328 url: str,
329 permanent: bool = False,
330 status: None | int = None,
331 ) -> None:
332 if url == self.request.full_url():
333 logging.getLogger(
334 f"{self.__class__.__module__}.{self.__class__.__qualname__}"
335 ).critical("Infinite redirect to %r detected", url)
336 if self._headers_written:
337 # pylint: disable=broad-exception-raised
338 raise Exception("Cannot redirect after headers have been written")
339 if status is None:
340 status = 308 if permanent else 307
341 else:
342 assert isinstance(status, int) and 300 <= status <= 399 # type: ignore[redundant-expr] # noqa: B950
343 self.set_status(status)
344 self.set_header("Location", url)
345 self.finish() # type: ignore[unused-awaitable]
347 if RequestHandler.redirect.__doc__:
348 # fmt: off
349 redirect.__doc__ = (
350 RequestHandler.redirect.__doc__
351 .replace("301", "308")
352 .replace("302", "307")
353 )
354 # fmt: on
356 RequestHandler.redirect = redirect # type: ignore[method-assign]
358 RedirectHandler.head = RedirectHandler.get
361def patch_xml() -> None:
362 """Make XML safer."""
363 defusedxml.defuse_stdlib()
364 defusedxml.xmlrpc.monkey_patch()