plain 0.75.0__py3-none-any.whl → 0.76.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ #
4
+ #
5
+ # This file is part of gunicorn released under the MIT license.
6
+ # See the LICENSE for more information.
7
+ #
8
+ # Vendored and modified for Plain.
9
+ import base64
10
+ import binascii
11
+ import datetime
12
+ import logging
13
+ import time
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ logging.Logger.manager.emittedNoHandlerWarning = True # type: ignore[attr-defined]
17
+ import os # noqa: E402
18
+ import sys # noqa: E402
19
+ import threading # noqa: E402
20
+ import traceback # noqa: E402
21
+
22
+ from . import util # noqa: E402
23
+
24
+ if TYPE_CHECKING:
25
+ from io import TextIOWrapper
26
+
27
+ from .config import Config
28
+
29
+
30
+ def loggers() -> list[logging.Logger]:
31
+ """get list of all loggers"""
32
+ root = logging.root
33
+ existing = list(root.manager.loggerDict.keys())
34
+ return [logging.getLogger(name) for name in existing]
35
+
36
+
37
+ class SafeAtoms(dict[str, Any]):
38
+ def __init__(self, atoms: dict[str, Any]) -> None:
39
+ dict.__init__(self)
40
+ for key, value in atoms.items():
41
+ if isinstance(value, str):
42
+ self[key] = value.replace('"', '\\"')
43
+ else:
44
+ self[key] = value
45
+
46
+ def __getitem__(self, k: str) -> Any:
47
+ if k.startswith("{"):
48
+ kl = k.lower()
49
+ if kl in self:
50
+ return super().__getitem__(kl)
51
+ else:
52
+ return "-"
53
+ if k in self:
54
+ return super().__getitem__(k)
55
+ else:
56
+ return "-"
57
+
58
+
59
+ class Logger:
60
+ LOG_LEVELS = {
61
+ "critical": logging.CRITICAL,
62
+ "error": logging.ERROR,
63
+ "warning": logging.WARNING,
64
+ "info": logging.INFO,
65
+ "debug": logging.DEBUG,
66
+ }
67
+ loglevel = logging.INFO
68
+
69
+ error_fmt = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s"
70
+ datefmt = r"[%Y-%m-%d %H:%M:%S %z]"
71
+
72
+ access_fmt = "%(message)s"
73
+ syslog_fmt = "[%(process)d] %(message)s"
74
+
75
+ atoms_wrapper_class = SafeAtoms
76
+
77
+ def __init__(self, cfg: Config) -> None:
78
+ self.error_log = logging.getLogger("plain.server.error")
79
+ self.error_log.propagate = False
80
+ self.access_log = logging.getLogger("plain.server.access")
81
+ self.access_log.propagate = False
82
+ self.error_handlers: list[logging.Handler] = []
83
+ self.access_handlers: list[logging.Handler] = []
84
+ self.logfile: TextIOWrapper | None = None
85
+ self.lock = threading.Lock()
86
+ self.cfg = cfg
87
+ self.setup(cfg)
88
+
89
+ def setup(self, cfg: Config) -> None:
90
+ self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO)
91
+ self.error_log.setLevel(self.loglevel)
92
+ self.access_log.setLevel(logging.INFO)
93
+
94
+ # set plain.server.error handler
95
+ self._set_handler(
96
+ self.error_log,
97
+ cfg.errorlog,
98
+ logging.Formatter(cfg.log_format, self.datefmt),
99
+ )
100
+
101
+ # set plain.server.access handler
102
+ if cfg.accesslog is not None:
103
+ self._set_handler(
104
+ self.access_log,
105
+ cfg.accesslog,
106
+ fmt=logging.Formatter(cfg.log_format, self.datefmt),
107
+ stream=sys.stdout,
108
+ )
109
+
110
+ def critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
111
+ self.error_log.critical(msg, *args, **kwargs)
112
+
113
+ def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
114
+ self.error_log.error(msg, *args, **kwargs)
115
+
116
+ def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
117
+ self.error_log.warning(msg, *args, **kwargs)
118
+
119
+ def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
120
+ self.error_log.info(msg, *args, **kwargs)
121
+
122
+ def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
123
+ self.error_log.debug(msg, *args, **kwargs)
124
+
125
+ def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
126
+ self.error_log.exception(msg, *args, **kwargs)
127
+
128
+ def log(self, lvl: int | str, msg: str, *args: Any, **kwargs: Any) -> None:
129
+ if isinstance(lvl, str):
130
+ lvl = self.LOG_LEVELS.get(lvl.lower(), logging.INFO)
131
+ self.error_log.log(lvl, msg, *args, **kwargs)
132
+
133
+ def atoms(
134
+ self,
135
+ resp: Any,
136
+ req: Any,
137
+ environ: dict[str, Any],
138
+ request_time: datetime.timedelta,
139
+ ) -> dict[str, Any]:
140
+ """Gets atoms for log formatting."""
141
+ status = resp.status
142
+ if isinstance(status, str):
143
+ status = status.split(None, 1)[0]
144
+ atoms = {
145
+ "h": environ.get("REMOTE_ADDR", "-"),
146
+ "l": "-",
147
+ "u": self._get_user(environ) or "-",
148
+ "t": self.now(),
149
+ "r": "{} {} {}".format(
150
+ environ["REQUEST_METHOD"],
151
+ environ["RAW_URI"],
152
+ environ["SERVER_PROTOCOL"],
153
+ ),
154
+ "s": status,
155
+ "m": environ.get("REQUEST_METHOD"),
156
+ "U": environ.get("PATH_INFO"),
157
+ "q": environ.get("QUERY_STRING"),
158
+ "H": environ.get("SERVER_PROTOCOL"),
159
+ "b": getattr(resp, "sent", None) is not None and str(resp.sent) or "-",
160
+ "B": getattr(resp, "sent", None),
161
+ "f": environ.get("HTTP_REFERER", "-"),
162
+ "a": environ.get("HTTP_USER_AGENT", "-"),
163
+ "T": request_time.seconds,
164
+ "D": (request_time.seconds * 1000000) + request_time.microseconds,
165
+ "M": (request_time.seconds * 1000) + int(request_time.microseconds / 1000),
166
+ "L": f"{request_time.seconds}.{request_time.microseconds:06d}",
167
+ "p": f"<{os.getpid()}>",
168
+ }
169
+
170
+ # add request headers
171
+ if hasattr(req, "headers"):
172
+ req_headers = req.headers
173
+ else:
174
+ req_headers = req
175
+
176
+ if hasattr(req_headers, "items"):
177
+ req_headers = req_headers.items()
178
+
179
+ atoms.update({f"{{{k.lower()}}}i": v for k, v in req_headers})
180
+
181
+ resp_headers = resp.headers
182
+ if hasattr(resp_headers, "items"):
183
+ resp_headers = resp_headers.items()
184
+
185
+ # add response headers
186
+ atoms.update({f"{{{k.lower()}}}o": v for k, v in resp_headers})
187
+
188
+ # add environ variables
189
+ environ_variables = environ.items()
190
+ atoms.update({f"{{{k.lower()}}}e": v for k, v in environ_variables})
191
+
192
+ return atoms
193
+
194
+ def access(
195
+ self,
196
+ resp: Any,
197
+ req: Any,
198
+ environ: dict[str, Any],
199
+ request_time: datetime.timedelta,
200
+ ) -> None:
201
+ """See http://httpd.apache.org/docs/2.0/logs.html#combined
202
+ for format details
203
+ """
204
+
205
+ if not self.cfg.accesslog:
206
+ return None
207
+
208
+ # wrap atoms:
209
+ # - make sure atoms will be test case insensitively
210
+ # - if atom doesn't exist replace it by '-'
211
+ safe_atoms = self.atoms_wrapper_class(
212
+ self.atoms(resp, req, environ, request_time)
213
+ )
214
+
215
+ try:
216
+ self.access_log.info(self.cfg.access_log_format, safe_atoms)
217
+ except Exception:
218
+ self.error(traceback.format_exc())
219
+
220
+ return None
221
+
222
+ def now(self) -> str:
223
+ """return date in Apache Common Log Format"""
224
+ return time.strftime("[%d/%b/%Y:%H:%M:%S %z]")
225
+
226
+ def reopen_files(self) -> None:
227
+ for log in loggers():
228
+ for handler in log.handlers:
229
+ if isinstance(handler, logging.FileHandler):
230
+ handler.acquire()
231
+ try:
232
+ if handler.stream:
233
+ handler.close()
234
+ handler.stream = handler._open()
235
+ finally:
236
+ handler.release()
237
+
238
+ def close_on_exec(self) -> None:
239
+ for log in loggers():
240
+ for handler in log.handlers:
241
+ if isinstance(handler, logging.FileHandler):
242
+ handler.acquire()
243
+ try:
244
+ if handler.stream:
245
+ util.close_on_exec(handler.stream.fileno())
246
+ finally:
247
+ handler.release()
248
+
249
+ def _get_plain_server_handler(self, log: logging.Logger) -> logging.Handler | None:
250
+ for h in log.handlers:
251
+ if getattr(h, "_plain_server", False):
252
+ return h
253
+ return None
254
+
255
+ def _set_handler(
256
+ self,
257
+ log: logging.Logger,
258
+ output: str | None,
259
+ fmt: logging.Formatter,
260
+ stream: Any = None,
261
+ ) -> None:
262
+ # remove previous plain server log handler
263
+ h = self._get_plain_server_handler(log)
264
+ if h:
265
+ log.handlers.remove(h)
266
+
267
+ if output is not None:
268
+ if output == "-":
269
+ h = logging.StreamHandler(stream)
270
+ else:
271
+ util.check_is_writable(output)
272
+ h = logging.FileHandler(output)
273
+
274
+ h.setFormatter(fmt)
275
+ h._plain_server = True # type: ignore[attr-defined] # custom attribute
276
+ log.addHandler(h)
277
+
278
+ def _get_user(self, environ: dict[str, Any]) -> str | None:
279
+ user = None
280
+ http_auth = environ.get("HTTP_AUTHORIZATION")
281
+ if http_auth and http_auth.lower().startswith("basic"):
282
+ auth = http_auth.split(" ", 1)
283
+ if len(auth) == 2:
284
+ try:
285
+ # b64decode doesn't accept unicode in Python < 3.3
286
+ # so we need to convert it to a byte string
287
+ auth = base64.b64decode(auth[1].strip().encode("utf-8"))
288
+ # b64decode returns a byte string
289
+ user = auth.split(b":", 1)[0].decode("UTF-8")
290
+ except (TypeError, binascii.Error, UnicodeDecodeError) as exc:
291
+ self.debug("Couldn't get username: %s", exc)
292
+ return user
@@ -0,0 +1,12 @@
1
+ #
2
+ #
3
+ # This file is part of gunicorn released under the MIT license.
4
+ # See the LICENSE for more information.
5
+ #
6
+ # Vendored and modified for Plain.
7
+
8
+ from . import errors
9
+ from .message import Message, Request
10
+ from .parser import RequestParser
11
+
12
+ __all__ = ["Message", "Request", "RequestParser", "errors"]
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ #
4
+ #
5
+ # This file is part of gunicorn released under the MIT license.
6
+ # See the LICENSE for more information.
7
+ #
8
+ # Vendored and modified for Plain.
9
+ import io
10
+ import sys
11
+ from collections.abc import Generator, Iterator
12
+ from typing import TYPE_CHECKING
13
+
14
+ from .errors import ChunkMissingTerminator, InvalidChunkSize, NoMoreData
15
+
16
+ if TYPE_CHECKING:
17
+ from .message import Request
18
+ from .unreader import Unreader
19
+
20
+
21
+ class ChunkedReader:
22
+ def __init__(self, req: Request, unreader: Unreader) -> None:
23
+ self.req = req
24
+ self.parser: Generator[bytes, None, None] | None = self.parse_chunked(unreader)
25
+ self.buf = io.BytesIO()
26
+
27
+ def read(self, size: int) -> bytes:
28
+ if not isinstance(size, int):
29
+ raise TypeError("size must be an integer type")
30
+ if size < 0:
31
+ raise ValueError("Size must be positive.")
32
+ if size == 0:
33
+ return b""
34
+
35
+ if self.parser:
36
+ while self.buf.tell() < size:
37
+ try:
38
+ self.buf.write(next(self.parser))
39
+ except StopIteration:
40
+ self.parser = None
41
+ break
42
+
43
+ data = self.buf.getvalue()
44
+ ret, rest = data[:size], data[size:]
45
+ self.buf = io.BytesIO()
46
+ self.buf.write(rest)
47
+ return ret
48
+
49
+ def parse_trailers(self, unreader: Unreader, data: bytes) -> None:
50
+ buf = io.BytesIO()
51
+ buf.write(data)
52
+
53
+ idx = buf.getvalue().find(b"\r\n\r\n")
54
+ done = buf.getvalue()[:2] == b"\r\n"
55
+ while idx < 0 and not done:
56
+ self.get_data(unreader, buf)
57
+ idx = buf.getvalue().find(b"\r\n\r\n")
58
+ done = buf.getvalue()[:2] == b"\r\n"
59
+ if done:
60
+ unreader.unread(buf.getvalue()[2:])
61
+ return None
62
+ self.req.trailers = self.req.parse_headers(
63
+ buf.getvalue()[:idx], from_trailer=True
64
+ )
65
+ unreader.unread(buf.getvalue()[idx + 4 :])
66
+ return None
67
+
68
+ def parse_chunked(self, unreader: Unreader) -> Generator[bytes, None, None]:
69
+ (size, rest) = self.parse_chunk_size(unreader)
70
+ while size > 0:
71
+ while size > len(rest):
72
+ size -= len(rest)
73
+ yield rest
74
+ rest = unreader.read()
75
+ if not rest:
76
+ raise NoMoreData()
77
+ yield rest[:size]
78
+ # Remove \r\n after chunk
79
+ rest = rest[size:]
80
+ while len(rest) < 2:
81
+ new_data = unreader.read()
82
+ if not new_data:
83
+ break
84
+ rest += new_data
85
+ if rest[:2] != b"\r\n":
86
+ raise ChunkMissingTerminator(rest[:2])
87
+ (size, rest) = self.parse_chunk_size(unreader, data=rest[2:])
88
+
89
+ def parse_chunk_size(
90
+ self, unreader: Unreader, data: bytes | None = None
91
+ ) -> tuple[int, bytes]:
92
+ buf = io.BytesIO()
93
+ if data is not None:
94
+ buf.write(data)
95
+
96
+ idx = buf.getvalue().find(b"\r\n")
97
+ while idx < 0:
98
+ self.get_data(unreader, buf)
99
+ idx = buf.getvalue().find(b"\r\n")
100
+
101
+ data = buf.getvalue()
102
+ line, rest_chunk = data[:idx], data[idx + 2 :]
103
+
104
+ # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
105
+ chunk_size, *chunk_ext = line.split(b";", 1)
106
+ if chunk_ext:
107
+ chunk_size = chunk_size.rstrip(b" \t")
108
+ if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
109
+ raise InvalidChunkSize(chunk_size)
110
+ if len(chunk_size) == 0:
111
+ raise InvalidChunkSize(chunk_size)
112
+ chunk_size = int(chunk_size, 16)
113
+
114
+ if chunk_size == 0:
115
+ try:
116
+ self.parse_trailers(unreader, rest_chunk)
117
+ except NoMoreData:
118
+ pass
119
+ return (0, b"")
120
+ return (chunk_size, rest_chunk)
121
+
122
+ def get_data(self, unreader: Unreader, buf: io.BytesIO) -> None:
123
+ data = unreader.read()
124
+ if not data:
125
+ raise NoMoreData()
126
+ buf.write(data)
127
+ return None
128
+
129
+
130
+ class LengthReader:
131
+ def __init__(self, unreader: Unreader, length: int) -> None:
132
+ self.unreader = unreader
133
+ self.length = length
134
+
135
+ def read(self, size: int) -> bytes:
136
+ if not isinstance(size, int):
137
+ raise TypeError("size must be an integral type")
138
+
139
+ size = min(self.length, size)
140
+ if size < 0:
141
+ raise ValueError("Size must be positive.")
142
+ if size == 0:
143
+ return b""
144
+
145
+ buf = io.BytesIO()
146
+ data = self.unreader.read()
147
+ while data:
148
+ buf.write(data)
149
+ if buf.tell() >= size:
150
+ break
151
+ data = self.unreader.read()
152
+
153
+ buf_data = buf.getvalue()
154
+ ret, rest = buf_data[:size], buf_data[size:]
155
+ self.unreader.unread(rest)
156
+ self.length -= size
157
+ return ret
158
+
159
+
160
+ class EOFReader:
161
+ def __init__(self, unreader: Unreader) -> None:
162
+ self.unreader = unreader
163
+ self.buf = io.BytesIO()
164
+ self.finished = False
165
+
166
+ def read(self, size: int) -> bytes:
167
+ if not isinstance(size, int):
168
+ raise TypeError("size must be an integral type")
169
+ if size < 0:
170
+ raise ValueError("Size must be positive.")
171
+ if size == 0:
172
+ return b""
173
+
174
+ if self.finished:
175
+ data = self.buf.getvalue()
176
+ ret, rest = data[:size], data[size:]
177
+ self.buf = io.BytesIO()
178
+ self.buf.write(rest)
179
+ return ret
180
+
181
+ data = self.unreader.read()
182
+ while data:
183
+ self.buf.write(data)
184
+ if self.buf.tell() > size:
185
+ break
186
+ data = self.unreader.read()
187
+
188
+ if not data:
189
+ self.finished = True
190
+
191
+ data = self.buf.getvalue()
192
+ ret, rest = data[:size], data[size:]
193
+ self.buf = io.BytesIO()
194
+ self.buf.write(rest)
195
+ return ret
196
+
197
+
198
+ class Body:
199
+ def __init__(self, reader: ChunkedReader | LengthReader | EOFReader) -> None:
200
+ self.reader = reader
201
+ self.buf = io.BytesIO()
202
+
203
+ def __iter__(self) -> Iterator[bytes]:
204
+ return self
205
+
206
+ def __next__(self) -> bytes:
207
+ ret = self.readline()
208
+ if not ret:
209
+ raise StopIteration()
210
+ return ret
211
+
212
+ next = __next__
213
+
214
+ def getsize(self, size: int | None) -> int:
215
+ if size is None:
216
+ return sys.maxsize
217
+ elif not isinstance(size, int):
218
+ raise TypeError("size must be an integral type")
219
+ elif size < 0:
220
+ return sys.maxsize
221
+ return size
222
+
223
+ def read(self, size: int | None = None) -> bytes:
224
+ size = self.getsize(size)
225
+ if size == 0:
226
+ return b""
227
+
228
+ if size < self.buf.tell():
229
+ data = self.buf.getvalue()
230
+ ret, rest = data[:size], data[size:]
231
+ self.buf = io.BytesIO()
232
+ self.buf.write(rest)
233
+ return ret
234
+
235
+ while size > self.buf.tell():
236
+ data = self.reader.read(1024)
237
+ if not data:
238
+ break
239
+ self.buf.write(data)
240
+
241
+ data = self.buf.getvalue()
242
+ ret, rest = data[:size], data[size:]
243
+ self.buf = io.BytesIO()
244
+ self.buf.write(rest)
245
+ return ret
246
+
247
+ def readline(self, size: int | None = None) -> bytes:
248
+ size = self.getsize(size)
249
+ if size == 0:
250
+ return b""
251
+
252
+ data = self.buf.getvalue()
253
+ self.buf = io.BytesIO()
254
+
255
+ ret = []
256
+ while 1:
257
+ idx = data.find(b"\n", 0, size)
258
+ idx = idx + 1 if idx >= 0 else size if len(data) >= size else 0
259
+ if idx:
260
+ ret.append(data[:idx])
261
+ self.buf.write(data[idx:])
262
+ break
263
+
264
+ ret.append(data)
265
+ size -= len(data)
266
+ data = self.reader.read(min(1024, size))
267
+ if not data:
268
+ break
269
+
270
+ return b"".join(ret)
271
+
272
+ def readlines(self, size: int | None = None) -> list[bytes]:
273
+ ret = []
274
+ data = self.read()
275
+ while data:
276
+ pos = data.find(b"\n")
277
+ if pos < 0:
278
+ ret.append(data)
279
+ data = b""
280
+ else:
281
+ line, data = data[: pos + 1], data[pos + 1 :]
282
+ ret.append(line)
283
+ return ret