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.
plain/server/sock.py ADDED
@@ -0,0 +1,219 @@
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 errno
10
+ import os
11
+ import socket
12
+ import ssl
13
+ import stat
14
+ import sys
15
+ import time
16
+ from typing import TYPE_CHECKING
17
+
18
+ from . import util
19
+
20
+ if TYPE_CHECKING:
21
+ from .config import Config
22
+ from .glogging import Logger
23
+
24
+ # Maximum number of pending connections in the socket listen queue
25
+ BACKLOG = 2048
26
+
27
+
28
+ class BaseSocket:
29
+ FAMILY: socket.AddressFamily
30
+
31
+ def __init__(
32
+ self,
33
+ address: tuple[str, int] | str,
34
+ conf: Config,
35
+ log: Logger,
36
+ fd: int | None = None,
37
+ ) -> None:
38
+ self.log = log
39
+ self.conf = conf
40
+
41
+ self.cfg_addr = address
42
+ if fd is None:
43
+ sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)
44
+ bound = False
45
+ else:
46
+ sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)
47
+ os.close(fd)
48
+ bound = True
49
+
50
+ self.sock: socket.socket | None = self.set_options(sock, bound=bound)
51
+
52
+ def __str__(self) -> str:
53
+ return f"<socket {self.sock.fileno()}>"
54
+
55
+ def __getattr__(self, name: str) -> object:
56
+ return getattr(self.sock, name)
57
+
58
+ def getsockname(self) -> tuple[str, int] | str:
59
+ return self.sock.getsockname()
60
+
61
+ def set_options(self, sock: socket.socket, bound: bool = False) -> socket.socket:
62
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
63
+ if not bound:
64
+ self.bind(sock)
65
+ sock.setblocking(False)
66
+
67
+ # make sure that the socket can be inherited
68
+ if hasattr(sock, "set_inheritable"):
69
+ sock.set_inheritable(True)
70
+
71
+ sock.listen(BACKLOG)
72
+ return sock
73
+
74
+ def bind(self, sock: socket.socket) -> None:
75
+ sock.bind(self.cfg_addr)
76
+
77
+ def close(self) -> None:
78
+ if self.sock is None:
79
+ return None
80
+
81
+ try:
82
+ self.sock.close()
83
+ except OSError as e:
84
+ self.log.info("Error while closing socket %s", str(e))
85
+
86
+ self.sock = None
87
+ return None
88
+
89
+
90
+ class TCPSocket(BaseSocket):
91
+ FAMILY = socket.AF_INET
92
+
93
+ def __str__(self) -> str:
94
+ if self.conf.is_ssl:
95
+ scheme = "https"
96
+ else:
97
+ scheme = "http"
98
+
99
+ addr = self.sock.getsockname()
100
+ return f"{scheme}://{addr[0]}:{addr[1]}"
101
+
102
+ def set_options(self, sock: socket.socket, bound: bool = False) -> socket.socket:
103
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
104
+ return super().set_options(sock, bound=bound)
105
+
106
+
107
+ class TCP6Socket(TCPSocket):
108
+ FAMILY = socket.AF_INET6
109
+
110
+ def __str__(self) -> str:
111
+ (host, port, _, _) = self.sock.getsockname()
112
+ return f"http://[{host}]:{port}"
113
+
114
+
115
+ class UnixSocket(BaseSocket):
116
+ FAMILY = socket.AF_UNIX
117
+
118
+ def __init__(
119
+ self,
120
+ addr: tuple[str, int] | str,
121
+ conf: Config,
122
+ log: Logger,
123
+ fd: int | None = None,
124
+ ):
125
+ if fd is None:
126
+ try:
127
+ st = os.stat(addr)
128
+ except OSError as e:
129
+ if e.args[0] != errno.ENOENT:
130
+ raise
131
+ else:
132
+ if stat.S_ISSOCK(st.st_mode):
133
+ os.remove(addr)
134
+ else:
135
+ raise ValueError(f"{addr!r} is not a socket")
136
+ super().__init__(addr, conf, log, fd=fd)
137
+
138
+ def __str__(self) -> str:
139
+ return f"unix:{self.cfg_addr}"
140
+
141
+ def bind(self, sock: socket.socket) -> None:
142
+ sock.bind(self.cfg_addr)
143
+
144
+
145
+ def _sock_type(addr: tuple[str, int] | str | bytes) -> type[BaseSocket]:
146
+ if isinstance(addr, tuple):
147
+ if util.is_ipv6(addr[0]):
148
+ sock_type = TCP6Socket
149
+ else:
150
+ sock_type = TCPSocket
151
+ elif isinstance(addr, str | bytes):
152
+ sock_type = UnixSocket
153
+ else:
154
+ raise TypeError(f"Unable to create socket from: {addr!r}")
155
+ return sock_type
156
+
157
+
158
+ def create_sockets(conf: Config, log: Logger) -> list[BaseSocket]:
159
+ """
160
+ Create a new socket for the configured addresses.
161
+
162
+ If a configured address is a tuple then a TCP socket is created.
163
+ If it is a string, a Unix socket is created. Otherwise, a TypeError is
164
+ raised.
165
+ """
166
+ listeners = []
167
+
168
+ # check ssl config early to raise the error on startup
169
+ # only the certfile is needed since it can contains the keyfile
170
+ if conf.certfile and not os.path.exists(conf.certfile):
171
+ raise ValueError(f'certfile "{conf.certfile}" does not exist')
172
+
173
+ if conf.keyfile and not os.path.exists(conf.keyfile):
174
+ raise ValueError(f'keyfile "{conf.keyfile}" does not exist')
175
+
176
+ for addr in conf.address:
177
+ sock_type = _sock_type(addr)
178
+ sock = None
179
+ for i in range(5):
180
+ try:
181
+ sock = sock_type(addr, conf, log)
182
+ except OSError as e:
183
+ if e.args[0] == errno.EADDRINUSE:
184
+ log.error("Connection in use: %s", str(addr))
185
+ if e.args[0] == errno.EADDRNOTAVAIL:
186
+ log.error("Invalid address: %s", str(addr))
187
+ msg = "connection to {addr} failed: {error}"
188
+ log.error(msg.format(addr=str(addr), error=str(e)))
189
+ if i < 5:
190
+ log.debug("Retrying in 1 second.")
191
+ time.sleep(1)
192
+ else:
193
+ break
194
+
195
+ if sock is None:
196
+ log.error("Can't connect to %s", str(addr))
197
+ sys.exit(1)
198
+
199
+ listeners.append(sock)
200
+
201
+ return listeners
202
+
203
+
204
+ def close_sockets(listeners: list[BaseSocket], unlink: bool = True) -> None:
205
+ for sock in listeners:
206
+ sock_name = sock.getsockname()
207
+ sock.close()
208
+ if unlink and _sock_type(sock_name) is UnixSocket:
209
+ os.unlink(sock_name)
210
+
211
+
212
+ def ssl_context(conf: Config) -> ssl.SSLContext:
213
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
214
+ context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile)
215
+ return context
216
+
217
+
218
+ def ssl_wrap_socket(sock: socket.socket, conf: Config) -> ssl.SSLSocket:
219
+ return ssl_context(conf).wrap_socket(sock, server_side=True)
plain/server/util.py ADDED
@@ -0,0 +1,380 @@
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 email.utils
10
+ import errno
11
+ import fcntl
12
+ import html
13
+ import importlib
14
+ import inspect
15
+ import io
16
+ import os
17
+ import random
18
+ import re
19
+ import socket
20
+ import sys
21
+ import textwrap
22
+ import time
23
+ import traceback
24
+ import urllib.parse
25
+ import warnings
26
+ from collections.abc import Callable
27
+ from typing import Any
28
+
29
+ from .workers import SUPPORTED_WORKERS
30
+
31
+ # Server and Date aren't technically hop-by-hop
32
+ # headers, but they are in the purview of the
33
+ # origin server which the WSGI spec says we should
34
+ # act like. So we drop them and add our own.
35
+ #
36
+ # In the future, concatenation server header values
37
+ # might be better, but nothing else does it and
38
+ # dropping them is easier.
39
+ hop_headers = set(
40
+ """
41
+ connection keep-alive proxy-authenticate proxy-authorization
42
+ te trailers transfer-encoding upgrade
43
+ server date
44
+ """.split()
45
+ )
46
+
47
+
48
+ def load_class(
49
+ uri: str | type,
50
+ default: str = "plain.server.workers.sync.SyncWorker",
51
+ section: str = "plain.server.workers",
52
+ ) -> type:
53
+ if inspect.isclass(uri):
54
+ return uri # type: ignore[return-value]
55
+
56
+ components = uri.split(".") # type: ignore[union-attr]
57
+ if len(components) == 1:
58
+ # Handle short names like "sync" or "gthread"
59
+ if uri.startswith("#"): # type: ignore[union-attr]
60
+ uri = uri[1:] # type: ignore[union-attr]
61
+
62
+ if uri in SUPPORTED_WORKERS:
63
+ components = SUPPORTED_WORKERS[uri].split(".")
64
+ else:
65
+ exc_msg = f"Worker type {uri!r} not found in SUPPORTED_WORKERS"
66
+ raise RuntimeError(exc_msg)
67
+
68
+ klass = components.pop(-1)
69
+
70
+ try:
71
+ mod = importlib.import_module(".".join(components))
72
+ except Exception:
73
+ exc = traceback.format_exc()
74
+ msg = "class uri %r invalid or not found: \n\n[%s]"
75
+ raise RuntimeError(msg % (uri, exc))
76
+ return getattr(mod, klass)
77
+
78
+
79
+ positionals = (
80
+ inspect.Parameter.POSITIONAL_ONLY,
81
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
82
+ )
83
+
84
+
85
+ def get_arity(f: Callable[..., Any]) -> int:
86
+ sig = inspect.signature(f)
87
+ arity = 0
88
+
89
+ for param in sig.parameters.values():
90
+ if param.kind in positionals:
91
+ arity += 1
92
+
93
+ return arity
94
+
95
+
96
+ if sys.platform.startswith("win"):
97
+
98
+ def _waitfor(
99
+ func: Callable[[str], None], pathname: str, waitall: bool = False
100
+ ) -> None:
101
+ # Perform the operation
102
+ func(pathname)
103
+ # Now setup the wait loop
104
+ if waitall:
105
+ dirname = pathname
106
+ else:
107
+ dirname, name = os.path.split(pathname)
108
+ dirname = dirname or "."
109
+ # Check for `pathname` to be removed from the filesystem.
110
+ # The exponential backoff of the timeout amounts to a total
111
+ # of ~1 second after which the deletion is probably an error
112
+ # anyway.
113
+ # Testing on a i7@4.3GHz shows that usually only 1 iteration is
114
+ # required when contention occurs.
115
+ timeout = 0.001
116
+ while timeout < 1.0:
117
+ # Note we are only testing for the existence of the file(s) in
118
+ # the contents of the directory regardless of any security or
119
+ # access rights. If we have made it this far, we have sufficient
120
+ # permissions to do that much using Python's equivalent of the
121
+ # Windows API FindFirstFile.
122
+ # Other Windows APIs can fail or give incorrect results when
123
+ # dealing with files that are pending deletion.
124
+ L = os.listdir(dirname)
125
+ if not L if waitall else name in L:
126
+ return None
127
+ # Increase the timeout and try again
128
+ time.sleep(timeout)
129
+ timeout *= 2
130
+ warnings.warn(
131
+ "tests may fail, delete still pending for " + pathname,
132
+ RuntimeWarning,
133
+ stacklevel=4,
134
+ )
135
+ return None
136
+
137
+ def _unlink(filename: str) -> None:
138
+ _waitfor(os.unlink, filename)
139
+ return None
140
+ else:
141
+ _unlink = os.unlink
142
+
143
+
144
+ def unlink(filename: str) -> None:
145
+ try:
146
+ _unlink(filename)
147
+ except OSError as error:
148
+ # The filename need not exist.
149
+ if error.errno not in (errno.ENOENT, errno.ENOTDIR):
150
+ raise
151
+
152
+
153
+ def is_ipv6(addr: str) -> bool:
154
+ try:
155
+ socket.inet_pton(socket.AF_INET6, addr)
156
+ except OSError: # not a valid address
157
+ return False
158
+ except ValueError: # ipv6 not supported on this platform
159
+ return False
160
+ return True
161
+
162
+
163
+ def parse_address(netloc: str, default_port: str = "8000") -> str | tuple[str, int]:
164
+ if re.match(r"unix:(//)?", netloc):
165
+ return re.split(r"unix:(//)?", netloc)[-1]
166
+
167
+ if netloc.startswith("tcp://"):
168
+ netloc = netloc.split("tcp://")[1]
169
+ host, port = netloc, default_port
170
+
171
+ if "[" in netloc and "]" in netloc:
172
+ host = netloc.split("]")[0][1:]
173
+ port = (netloc.split("]:") + [default_port])[1]
174
+ elif ":" in netloc:
175
+ host, port = (netloc.split(":") + [default_port])[:2]
176
+ elif netloc == "":
177
+ host, port = "0.0.0.0", default_port
178
+
179
+ try:
180
+ port = int(port)
181
+ except ValueError:
182
+ raise RuntimeError(f"{port!r} is not a valid port number.")
183
+
184
+ return host.lower(), port
185
+
186
+
187
+ def close_on_exec(fd: int) -> None:
188
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
189
+ flags |= fcntl.FD_CLOEXEC
190
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags)
191
+
192
+
193
+ def set_non_blocking(fd: int) -> None:
194
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
195
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
196
+
197
+
198
+ def close(sock: socket.socket) -> None:
199
+ try:
200
+ sock.close()
201
+ except OSError:
202
+ pass
203
+
204
+
205
+ def write_chunk(sock: socket.socket, data: str | bytes) -> None:
206
+ if isinstance(data, str):
207
+ data = data.encode("utf-8")
208
+ chunk_size = f"{len(data):X}\r\n"
209
+ chunk = b"".join([chunk_size.encode("utf-8"), data, b"\r\n"])
210
+ sock.sendall(chunk)
211
+
212
+
213
+ def write(sock: socket.socket, data: str | bytes, chunked: bool = False) -> None:
214
+ if chunked:
215
+ return write_chunk(sock, data)
216
+ sock.sendall(data)
217
+
218
+
219
+ def write_nonblock(
220
+ sock: socket.socket, data: str | bytes, chunked: bool = False
221
+ ) -> None:
222
+ timeout = sock.gettimeout()
223
+ if timeout != 0.0:
224
+ try:
225
+ sock.setblocking(False)
226
+ return write(sock, data, chunked)
227
+ finally:
228
+ sock.setblocking(True)
229
+ else:
230
+ return write(sock, data, chunked)
231
+
232
+
233
+ def write_error(sock: socket.socket, status_int: int, reason: str, mesg: str) -> None:
234
+ html_error = textwrap.dedent("""\
235
+ <html>
236
+ <head>
237
+ <title>%(reason)s</title>
238
+ </head>
239
+ <body>
240
+ <h1><p>%(reason)s</p></h1>
241
+ %(mesg)s
242
+ </body>
243
+ </html>
244
+ """) % {"reason": reason, "mesg": html.escape(mesg)}
245
+
246
+ http = textwrap.dedent("""\
247
+ HTTP/1.1 %s %s\r
248
+ Connection: close\r
249
+ Content-Type: text/html\r
250
+ Content-Length: %d\r
251
+ \r
252
+ %s""") % (str(status_int), reason, len(html_error), html_error)
253
+ write_nonblock(sock, http.encode("latin1"))
254
+
255
+
256
+ def getcwd() -> str:
257
+ # get current path, try to use PWD env first
258
+ try:
259
+ a = os.stat(os.environ["PWD"])
260
+ b = os.stat(os.getcwd())
261
+ if a.st_ino == b.st_ino and a.st_dev == b.st_dev:
262
+ cwd = os.environ["PWD"]
263
+ else:
264
+ cwd = os.getcwd()
265
+ except Exception:
266
+ cwd = os.getcwd()
267
+ return cwd
268
+
269
+
270
+ def http_date(timestamp: float | None = None) -> str:
271
+ """Return the current date and time formatted for a message header."""
272
+ if timestamp is None:
273
+ timestamp = time.time()
274
+ s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)
275
+ return s
276
+
277
+
278
+ def is_hoppish(header: str) -> bool:
279
+ return header.lower().strip() in hop_headers
280
+
281
+
282
+ def seed() -> None:
283
+ try:
284
+ random.seed(os.urandom(64))
285
+ except NotImplementedError:
286
+ random.seed(f"{time.time()}.{os.getpid()}")
287
+
288
+
289
+ def check_is_writable(path: str) -> None:
290
+ try:
291
+ with open(path, "a") as f:
292
+ f.close()
293
+ except OSError as e:
294
+ raise RuntimeError(f"Error: '{path}' isn't writable [{e!r}]")
295
+
296
+
297
+ def to_bytestring(value: str | bytes, encoding: str = "utf8") -> bytes:
298
+ """Converts a string argument to a byte string"""
299
+ if isinstance(value, bytes):
300
+ return value
301
+ if not isinstance(value, str):
302
+ raise TypeError(f"{value!r} is not a string")
303
+
304
+ return value.encode(encoding)
305
+
306
+
307
+ def has_fileno(obj: Any) -> bool:
308
+ if not hasattr(obj, "fileno"):
309
+ return False
310
+
311
+ # check BytesIO case and maybe others
312
+ try:
313
+ obj.fileno()
314
+ except (AttributeError, OSError, io.UnsupportedOperation):
315
+ return False
316
+
317
+ return True
318
+
319
+
320
+ def warn(msg: str) -> None:
321
+ print("!!!", file=sys.stderr)
322
+
323
+ lines = msg.splitlines()
324
+ for i, line in enumerate(lines):
325
+ if i == 0:
326
+ line = f"WARNING: {line}"
327
+ print(f"!!! {line}", file=sys.stderr)
328
+
329
+ print("!!!\n", file=sys.stderr)
330
+ sys.stderr.flush()
331
+
332
+
333
+ def make_fail_app(msg: str | bytes) -> Callable[..., Any]:
334
+ msg = to_bytestring(msg)
335
+
336
+ def app(environ: Any, start_response: Any) -> list[bytes]:
337
+ start_response(
338
+ "500 Internal Server Error",
339
+ [("Content-Type", "text/plain"), ("Content-Length", str(len(msg)))],
340
+ )
341
+ return [msg]
342
+
343
+ return app
344
+
345
+
346
+ def split_request_uri(uri: str) -> urllib.parse.SplitResult:
347
+ if uri.startswith("//"):
348
+ # When the path starts with //, urlsplit considers it as a
349
+ # relative uri while the RFC says we should consider it as abs_path
350
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
351
+ # We use temporary dot prefix to workaround this behaviour
352
+ parts = urllib.parse.urlsplit("." + uri)
353
+ return parts._replace(path=parts.path[1:])
354
+
355
+ return urllib.parse.urlsplit(uri)
356
+
357
+
358
+ # From six.reraise
359
+ def reraise(
360
+ tp: type[BaseException], value: BaseException | None, tb: Any = None
361
+ ) -> None:
362
+ try:
363
+ if value is None:
364
+ value = tp()
365
+ if value.__traceback__ is not tb:
366
+ raise value.with_traceback(tb)
367
+ raise value
368
+ finally:
369
+ value = None
370
+ tb = None
371
+
372
+
373
+ def bytes_to_str(b: str | bytes) -> str:
374
+ if isinstance(b, str):
375
+ return b
376
+ return str(b, "latin1")
377
+
378
+
379
+ def unquote_to_wsgi_str(string: str) -> str:
380
+ return urllib.parse.unquote_to_bytes(string).decode("latin-1")
@@ -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
+ # Supported workers
9
+ SUPPORTED_WORKERS = {
10
+ "sync": "plain.server.workers.sync.SyncWorker",
11
+ "gthread": "plain.server.workers.gthread.ThreadWorker",
12
+ }