Coverage for an_website / patches / __init__.py: 89.412%
170 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 19:37 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 19:37 +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
34from warnings import catch_warnings, simplefilter
36import certifi
37import defusedxml # type: ignore[import-untyped]
38import jsonpickle # type: ignore[import-untyped]
39import orjson
40import pycurl
41import tornado.httputil
42import yaml
43from emoji import EMOJI_DATA
44from pillow_jxl import JpegXLImagePlugin # noqa: F401
45from setproctitle import setthreadtitle
46from tornado.httpclient import AsyncHTTPClient, HTTPRequest
47from tornado.httputil import HTTPFile, HTTPHeaders, HTTPServerRequest
48from tornado.log import gen_log
49from tornado.web import GZipContentEncoding, RedirectHandler, RequestHandler
51from .. import CA_BUNDLE_PATH, MEDIA_TYPES
52from . import braille, json # noqa: F401 # pylint: disable=reimported
55def apply() -> None:
56 """Improve."""
57 patch_asyncio()
58 patch_certifi()
59 patch_configparser()
60 patch_emoji()
61 patch_http()
62 patch_json()
63 patch_jsonpickle()
64 patch_threading()
65 patch_xml()
67 patch_tornado_418()
68 patch_tornado_arguments()
69 patch_tornado_gzip()
70 patch_tornado_httpclient()
71 patch_tornado_logs()
72 patch_tornado_redirect()
75def patch_asyncio() -> None:
76 """Make stuff faster."""
77 if os.environ.get("DISABLE_UVLOOP") not in {
78 "y", "yes", "t", "true", "on", "1" # fmt: skip
79 }:
80 with catch_warnings():
81 simplefilter("ignore", DeprecationWarning)
82 with suppress(ModuleNotFoundError):
83 asyncio.set_event_loop_policy(
84 import_module("uvloop").EventLoopPolicy()
85 )
88def patch_certifi() -> None:
89 """Make everything use our CA bundle."""
90 certifi.where = lambda: CA_BUNDLE_PATH
91 certifi.contents = lambda: Path(certifi.where()).read_text("ASCII")
94def patch_configparser() -> None:
95 """Make configparser funky."""
96 RawConfigParser.BOOLEAN_STATES.update( # type: ignore[attr-defined]
97 {
98 "sure": True,
99 "nope": False,
100 "accept": True,
101 "reject": False,
102 "enabled": True,
103 "disabled": False,
104 }
105 )
108def patch_emoji() -> None:
109 """Add cool new emoji."""
110 EMOJI_DATA["🐱\u200D💻"] = {
111 "de": ":hacker_katze:",
112 "en": ":hacker_cat:",
113 "status": 2,
114 "E": 1,
115 }
116 for de_name, en_name, rect in (
117 ("rot", "red", "🟥"),
118 ("blau", "blue", "🟦"),
119 ("orang", "orange", "🟧"),
120 ("gelb", "yellow", "🟨"),
121 ("grün", "green", "🟩"),
122 ("lilan", "purple", "🟪"),
123 ("braun", "brown", "🟫"),
124 ):
125 EMOJI_DATA[f"🫙\u200D{rect}"] = {
126 "de": f":{de_name}es_glas:",
127 "en": f":{en_name}_jar:",
128 "status": 2,
129 "E": 14,
130 }
131 EMOJI_DATA[f"🏳\uFE0F\u200D{rect}"] = {
132 "de": f":{de_name}e_flagge:",
133 "en": f":{en_name}_flag:",
134 "status": 2,
135 "E": 11,
136 }
137 EMOJI_DATA[f"\u2691\uFE0F\u200D{rect}"] = {
138 "de": f":tief{de_name}e_flagge:",
139 "en": f":deep_{en_name}_flag:",
140 "status": 2,
141 "E": 11,
142 }
145def patch_http() -> None:
146 """Add response code 420."""
147 http.client.responses[420] = "Enhance Your Calm"
150def patch_json() -> None:
151 """Replace json with orjson."""
152 if getattr(stdlib_json, "_omegajson", False) or sys.version_info < (3, 12):
153 return
154 stdlib_json.dumps = json.dumps
155 stdlib_json.dump = json.dump # type: ignore[assignment]
156 stdlib_json.loads = json.loads # type: ignore[assignment]
157 stdlib_json.load = json.load
160def patch_jsonpickle() -> None:
161 """Make jsonpickle return bytes."""
162 jsonpickle.load_backend("orjson")
163 jsonpickle.set_preferred_backend("orjson")
164 jsonpickle.enable_fallthrough(False)
167def patch_threading() -> None:
168 """Set thread names."""
169 _bootstrap = Thread._bootstrap # type: ignore[attr-defined]
171 def bootstrap(self: Thread) -> None:
172 with suppress(Exception):
173 setthreadtitle(self.name)
174 _bootstrap(self)
176 Thread._bootstrap = bootstrap # type: ignore[attr-defined]
179def patch_tornado_418() -> None:
180 """Add support for RFC 7168."""
181 RequestHandler.SUPPORTED_METHODS += (
182 "PROPFIND",
183 "BREW",
184 "WHEN",
185 )
186 _ = RequestHandler._unimplemented_method
187 RequestHandler.propfind = _ # type: ignore[attr-defined]
188 RequestHandler.brew = _ # type: ignore[attr-defined]
189 RequestHandler.when = _ # type: ignore[attr-defined]
192def patch_tornado_arguments() -> None: # noqa: C901
193 """Improve argument parsing."""
194 # pylint: disable=too-complex
196 def ensure_bytes(value: Any) -> bytes:
197 """Return the value as bytes."""
198 if isinstance(value, bool):
199 return b"true" if value else b"false"
200 if isinstance(value, bytes):
201 return value
202 return str(value).encode("UTF-8")
204 def parse_body_arguments(
205 content_type: str,
206 body: bytes,
207 arguments: dict[str, list[bytes]],
208 files: dict[str, list[HTTPFile]],
209 headers: None | HTTPHeaders = None,
210 *,
211 _: Callable[..., None] = tornado.httputil.parse_body_arguments,
212 ) -> None:
213 # pylint: disable=too-many-branches
214 if content_type.startswith("application/json"):
215 if headers and "Content-Encoding" in headers:
216 gen_log.warning(
217 "Unsupported Content-Encoding: %s",
218 headers["Content-Encoding"],
219 )
220 return
221 try:
222 spam = orjson.loads(body)
223 except Exception as exc: # pylint: disable=broad-except
224 gen_log.warning("Invalid JSON body: %s", exc)
225 else:
226 if not isinstance(spam, dict):
227 return
228 for key, value in spam.items():
229 if value is not None:
230 arguments.setdefault(key, []).append(
231 ensure_bytes(value)
232 )
233 elif content_type.startswith("application/yaml"):
234 if headers and "Content-Encoding" in headers:
235 gen_log.warning(
236 "Unsupported Content-Encoding: %s",
237 headers["Content-Encoding"],
238 )
239 return
240 try:
241 spam = yaml.safe_load(body)
242 except Exception as exc: # pylint: disable=broad-except
243 gen_log.warning("Invalid YAML body: %s", exc)
244 else:
245 if not isinstance(spam, dict):
246 return
247 for key, value in spam.items():
248 if value is not None:
249 arguments.setdefault(key, []).append(
250 ensure_bytes(value)
251 )
252 else:
253 _(content_type, body, arguments, files, headers)
255 parse_body_arguments.__doc__ = tornado.httputil.parse_body_arguments.__doc__
257 tornado.httputil.parse_body_arguments = parse_body_arguments
260def patch_tornado_gzip() -> None:
261 """Use gzip for more content types."""
262 GZipContentEncoding.CONTENT_TYPES = {
263 type for type, data in MEDIA_TYPES.items() if data.get("compressible")
264 }
267def patch_tornado_httpclient() -> None: # fmt: off
268 """Make requests quick."""
269 BACON = 0x75800 # noqa: N806 # pylint: disable=invalid-name
270 EGGS = 1 << 25 # noqa: N806 # pylint: disable=invalid-name
272 AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
274 def prepare_curl_callback(self: HTTPRequest, curl: pycurl.Curl) -> None:
275 # pylint: disable=c-extension-no-member, useless-suppression
276 if urlsplit(self.url).scheme == "https": # noqa: SIM102
277 if (ver := pycurl.version_info())[2] >= BACON and ver[4] & EGGS:
278 curl.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_3)
280 original_request_init = HTTPRequest.__init__
282 def request_init(self: HTTPRequest, *args: Any, **kwargs: Any) -> None:
283 if len(args) < 18: # there are too many positional arguments here
284 prepare_curl_method = MethodType(prepare_curl_callback, self)
285 kwargs.setdefault("prepare_curl_callback", prepare_curl_method)
286 original_request_init(self, *args, **kwargs)
288 request_init.__doc__ = HTTPRequest.__init__.__doc__
290 HTTPRequest.__init__ = request_init # type: ignore[method-assign]
293def patch_tornado_logs() -> None:
294 """Anonymize Tornado logs."""
295 # pylint: disable=import-outside-toplevel
296 from ..utils.utils import SUS_PATHS, anonymize_ip
298 RequestHandler._request_summary = ( # type: ignore[method-assign]
299 lambda self: "%s %s (%s)" # pylint: disable=consider-using-f-string
300 % (
301 self.request.method,
302 self.request.uri,
303 (
304 self.request.remote_ip
305 if self.request.path == "/robots.txt"
306 or self.request.path.lower() in SUS_PATHS
307 else anonymize_ip(self.request.remote_ip, ignore_invalid=True)
308 ),
309 )
310 )
312 HTTPServerRequest.__repr__ = ( # type: ignore[method-assign]
313 lambda self: "%s(%s)" # pylint: disable=consider-using-f-string
314 % (
315 self.__class__.__name__,
316 ", ".join(
317 [
318 "%s=%r" # pylint: disable=consider-using-f-string
319 % (
320 n,
321 getattr(self, n),
322 )
323 for n in ("protocol", "host", "method", "uri", "version")
324 ]
325 ),
326 )
327 )
330def patch_tornado_redirect() -> None:
331 """Use modern redirect codes and support HEAD requests."""
333 def redirect(
334 self: RequestHandler,
335 url: str,
336 permanent: bool = False,
337 status: None | int = None,
338 ) -> None:
339 if url == self.request.full_url():
340 logging.getLogger(
341 f"{self.__class__.__module__}.{self.__class__.__qualname__}"
342 ).critical("Infinite redirect to %r detected", url)
343 if self._headers_written:
344 # pylint: disable=broad-exception-raised
345 raise Exception("Cannot redirect after headers have been written")
346 if status is None:
347 status = 308 if permanent else 307
348 else:
349 assert isinstance(status, int) and 300 <= status <= 399 # type: ignore[redundant-expr] # noqa: B950
350 self.set_status(status)
351 self.set_header("Location", url)
352 self.finish() # type: ignore[unused-awaitable]
354 if RequestHandler.redirect.__doc__:
355 # fmt: off
356 redirect.__doc__ = (
357 RequestHandler.redirect.__doc__
358 .replace("301", "308")
359 .replace("302", "307")
360 )
361 # fmt: on
363 RequestHandler.redirect = redirect # type: ignore[method-assign]
365 RedirectHandler.head = RedirectHandler.get
368def patch_xml() -> None:
369 """Make XML safer."""
370 defusedxml.defuse_stdlib()
371 defusedxml.xmlrpc.monkey_patch()