plain 0.74.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/CHANGELOG.md +29 -0
- plain/README.md +1 -1
- plain/chores/README.md +1 -1
- plain/cli/core.py +35 -17
- plain/cli/runtime.py +28 -0
- plain/cli/server.py +143 -0
- plain/server/LICENSE +35 -0
- plain/server/README.md +75 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +150 -0
- plain/server/http/message.py +399 -0
- plain/server/http/parser.py +69 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +91 -0
- plain/server/reloader.py +158 -0
- plain/server/sock.py +219 -0
- plain/server/util.py +380 -0
- plain/server/workers/__init__.py +12 -0
- plain/server/workers/base.py +305 -0
- plain/server/workers/gthread.py +393 -0
- plain/server/workers/sync.py +210 -0
- plain/server/workers/workertmp.py +50 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/METADATA +2 -2
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/RECORD +35 -9
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/WHEEL +0 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/entry_points.txt +0 -0
- {plain-0.74.0.dist-info → plain-0.76.0.dist-info}/licenses/LICENSE +0 -0
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
|
+
}
|