plain 0.75.0__py3-none-any.whl → 0.77.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.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

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,313 @@
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 io
14
+ import os
15
+ import random
16
+ import re
17
+ import socket
18
+ import sys
19
+ import textwrap
20
+ import time
21
+ import urllib.parse
22
+ import warnings
23
+ from collections.abc import Callable
24
+ from typing import Any
25
+
26
+ # Server and Date aren't technically hop-by-hop
27
+ # headers, but they are in the purview of the
28
+ # origin server which the WSGI spec says we should
29
+ # act like. So we drop them and add our own.
30
+ #
31
+ # In the future, concatenation server header values
32
+ # might be better, but nothing else does it and
33
+ # dropping them is easier.
34
+ hop_headers = set(
35
+ """
36
+ connection keep-alive proxy-authenticate proxy-authorization
37
+ te trailers transfer-encoding upgrade
38
+ server date
39
+ """.split()
40
+ )
41
+
42
+ if sys.platform.startswith("win"):
43
+
44
+ def _waitfor(
45
+ func: Callable[[str], None], pathname: str, waitall: bool = False
46
+ ) -> None:
47
+ # Perform the operation
48
+ func(pathname)
49
+ # Now setup the wait loop
50
+ if waitall:
51
+ dirname = pathname
52
+ else:
53
+ dirname, name = os.path.split(pathname)
54
+ dirname = dirname or "."
55
+ # Check for `pathname` to be removed from the filesystem.
56
+ # The exponential backoff of the timeout amounts to a total
57
+ # of ~1 second after which the deletion is probably an error
58
+ # anyway.
59
+ # Testing on a i7@4.3GHz shows that usually only 1 iteration is
60
+ # required when contention occurs.
61
+ timeout = 0.001
62
+ while timeout < 1.0:
63
+ # Note we are only testing for the existence of the file(s) in
64
+ # the contents of the directory regardless of any security or
65
+ # access rights. If we have made it this far, we have sufficient
66
+ # permissions to do that much using Python's equivalent of the
67
+ # Windows API FindFirstFile.
68
+ # Other Windows APIs can fail or give incorrect results when
69
+ # dealing with files that are pending deletion.
70
+ L = os.listdir(dirname)
71
+ if not L if waitall else name in L:
72
+ return None
73
+ # Increase the timeout and try again
74
+ time.sleep(timeout)
75
+ timeout *= 2
76
+ warnings.warn(
77
+ "tests may fail, delete still pending for " + pathname,
78
+ RuntimeWarning,
79
+ stacklevel=4,
80
+ )
81
+ return None
82
+
83
+ def _unlink(filename: str) -> None:
84
+ _waitfor(os.unlink, filename)
85
+ return None
86
+ else:
87
+ _unlink = os.unlink
88
+
89
+
90
+ def unlink(filename: str) -> None:
91
+ try:
92
+ _unlink(filename)
93
+ except OSError as error:
94
+ # The filename need not exist.
95
+ if error.errno not in (errno.ENOENT, errno.ENOTDIR):
96
+ raise
97
+
98
+
99
+ def is_ipv6(addr: str) -> bool:
100
+ try:
101
+ socket.inet_pton(socket.AF_INET6, addr)
102
+ except OSError: # not a valid address
103
+ return False
104
+ except ValueError: # ipv6 not supported on this platform
105
+ return False
106
+ return True
107
+
108
+
109
+ def parse_address(netloc: str, default_port: str = "8000") -> str | tuple[str, int]:
110
+ if re.match(r"unix:(//)?", netloc):
111
+ return re.split(r"unix:(//)?", netloc)[-1]
112
+
113
+ if netloc.startswith("tcp://"):
114
+ netloc = netloc.split("tcp://")[1]
115
+ host, port = netloc, default_port
116
+
117
+ if "[" in netloc and "]" in netloc:
118
+ host = netloc.split("]")[0][1:]
119
+ port = (netloc.split("]:") + [default_port])[1]
120
+ elif ":" in netloc:
121
+ host, port = (netloc.split(":") + [default_port])[:2]
122
+ elif netloc == "":
123
+ host, port = "0.0.0.0", default_port
124
+
125
+ try:
126
+ port = int(port)
127
+ except ValueError:
128
+ raise RuntimeError(f"{port!r} is not a valid port number.")
129
+
130
+ return host.lower(), port
131
+
132
+
133
+ def close_on_exec(fd: int) -> None:
134
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
135
+ flags |= fcntl.FD_CLOEXEC
136
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags)
137
+
138
+
139
+ def set_non_blocking(fd: int) -> None:
140
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
141
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags)
142
+
143
+
144
+ def close(sock: socket.socket) -> None:
145
+ try:
146
+ sock.close()
147
+ except OSError:
148
+ pass
149
+
150
+
151
+ def write_chunk(sock: socket.socket, data: str | bytes) -> None:
152
+ if isinstance(data, str):
153
+ data = data.encode("utf-8")
154
+ chunk_size = f"{len(data):X}\r\n"
155
+ chunk = b"".join([chunk_size.encode("utf-8"), data, b"\r\n"])
156
+ sock.sendall(chunk)
157
+
158
+
159
+ def write(sock: socket.socket, data: str | bytes, chunked: bool = False) -> None:
160
+ if chunked:
161
+ return write_chunk(sock, data)
162
+ sock.sendall(data)
163
+
164
+
165
+ def write_nonblock(
166
+ sock: socket.socket, data: str | bytes, chunked: bool = False
167
+ ) -> None:
168
+ timeout = sock.gettimeout()
169
+ if timeout != 0.0:
170
+ try:
171
+ sock.setblocking(False)
172
+ return write(sock, data, chunked)
173
+ finally:
174
+ sock.setblocking(True)
175
+ else:
176
+ return write(sock, data, chunked)
177
+
178
+
179
+ def write_error(sock: socket.socket, status_int: int, reason: str, mesg: str) -> None:
180
+ html_error = textwrap.dedent("""\
181
+ <html>
182
+ <head>
183
+ <title>%(reason)s</title>
184
+ </head>
185
+ <body>
186
+ <h1><p>%(reason)s</p></h1>
187
+ %(mesg)s
188
+ </body>
189
+ </html>
190
+ """) % {"reason": reason, "mesg": html.escape(mesg)}
191
+
192
+ http = textwrap.dedent("""\
193
+ HTTP/1.1 %s %s\r
194
+ Connection: close\r
195
+ Content-Type: text/html\r
196
+ Content-Length: %d\r
197
+ \r
198
+ %s""") % (str(status_int), reason, len(html_error), html_error)
199
+ write_nonblock(sock, http.encode("latin1"))
200
+
201
+
202
+ def getcwd() -> str:
203
+ # get current path, try to use PWD env first
204
+ try:
205
+ a = os.stat(os.environ["PWD"])
206
+ b = os.stat(os.getcwd())
207
+ if a.st_ino == b.st_ino and a.st_dev == b.st_dev:
208
+ cwd = os.environ["PWD"]
209
+ else:
210
+ cwd = os.getcwd()
211
+ except Exception:
212
+ cwd = os.getcwd()
213
+ return cwd
214
+
215
+
216
+ def http_date(timestamp: float | None = None) -> str:
217
+ """Return the current date and time formatted for a message header."""
218
+ if timestamp is None:
219
+ timestamp = time.time()
220
+ s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)
221
+ return s
222
+
223
+
224
+ def is_hoppish(header: str) -> bool:
225
+ return header.lower().strip() in hop_headers
226
+
227
+
228
+ def seed() -> None:
229
+ try:
230
+ random.seed(os.urandom(64))
231
+ except NotImplementedError:
232
+ random.seed(f"{time.time()}.{os.getpid()}")
233
+
234
+
235
+ def check_is_writable(path: str) -> None:
236
+ try:
237
+ with open(path, "a") as f:
238
+ f.close()
239
+ except OSError as e:
240
+ raise RuntimeError(f"Error: '{path}' isn't writable [{e!r}]")
241
+
242
+
243
+ def to_bytestring(value: str | bytes, encoding: str = "utf8") -> bytes:
244
+ """Converts a string argument to a byte string"""
245
+ if isinstance(value, bytes):
246
+ return value
247
+ if not isinstance(value, str):
248
+ raise TypeError(f"{value!r} is not a string")
249
+
250
+ return value.encode(encoding)
251
+
252
+
253
+ def has_fileno(obj: Any) -> bool:
254
+ if not hasattr(obj, "fileno"):
255
+ return False
256
+
257
+ # check BytesIO case and maybe others
258
+ try:
259
+ obj.fileno()
260
+ except (AttributeError, OSError, io.UnsupportedOperation):
261
+ return False
262
+
263
+ return True
264
+
265
+
266
+ def make_fail_app(msg: str | bytes) -> Callable[..., Any]:
267
+ msg = to_bytestring(msg)
268
+
269
+ def app(environ: Any, start_response: Any) -> list[bytes]:
270
+ start_response(
271
+ "500 Internal Server Error",
272
+ [("Content-Type", "text/plain"), ("Content-Length", str(len(msg)))],
273
+ )
274
+ return [msg]
275
+
276
+ return app
277
+
278
+
279
+ def split_request_uri(uri: str) -> urllib.parse.SplitResult:
280
+ if uri.startswith("//"):
281
+ # When the path starts with //, urlsplit considers it as a
282
+ # relative uri while the RFC says we should consider it as abs_path
283
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
284
+ # We use temporary dot prefix to workaround this behaviour
285
+ parts = urllib.parse.urlsplit("." + uri)
286
+ return parts._replace(path=parts.path[1:])
287
+
288
+ return urllib.parse.urlsplit(uri)
289
+
290
+
291
+ # From six.reraise
292
+ def reraise(
293
+ tp: type[BaseException], value: BaseException | None, tb: Any = None
294
+ ) -> None:
295
+ try:
296
+ if value is None:
297
+ value = tp()
298
+ if value.__traceback__ is not tb:
299
+ raise value.with_traceback(tb)
300
+ raise value
301
+ finally:
302
+ value = None
303
+ tb = None
304
+
305
+
306
+ def bytes_to_str(b: str | bytes) -> str:
307
+ if isinstance(b, str):
308
+ return b
309
+ return str(b, "latin1")
310
+
311
+
312
+ def unquote_to_wsgi_str(string: str) -> str:
313
+ return urllib.parse.unquote_to_bytes(string).decode("latin-1")
@@ -0,0 +1,6 @@
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.