Pyjoyment 0.1.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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Server/FastCGI.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.FastCGI - FastCGI connections of the servers
|
|
3
|
+
========================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
import Pyjo.Server.Daemon
|
|
7
|
+
|
|
8
|
+
daemon = Pyjo.Server.Daemon.new(listen=['fcgi://127.0.0.1:9000'])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@daemon.unsubscribe('request').on('request')
|
|
12
|
+
def request(daemon, tx):
|
|
13
|
+
# Request
|
|
14
|
+
method = tx.req.method
|
|
15
|
+
path = tx.req.url.path
|
|
16
|
+
|
|
17
|
+
# Response
|
|
18
|
+
tx.res.code = 200
|
|
19
|
+
tx.res.headers.content_type = 'text/plain'
|
|
20
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
21
|
+
|
|
22
|
+
# Resume transaction
|
|
23
|
+
tx.resume()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
daemon.run()
|
|
27
|
+
|
|
28
|
+
:mod:`Pyjo.Server.FastCGI` speaks `FastCGI 1.0
|
|
29
|
+
<https://fastcgi-archives.github.io/FastCGI_Specification.html>`_ on the
|
|
30
|
+
connections of the listen locations ``fcgi://`` and ``fcgi+unix://`` of
|
|
31
|
+
:mod:`Pyjo.Server.Daemon`, and so of the servers built on it,
|
|
32
|
+
:mod:`Pyjo.Server.Prefork`, :mod:`Pyjo.Server.Hot` and
|
|
33
|
+
:mod:`Pyjo.Server.Reload`, with the same applications, timeouts, signals and
|
|
34
|
+
graceful shutdown as HTTP. ::
|
|
35
|
+
|
|
36
|
+
$ ./myapp.py daemon -l fcgi://127.0.0.1:9000
|
|
37
|
+
$ ./myapp.py prefork -l fcgi+unix://%2Ftmp%2Fmyapp.sock
|
|
38
|
+
|
|
39
|
+
A web server in front of the application speaks HTTP to the clients and
|
|
40
|
+
FastCGI to the application, which gets the request as the environment of
|
|
41
|
+
:rfc:`3875`, as :mod:`Pyjo.Server.CGI` does, but in a process that keeps
|
|
42
|
+
running. A connection takes one request at a time, in the role of a
|
|
43
|
+
responder, the only one that it supports, and stays open after it when the
|
|
44
|
+
web server asks for it with ``FCGI_KEEP_CONN``. The content of the response
|
|
45
|
+
can come later, as with :mod:`Pyjo.Server.Daemon`, and goes to the web server
|
|
46
|
+
as soon as it is written. WebSockets are not available, as FastCGI has no
|
|
47
|
+
protocol for them.
|
|
48
|
+
|
|
49
|
+
The application can also be started by the web server, which gives it its
|
|
50
|
+
listen socket as the file descriptor ``0``, as Apache with ``mod_fcgid`` and
|
|
51
|
+
lighttpd do; the command :mod:`Pyjoyment.Command.fastcgi` serves it, and is
|
|
52
|
+
detected by :mod:`Pyjoyment.Commands`, so the script of the application
|
|
53
|
+
needs no command.
|
|
54
|
+
|
|
55
|
+
Environment
|
|
56
|
+
-----------
|
|
57
|
+
|
|
58
|
+
The request is read from the parameters of FastCGI, as the environment of
|
|
59
|
+
CGI that :meth:`Pyjo.Message.Request.Pyjo_Message_Request.parse` takes, with
|
|
60
|
+
a few changes for what the web servers send:
|
|
61
|
+
|
|
62
|
+
* A path in ``SCRIPT_NAME`` without ``PATH_INFO`` is the path of the request,
|
|
63
|
+
as nginx with its ``fastcgi_params`` and Caddy send the whole path in
|
|
64
|
+
``SCRIPT_NAME``. An application deployed under a path prefix gets it in
|
|
65
|
+
``SCRIPT_NAME`` and the rest of the path in ``PATH_INFO``.
|
|
66
|
+
* The bytes of the paths beyond ASCII are percent-encoded, as the path is
|
|
67
|
+
escaped, as with :mod:`Pyjo.Server.WSGI`.
|
|
68
|
+
* A request body without ``CONTENT_LENGTH``, of a chunked request whose
|
|
69
|
+
framing the web server removed, is read whole before the request is parsed.
|
|
70
|
+
|
|
71
|
+
Web servers
|
|
72
|
+
-----------
|
|
73
|
+
|
|
74
|
+
* Caddy, with ``transport fastcgi``: it sends the response to the client with
|
|
75
|
+
the ``Status`` header next to its status line. ::
|
|
76
|
+
|
|
77
|
+
example.com {
|
|
78
|
+
reverse_proxy 127.0.0.1:9000 {
|
|
79
|
+
transport fastcgi
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
* Apache with ``mod_proxy_fcgi``: ::
|
|
84
|
+
|
|
85
|
+
ProxyPass / fcgi://127.0.0.1:9000/ enablereuse=on
|
|
86
|
+
|
|
87
|
+
* Apache with ``mod_fcgid``, which starts the application: ::
|
|
88
|
+
|
|
89
|
+
<VirtualHost *:80>
|
|
90
|
+
ServerName localhost
|
|
91
|
+
ScriptAlias / /home/sri/myapp/myapp.py/
|
|
92
|
+
<Directory /home/sri/myapp>
|
|
93
|
+
SetHandler fcgid-script
|
|
94
|
+
Options +ExecCGI
|
|
95
|
+
Require all granted
|
|
96
|
+
</Directory>
|
|
97
|
+
</VirtualHost>
|
|
98
|
+
|
|
99
|
+
* nginx: ::
|
|
100
|
+
|
|
101
|
+
location / {
|
|
102
|
+
include fastcgi_params;
|
|
103
|
+
fastcgi_pass 127.0.0.1:9000;
|
|
104
|
+
fastcgi_keep_conn on;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
See also
|
|
108
|
+
--------
|
|
109
|
+
|
|
110
|
+
:mod:`Pyjoyment.Command.fastcgi`, :mod:`Pyjo.Server.Daemon`, :mod:`Pyjoyment`,
|
|
111
|
+
https://mojolicious.org.
|
|
112
|
+
|
|
113
|
+
It has no counterpart in Mojolicious; :mod:`Pyjo.Server.CGI` is the
|
|
114
|
+
counterpart of `Mojo::Server::CGI
|
|
115
|
+
<https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/CGI.pm>`_.
|
|
116
|
+
|
|
117
|
+
Classes
|
|
118
|
+
-------
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
import Pyjo.Base
|
|
122
|
+
import Pyjo.Content.Single
|
|
123
|
+
import Pyjo.Transaction
|
|
124
|
+
|
|
125
|
+
from Pyjo.Util import url_escape
|
|
126
|
+
|
|
127
|
+
import struct
|
|
128
|
+
import weakref
|
|
129
|
+
import wsgiref.util
|
|
130
|
+
|
|
131
|
+
from typing import TYPE_CHECKING, Any
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if TYPE_CHECKING:
|
|
135
|
+
import Pyjo.Server.Daemon
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# The types of the records
|
|
139
|
+
BEGIN_REQUEST = 1
|
|
140
|
+
ABORT_REQUEST = 2
|
|
141
|
+
END_REQUEST = 3
|
|
142
|
+
PARAMS = 4
|
|
143
|
+
STDIN = 5
|
|
144
|
+
STDOUT = 6
|
|
145
|
+
GET_VALUES = 9
|
|
146
|
+
GET_VALUES_RESULT = 10
|
|
147
|
+
UNKNOWN_TYPE = 11
|
|
148
|
+
|
|
149
|
+
# The role, the flag of BEGIN_REQUEST and the statuses of END_REQUEST
|
|
150
|
+
RESPONDER = 1
|
|
151
|
+
KEEP_CONN = 1
|
|
152
|
+
REQUEST_COMPLETE = 0
|
|
153
|
+
CANT_MPX_CONN = 1
|
|
154
|
+
UNKNOWN_ROLE = 3
|
|
155
|
+
|
|
156
|
+
# The header of a record: version, type, request id, content length, padding length and a reserved byte
|
|
157
|
+
HEADER = struct.Struct('>BBHHBx')
|
|
158
|
+
|
|
159
|
+
# The longest content of a record
|
|
160
|
+
MAX_CONTENT = 65535
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Pyjo_Server_FastCGI(Pyjo.Base.object):
|
|
164
|
+
"""
|
|
165
|
+
:mod:`Pyjo.Server.FastCGI` is the FastCGI side of a connection of
|
|
166
|
+
:mod:`Pyjo.Server.Daemon`, which makes it for the connections of its
|
|
167
|
+
listen locations ``fcgi://`` and ``fcgi+unix://``.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, daemon: 'Pyjo.Server.Daemon.object', id: str) -> None:
|
|
171
|
+
"""::
|
|
172
|
+
|
|
173
|
+
fcgi = Pyjo.Server.FastCGI.new(daemon, id)
|
|
174
|
+
|
|
175
|
+
Construct a new :mod:`Pyjo.Server.FastCGI` object for the connection
|
|
176
|
+
``id`` of the daemon.
|
|
177
|
+
"""
|
|
178
|
+
super().__init__()
|
|
179
|
+
|
|
180
|
+
self.id = id
|
|
181
|
+
"""::
|
|
182
|
+
|
|
183
|
+
id = fcgi.id
|
|
184
|
+
|
|
185
|
+
Id of the connection in the event loop of the daemon.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
self._daemon = weakref.ref(daemon)
|
|
189
|
+
self._buffer = bytearray()
|
|
190
|
+
self._closing = False
|
|
191
|
+
self._request: dict[str, Any] | None = None
|
|
192
|
+
self._writing = False
|
|
193
|
+
|
|
194
|
+
def closed(self) -> None:
|
|
195
|
+
"""::
|
|
196
|
+
|
|
197
|
+
fcgi.closed()
|
|
198
|
+
|
|
199
|
+
The connection has been closed, which finishes the transaction of the
|
|
200
|
+
request in progress.
|
|
201
|
+
"""
|
|
202
|
+
self._closing = True
|
|
203
|
+
if (req := self._request) is not None:
|
|
204
|
+
self._finish(req)
|
|
205
|
+
|
|
206
|
+
def read(self, chunk: bytes) -> None:
|
|
207
|
+
"""::
|
|
208
|
+
|
|
209
|
+
fcgi.read(chunk)
|
|
210
|
+
|
|
211
|
+
Read the records of a chunk of data from the web server.
|
|
212
|
+
"""
|
|
213
|
+
buffer = self._buffer
|
|
214
|
+
buffer += chunk
|
|
215
|
+
while len(buffer) >= HEADER.size and not self._closing:
|
|
216
|
+
version, type, id, length, padding = HEADER.unpack_from(buffer)
|
|
217
|
+
|
|
218
|
+
# Not FastCGI
|
|
219
|
+
if version != 1:
|
|
220
|
+
self._trace(f'FastCGI version {version} is not supported')
|
|
221
|
+
return self._close()
|
|
222
|
+
|
|
223
|
+
end = HEADER.size + length + padding
|
|
224
|
+
if len(buffer) < end:
|
|
225
|
+
break
|
|
226
|
+
content = bytes(buffer[HEADER.size : HEADER.size + length])
|
|
227
|
+
del buffer[:end]
|
|
228
|
+
self._record(type, id, content)
|
|
229
|
+
|
|
230
|
+
def _begin(self, id: int, content: bytes) -> None:
|
|
231
|
+
role, flags = struct.unpack_from('>HB', content)
|
|
232
|
+
keep = bool(flags & KEEP_CONN)
|
|
233
|
+
|
|
234
|
+
# One request at a time
|
|
235
|
+
if self._request is not None:
|
|
236
|
+
return self._send(_end(id, CANT_MPX_CONN))
|
|
237
|
+
|
|
238
|
+
# Only responders
|
|
239
|
+
if role != RESPONDER:
|
|
240
|
+
self._send(_end(id, UNKNOWN_ROLE))
|
|
241
|
+
if not keep:
|
|
242
|
+
self._close()
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
self._request = {'id': id, 'keep': keep, 'params': bytearray()}
|
|
246
|
+
if (daemon := self._daemon()) is not None:
|
|
247
|
+
daemon.ioloop.stream(self.id).timeout = daemon.inactivity_timeout
|
|
248
|
+
|
|
249
|
+
def _close(self) -> None:
|
|
250
|
+
self._closing = True
|
|
251
|
+
if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
|
|
252
|
+
stream.close_gracefully()
|
|
253
|
+
|
|
254
|
+
def _env(self, req: dict[str, Any]) -> None:
|
|
255
|
+
if (daemon := self._daemon()) is None:
|
|
256
|
+
return
|
|
257
|
+
env = _environ(_pairs(bytes(req.pop('params'))))
|
|
258
|
+
|
|
259
|
+
tx = req['tx'] = daemon.build_tx()
|
|
260
|
+
tx.connection = self.id
|
|
261
|
+
# Unlike Mojolicious: Pyjoyment names itself
|
|
262
|
+
tx.res.headers.server = 'Pyjoyment (Python)'
|
|
263
|
+
|
|
264
|
+
# A body of unknown length is read whole before the request is parsed
|
|
265
|
+
if env.get('CONTENT_LENGTH'):
|
|
266
|
+
tx.req.parse(env)
|
|
267
|
+
else:
|
|
268
|
+
req['env'], req['body'] = env, bytearray()
|
|
269
|
+
|
|
270
|
+
tx.local_address = env.get('SERVER_ADDR')
|
|
271
|
+
tx.local_port = int(port) if (port := env.get('SERVER_PORT')) else None
|
|
272
|
+
tx.remote_address = env.get('REMOTE_ADDR')
|
|
273
|
+
tx.remote_port = int(port) if (port := env.get('REMOTE_PORT')) else None
|
|
274
|
+
|
|
275
|
+
def _finish(self, req: dict[str, Any]) -> None:
|
|
276
|
+
# Once, as an aborted request may still be written
|
|
277
|
+
if req.get('finished'):
|
|
278
|
+
return
|
|
279
|
+
req['finished'] = True
|
|
280
|
+
if self._request is req:
|
|
281
|
+
self._request = None
|
|
282
|
+
if (tx := req.get('tx')) is not None:
|
|
283
|
+
if (resume := req.get('resume')) is not None:
|
|
284
|
+
tx.unsubscribe('resume', resume)
|
|
285
|
+
tx.closed()
|
|
286
|
+
|
|
287
|
+
if self._closing:
|
|
288
|
+
return
|
|
289
|
+
if not req['keep']:
|
|
290
|
+
return self._close()
|
|
291
|
+
if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
|
|
292
|
+
stream.timeout = daemon.keep_alive_timeout
|
|
293
|
+
|
|
294
|
+
def _get_values(self, content: bytes) -> None:
|
|
295
|
+
daemon = self._daemon()
|
|
296
|
+
max = daemon.ioloop.max_connections if daemon is not None else 1
|
|
297
|
+
values = {'FCGI_MAX_CONNS': str(max), 'FCGI_MAX_REQS': str(max), 'FCGI_MPXS_CONNS': '0'}
|
|
298
|
+
result = {name: values[name] for name in _pairs(content) if name in values}
|
|
299
|
+
self._send(_record(GET_VALUES_RESULT, 0, _encode_pairs(result)))
|
|
300
|
+
|
|
301
|
+
def _record(self, type: int, id: int, content: bytes) -> None:
|
|
302
|
+
# Management records
|
|
303
|
+
if id == 0:
|
|
304
|
+
if type == GET_VALUES:
|
|
305
|
+
return self._get_values(content)
|
|
306
|
+
return self._send(_record(UNKNOWN_TYPE, 0, struct.pack('>B7x', type)))
|
|
307
|
+
|
|
308
|
+
if type == BEGIN_REQUEST:
|
|
309
|
+
return self._begin(id, content)
|
|
310
|
+
|
|
311
|
+
# Records of another request are ignored, as the ones of a request that is not active
|
|
312
|
+
if (req := self._request) is None or req['id'] != id:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if type == ABORT_REQUEST:
|
|
316
|
+
self._send(_end(id))
|
|
317
|
+
return self._finish(req)
|
|
318
|
+
|
|
319
|
+
if type == PARAMS and 'params' in req:
|
|
320
|
+
if len(content):
|
|
321
|
+
req['params'] += content
|
|
322
|
+
else:
|
|
323
|
+
self._env(req)
|
|
324
|
+
|
|
325
|
+
elif type == STDIN and req.get('tx') is not None and 'resume' not in req:
|
|
326
|
+
if (body := req.get('body')) is not None:
|
|
327
|
+
body += content
|
|
328
|
+
elif len(content):
|
|
329
|
+
req['tx'].req.parse(content)
|
|
330
|
+
if not len(content):
|
|
331
|
+
self._request_ready(req)
|
|
332
|
+
|
|
333
|
+
def _request_ready(self, req: dict[str, Any]) -> None:
|
|
334
|
+
if (daemon := self._daemon()) is None:
|
|
335
|
+
return
|
|
336
|
+
tx = req['tx']
|
|
337
|
+
|
|
338
|
+
# The request of unknown length with its body
|
|
339
|
+
if (body := req.pop('body', None)) is not None:
|
|
340
|
+
env = req.pop('env')
|
|
341
|
+
if len(body):
|
|
342
|
+
env['CONTENT_LENGTH'] = str(len(body))
|
|
343
|
+
tx.req.parse(env)
|
|
344
|
+
if len(body):
|
|
345
|
+
tx.req.parse(bytes(body))
|
|
346
|
+
|
|
347
|
+
if error := tx.req.error:
|
|
348
|
+
self._trace(error['message'])
|
|
349
|
+
|
|
350
|
+
ref = weakref.ref(self)
|
|
351
|
+
|
|
352
|
+
# The response is written once the transaction is resumed, as the content of a response can come later
|
|
353
|
+
def resume(tx: Pyjo.Transaction.object) -> None:
|
|
354
|
+
req['resumed'] = True
|
|
355
|
+
if (this := ref()) is not None:
|
|
356
|
+
this._write()
|
|
357
|
+
|
|
358
|
+
req['resume'] = resume
|
|
359
|
+
tx.on('resume', resume)
|
|
360
|
+
daemon.emit('request', tx)
|
|
361
|
+
self._write()
|
|
362
|
+
|
|
363
|
+
def _send(self, data: bytes, cb: Any = None) -> None:
|
|
364
|
+
if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
|
|
365
|
+
stream.write(data, cb)
|
|
366
|
+
|
|
367
|
+
def _trace(self, message: str) -> None:
|
|
368
|
+
if (daemon := self._daemon()) is not None:
|
|
369
|
+
daemon.app.log.trace(message)
|
|
370
|
+
|
|
371
|
+
def _write(self) -> None:
|
|
372
|
+
# Protect from resume event recursion
|
|
373
|
+
if (req := self._request) is None or not req.get('resumed') or req.get('ended') or self._writing:
|
|
374
|
+
return
|
|
375
|
+
self._writing = True
|
|
376
|
+
try:
|
|
377
|
+
tx = req['tx']
|
|
378
|
+
res = tx.res
|
|
379
|
+
finished = False
|
|
380
|
+
|
|
381
|
+
# Response headers, with the status in a header as CGI has it
|
|
382
|
+
if 'offset' not in req:
|
|
383
|
+
res.fix_headers()
|
|
384
|
+
code = res.code or 404
|
|
385
|
+
lines = [f'Status: {code} {res.message or res.default_message()}']
|
|
386
|
+
# Unlike Mojolicious: without the hop-by-hop headers, so chunked content goes without its framing,
|
|
387
|
+
# which the web server adds
|
|
388
|
+
lines += [f'{name}: {value}' for name, value in res.headers.to_list() if not _hop_by_hop(name)]
|
|
389
|
+
data = ('\x0d\x0a'.join(lines) + '\x0d\x0a\x0d\x0a').encode('latin-1')
|
|
390
|
+
req['offset'] = 0
|
|
391
|
+
req['unchunk'] = _Unchunk() if res.content.is_chunked else None
|
|
392
|
+
finished = tx.is_empty
|
|
393
|
+
|
|
394
|
+
# Response body
|
|
395
|
+
else:
|
|
396
|
+
chunk = res.get_body_chunk(req['offset'])
|
|
397
|
+
|
|
398
|
+
# No content yet, the resume event comes when there is some
|
|
399
|
+
if chunk is None:
|
|
400
|
+
return
|
|
401
|
+
if not len(chunk):
|
|
402
|
+
finished = True
|
|
403
|
+
req['offset'] += len(chunk)
|
|
404
|
+
data = req['unchunk'].parse(chunk) if req['unchunk'] is not None else chunk
|
|
405
|
+
finally:
|
|
406
|
+
self._writing = False
|
|
407
|
+
|
|
408
|
+
records = b''.join(
|
|
409
|
+
_record(STDOUT, req['id'], data[i : i + MAX_CONTENT]) for i in range(0, len(data), MAX_CONTENT)
|
|
410
|
+
)
|
|
411
|
+
ref = weakref.ref(self)
|
|
412
|
+
|
|
413
|
+
# Finish the request
|
|
414
|
+
if finished:
|
|
415
|
+
req['ended'] = True
|
|
416
|
+
records += _record(STDOUT, req['id']) + _end(req['id'])
|
|
417
|
+
|
|
418
|
+
def done(stream: Any) -> None:
|
|
419
|
+
if (this := ref()) is not None:
|
|
420
|
+
this._finish(req)
|
|
421
|
+
|
|
422
|
+
return self._send(records, done)
|
|
423
|
+
|
|
424
|
+
# Next chunk
|
|
425
|
+
def written(stream: Any) -> None:
|
|
426
|
+
if (this := ref()) is not None:
|
|
427
|
+
this._write()
|
|
428
|
+
|
|
429
|
+
self._send(records, written)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# Chunked content without its framing, as in Pyjo.Server.WSGI
|
|
433
|
+
class _Unchunk:
|
|
434
|
+
def __init__(self) -> None:
|
|
435
|
+
self._chunks: list[bytes] = []
|
|
436
|
+
self._parser = Pyjo.Content.Single.new()
|
|
437
|
+
self._parser.unsubscribe('read').on('read', self._read)
|
|
438
|
+
self._parser.parse(b'Transfer-Encoding: chunked\x0d\x0a\x0d\x0a')
|
|
439
|
+
|
|
440
|
+
def _read(self, content: Pyjo.Content.Single.object, chunk: bytes) -> None:
|
|
441
|
+
self._chunks.append(chunk)
|
|
442
|
+
|
|
443
|
+
def parse(self, chunk: bytes) -> bytes:
|
|
444
|
+
self._parser.parse(chunk)
|
|
445
|
+
chunks, self._chunks = self._chunks, []
|
|
446
|
+
return b''.join(chunks)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _encode_length(value: bytes) -> bytes:
|
|
450
|
+
return struct.pack('>B', len(value)) if len(value) < 128 else struct.pack('>I', len(value) | 0x80000000)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _encode_pairs(pairs: dict[str, str]) -> bytes:
|
|
454
|
+
data = b''
|
|
455
|
+
for name, value in pairs.items():
|
|
456
|
+
name_bytes, value_bytes = name.encode('latin-1'), value.encode('latin-1')
|
|
457
|
+
data += _encode_length(name_bytes) + _encode_length(value_bytes) + name_bytes + value_bytes
|
|
458
|
+
return data
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _end(id: int, status: int = REQUEST_COMPLETE) -> bytes:
|
|
462
|
+
# The application status of a request is 0, as a process of CGI that exits normally
|
|
463
|
+
return _record(END_REQUEST, id, struct.pack('>IB3x', 0, status))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# The environment of CGI that Pyjo.Message.Request parses, from the parameters that the web server sends
|
|
467
|
+
def _environ(params: dict[str, str]) -> dict[str, str]:
|
|
468
|
+
env = dict(params)
|
|
469
|
+
|
|
470
|
+
# The whole path in SCRIPT_NAME, as nginx with fastcgi_params and Caddy send it
|
|
471
|
+
if not env.get('PATH_INFO') and env.get('SCRIPT_NAME'):
|
|
472
|
+
env['PATH_INFO'], env['SCRIPT_NAME'] = env['SCRIPT_NAME'], ''
|
|
473
|
+
|
|
474
|
+
# A path is bytes as the characters 0-255, so the bytes beyond ASCII are percent-encoded, as the path is escaped
|
|
475
|
+
for key in ('PATH_INFO', 'SCRIPT_NAME'):
|
|
476
|
+
if key in env:
|
|
477
|
+
env[key] = url_escape(env[key].encode('latin-1'), rb'\x80-\xff').decode('ascii')
|
|
478
|
+
|
|
479
|
+
# The web server removes the chunked framing of a request
|
|
480
|
+
env.pop('HTTP_TRANSFER_ENCODING', None)
|
|
481
|
+
return env
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _hop_by_hop(name: str) -> bool:
|
|
485
|
+
return wsgiref.util.is_hop_by_hop(name)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# The name-value pairs of FastCGI, whose lengths take one byte, or four with the high bit set, as characters 0-255
|
|
489
|
+
def _pairs(data: bytes) -> dict[str, str]:
|
|
490
|
+
pairs: dict[str, str] = {}
|
|
491
|
+
offset = 0
|
|
492
|
+
while offset < len(data):
|
|
493
|
+
lengths: list[int] = []
|
|
494
|
+
for _ in range(2):
|
|
495
|
+
if offset >= len(data):
|
|
496
|
+
return pairs
|
|
497
|
+
if data[offset] < 128:
|
|
498
|
+
lengths.append(data[offset])
|
|
499
|
+
offset += 1
|
|
500
|
+
else:
|
|
501
|
+
lengths.append(struct.unpack_from('>I', data, offset)[0] & 0x7FFFFFFF)
|
|
502
|
+
offset += 4
|
|
503
|
+
name = data[offset : offset + lengths[0]].decode('latin-1')
|
|
504
|
+
offset += lengths[0]
|
|
505
|
+
pairs[name] = data[offset : offset + lengths[1]].decode('latin-1')
|
|
506
|
+
offset += lengths[1]
|
|
507
|
+
return pairs
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _record(type: int, id: int, content: bytes = b'') -> bytes:
|
|
511
|
+
padding = -len(content) % 8
|
|
512
|
+
return HEADER.pack(1, type, id, len(content), padding) + content + b'\0' * padding
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
new = Pyjo_Server_FastCGI.new
|
|
516
|
+
object = Pyjo_Server_FastCGI
|
|
517
|
+
FastCGI = Pyjo_Server_FastCGI
|