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