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/App/CGI.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.App.CGI - CGI application
|
|
3
|
+
==============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.App.CGI import CGI
|
|
7
|
+
|
|
8
|
+
app = CGI(script='/usr/lib/cgi-bin/hello.cgi')
|
|
9
|
+
|
|
10
|
+
# Or import the module, as in Google Python Style Guide
|
|
11
|
+
|
|
12
|
+
import Pyjo.App.CGI
|
|
13
|
+
|
|
14
|
+
app = Pyjo.App.CGI.new(script='/usr/lib/cgi-bin/hello.cgi')
|
|
15
|
+
|
|
16
|
+
# The programs of a directory, as cgi-bin
|
|
17
|
+
app = Pyjo.App.CGI.new(document_root='/usr/lib/cgi-bin')
|
|
18
|
+
|
|
19
|
+
# PHP on php-cgi, with the files of its directory
|
|
20
|
+
app = Pyjo.App.CGI.new(
|
|
21
|
+
command=['php-cgi'], document_root='/var/www', script_extensions=['.php'], static=True
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Mount it in an application of Pyjoyment
|
|
25
|
+
import Pyjoyment
|
|
26
|
+
|
|
27
|
+
pyjoyment = Pyjoyment.new()
|
|
28
|
+
pyjoyment.plugin('Mount', {'/cgi-bin': app})
|
|
29
|
+
|
|
30
|
+
:mod:`Pyjo.App.CGI` is an application of :mod:`Pyjo` that runs a ``CGI``
|
|
31
|
+
program, as :rfc:`3875` describes it, in a process of its own for each
|
|
32
|
+
request, which :mod:`Pyjo.IOLoop.ChildProcess` runs, so that a server of :mod:`Pyjo` serves it, a route of
|
|
33
|
+
:mod:`Pyjoyment` embeds it, as :mod:`Pyjoyment.Plugin.Mount` does, and
|
|
34
|
+
:mod:`Pyjo.Test.Pyjo` tests it.
|
|
35
|
+
|
|
36
|
+
The environment of the process is the one of ``CGI`` of the request: the part
|
|
37
|
+
of the path that a route embeds the application under is ``SCRIPT_NAME``, after
|
|
38
|
+
the base path of the request, which a part of the path that does not end with
|
|
39
|
+
a slash, as ``/cgi`` of ``/cgix``, cannot be, so that the application is not
|
|
40
|
+
found there, the rest of the path is ``PATH_INFO``, ``REQUEST_URI`` is the path
|
|
41
|
+
with the query, without the header ``Proxy``, whose ``HTTP_PROXY`` a program
|
|
42
|
+
may take for the proxy of its own requests, as with httpoxy, with ``PATH`` of
|
|
43
|
+
the process of Pyjoyment, and :attr:`env` adds to them. The program runs in
|
|
44
|
+
the directory of its file, and the body of the request goes to its standard
|
|
45
|
+
input, each chunk once the one before has been written.
|
|
46
|
+
|
|
47
|
+
The standard output of the program is its response of ``CGI``, read through
|
|
48
|
+
the event loop, as it comes. A response that comes whole, before the program
|
|
49
|
+
closes its standard output, is written with its ``Content-Length``. Another one
|
|
50
|
+
is written as it comes, each chunk once the one before has been written to the
|
|
51
|
+
client, with the chunked transfer encoding when it does not give its
|
|
52
|
+
``Content-Length``. What the program writes to its standard error is logged.
|
|
53
|
+
|
|
54
|
+
A program that does not start or ends before its response is a response
|
|
55
|
+
``502``, and one that ends otherwise after the start of the response closes
|
|
56
|
+
the connection of the client, as one that is quiet for longer than
|
|
57
|
+
:attr:`inactivity_timeout` does, which, until the response starts, the
|
|
58
|
+
connection of the client waits for without the inactivity timeout of the
|
|
59
|
+
server. The process of a request whose client is gone is killed, with the
|
|
60
|
+
processes that it has started, as it runs as the leader of a process group.
|
|
61
|
+
|
|
62
|
+
The reactor watches the pipes of the process, which it can on POSIX systems,
|
|
63
|
+
not on Windows, where the response is ``502``.
|
|
64
|
+
|
|
65
|
+
Attributes
|
|
66
|
+
----------
|
|
67
|
+
|
|
68
|
+
:mod:`Pyjo.App.CGI` inherits all attributes from :mod:`Pyjo.App` and
|
|
69
|
+
implements the following new ones.
|
|
70
|
+
|
|
71
|
+
See also
|
|
72
|
+
--------
|
|
73
|
+
|
|
74
|
+
:mod:`Pyjoyment`, :mod:`Pyjoyment.Plugin.Mount`, :mod:`Pyjo.App.FastCGI`,
|
|
75
|
+
:mod:`Pyjo.IOLoop.ChildProcess`, :mod:`Pyjo.Server.CGI`, https://mojolicious.org.
|
|
76
|
+
|
|
77
|
+
It has no counterpart in Mojolicious; it is the reverse of
|
|
78
|
+
:mod:`Pyjo.Server.CGI`, inspired by ``Mojolicious::Plugin::CGI`` of CPAN.
|
|
79
|
+
|
|
80
|
+
Classes
|
|
81
|
+
-------
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
import Pyjo.App
|
|
85
|
+
import Pyjo.Exception
|
|
86
|
+
import Pyjo.IOLoop
|
|
87
|
+
import Pyjo.IOLoop.ChildProcess
|
|
88
|
+
import Pyjo.Message.Response
|
|
89
|
+
import Pyjo.Transaction
|
|
90
|
+
|
|
91
|
+
import os
|
|
92
|
+
import re
|
|
93
|
+
import signal
|
|
94
|
+
import wsgiref.util
|
|
95
|
+
|
|
96
|
+
from collections.abc import Sequence
|
|
97
|
+
from typing import Any
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Directories of the files of X-Sendfile, or regular expressions of their paths
|
|
101
|
+
type Place = str | os.PathLike[str] | re.Pattern[str]
|
|
102
|
+
type Sendfile = Place | Sequence[Place]
|
|
103
|
+
|
|
104
|
+
# The headers of a response by X-Sendfile that the file takes the place of
|
|
105
|
+
_SENT = ('content-length', 'x-sendfile')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Pyjo_App_CGI(Pyjo.App.object):
|
|
109
|
+
"""
|
|
110
|
+
:mod:`Pyjo.App.CGI` inherits all methods from :mod:`Pyjo.App` and
|
|
111
|
+
implements the following new ones.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, attrs: dict[str, Any] | None = None, /, **kwargs: Any) -> None:
|
|
115
|
+
"""::
|
|
116
|
+
|
|
117
|
+
app = Pyjo.App.CGI.new(script='/usr/lib/cgi-bin/hello.cgi')
|
|
118
|
+
|
|
119
|
+
Construct a new :mod:`Pyjo.App.CGI` object.
|
|
120
|
+
"""
|
|
121
|
+
self.command: list[str] | None = None
|
|
122
|
+
"""::
|
|
123
|
+
|
|
124
|
+
command = app.command
|
|
125
|
+
app.command = ['php-cgi']
|
|
126
|
+
|
|
127
|
+
Command that runs the script, with its file as the last argument, as an
|
|
128
|
+
interpreter, the script itself otherwise, which has to be executable.
|
|
129
|
+
With it, ``REDIRECT_STATUS`` is ``200``, as Apache gives it to the
|
|
130
|
+
command of ``Action``, without which php-cgi runs no script.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
self.document_root: str | os.PathLike[str] | None = None
|
|
134
|
+
"""::
|
|
135
|
+
|
|
136
|
+
root = app.document_root
|
|
137
|
+
app.document_root = '/usr/lib/cgi-bin'
|
|
138
|
+
|
|
139
|
+
Directory of the scripts, which takes the script of each request from
|
|
140
|
+
its path, as a web server does: the path after the mount, up to its
|
|
141
|
+
first part that is a file of the directory, or that ends with one of
|
|
142
|
+
:attr:`script_extensions`, names the script, whose file is
|
|
143
|
+
``SCRIPT_FILENAME`` and whose path after the mount is ``SCRIPT_NAME``,
|
|
144
|
+
the rest of the path is ``PATH_INFO``, and a path that ends with a
|
|
145
|
+
slash names ``index`` with the first of :attr:`script_extensions` in
|
|
146
|
+
it, as ``index.php``. A path with ``..``, or that names no file, or no
|
|
147
|
+
script of :attr:`script_extensions`, is not found. With :attr:`script`, a path that names no script of the
|
|
148
|
+
directory is the one of :attr:`script`, as the one entry of an
|
|
149
|
+
application that takes every path, as with ``try_files`` of nginx.
|
|
150
|
+
``SCRIPT_FILENAME`` of :attr:`env` takes its place.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
self.env: dict[str, str] = {}
|
|
154
|
+
"""::
|
|
155
|
+
|
|
156
|
+
env = app.env
|
|
157
|
+
app.env = {'PERL5LIB': '/usr/local/lib/perl5'}
|
|
158
|
+
|
|
159
|
+
Variables that are added to the environment of ``CGI`` of each
|
|
160
|
+
request, and replace the ones of the same name.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
self.inactivity_timeout: float = 60
|
|
164
|
+
"""::
|
|
165
|
+
|
|
166
|
+
timeout = app.inactivity_timeout
|
|
167
|
+
app.inactivity_timeout = 300
|
|
168
|
+
|
|
169
|
+
Maximum amount of time in seconds the standard output of the program
|
|
170
|
+
can be inactive before the process gets killed, defaults to ``60``.
|
|
171
|
+
Until the response starts, the connection of the client waits for it
|
|
172
|
+
without the inactivity timeout of the server, so that this one gives
|
|
173
|
+
the response ``502`` to a program that takes longer.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
self.nph: bool | None = None
|
|
177
|
+
"""::
|
|
178
|
+
|
|
179
|
+
boolean = app.nph
|
|
180
|
+
app.nph = True
|
|
181
|
+
|
|
182
|
+
The program writes the start line of an HTTP response, in non-parsed
|
|
183
|
+
header mode, as :mod:`Pyjo.Server.CGI` does with its attribute ``nph``,
|
|
184
|
+
which takes the place of the header ``Status``, true by default for a
|
|
185
|
+
script whose file name starts with ``nph-``, as web servers have it.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
self.script: str | None = None
|
|
189
|
+
"""::
|
|
190
|
+
|
|
191
|
+
script = app.script
|
|
192
|
+
app.script = '/usr/lib/cgi-bin/hello.cgi'
|
|
193
|
+
app.script = 'index.php'
|
|
194
|
+
|
|
195
|
+
Script of the application for every path, with the path after the
|
|
196
|
+
mount as ``PATH_INFO``, or for every path that names no script of
|
|
197
|
+
:attr:`document_root`, in which it is then.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
self.script_extensions: list[str] | None = None
|
|
201
|
+
"""::
|
|
202
|
+
|
|
203
|
+
extensions = app.script_extensions
|
|
204
|
+
app.script_extensions = ['.php']
|
|
205
|
+
|
|
206
|
+
Extensions of the names of the scripts of :attr:`document_root`, every
|
|
207
|
+
file of which is a script without them, as cgi-bin.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
self.sendfile: Sendfile | None = None
|
|
211
|
+
"""::
|
|
212
|
+
|
|
213
|
+
places = app.sendfile
|
|
214
|
+
app.sendfile = '/var/www/private'
|
|
215
|
+
app.sendfile = ['/var/www/private', '/var/www/media']
|
|
216
|
+
app.sendfile = re.compile(r'/var/www/[^/]+/private/.*')
|
|
217
|
+
|
|
218
|
+
Directories of the files that the program may reply with by the header
|
|
219
|
+
``X-Sendfile``, as Apache and lighttpd take it, with the path of the
|
|
220
|
+
file, absolute or in one of them, or regular expressions of the whole
|
|
221
|
+
path of such a file, once its symbolic links are resolved, as the
|
|
222
|
+
private files that are not in :attr:`document_root`, none by default,
|
|
223
|
+
which passes the header on as it is. A route that embeds the
|
|
224
|
+
application replies then with the file, as the helper ``reply.file`` of
|
|
225
|
+
:mod:`Pyjoyment` does, with the status and the other headers of the
|
|
226
|
+
program, and a file that they do not allow is not found.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
self.static: bool = False
|
|
230
|
+
"""::
|
|
231
|
+
|
|
232
|
+
boolean = app.static
|
|
233
|
+
app.static = True
|
|
234
|
+
|
|
235
|
+
Reply with a file of :attr:`document_root` for a path that names it and
|
|
236
|
+
no script of :attr:`script_extensions`, as ``try_files`` of nginx does,
|
|
237
|
+
as the helper ``reply.file`` of :mod:`Pyjoyment` does, with ranges and
|
|
238
|
+
conditional requests, false by default. A route that embeds the
|
|
239
|
+
application replies with them, as :mod:`Pyjoyment.Plugin.Mount` makes.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
super().__init__(attrs, **kwargs)
|
|
243
|
+
|
|
244
|
+
def handler(self, tx: Any) -> None:
|
|
245
|
+
"""::
|
|
246
|
+
|
|
247
|
+
app.handler(Pyjo.Transaction.HTTP.new())
|
|
248
|
+
app.handler(Pyjoyment.Controller.new())
|
|
249
|
+
|
|
250
|
+
Run the program for the request of the transaction, or of the
|
|
251
|
+
controller of a route that embeds the application, and write its
|
|
252
|
+
response.
|
|
253
|
+
"""
|
|
254
|
+
if self.script is None and self.document_root is None and 'SCRIPT_FILENAME' not in self.env:
|
|
255
|
+
raise Pyjo.Exception.new('No CGI application')
|
|
256
|
+
config = {
|
|
257
|
+
'extensions': self.script_extensions,
|
|
258
|
+
'inactivity': self.inactivity_timeout,
|
|
259
|
+
'root': os.fspath(self.document_root) if self.document_root is not None else None,
|
|
260
|
+
'script': self.script,
|
|
261
|
+
'sendfile': _places(self.sendfile),
|
|
262
|
+
'static': self.static,
|
|
263
|
+
}
|
|
264
|
+
_Call.run(self, config, tx)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# A request to an application, whose response of CGI is written as it comes, which a subclass sends and reads
|
|
268
|
+
class _Response:
|
|
269
|
+
name = 'CGI'
|
|
270
|
+
|
|
271
|
+
# The path of the mount itself, which a server takes for the whole path in SCRIPT_NAME without PATH_INFO
|
|
272
|
+
mount_path = ''
|
|
273
|
+
|
|
274
|
+
def __init__(self, app: Any, config: dict[str, Any], tx: Any, c: Any) -> None:
|
|
275
|
+
self.app = app
|
|
276
|
+
self.extensions: list[str] | None = config['extensions']
|
|
277
|
+
self.inactivity: float = config['inactivity']
|
|
278
|
+
self.root: str | None = config['root']
|
|
279
|
+
self.script: str | None = config['script']
|
|
280
|
+
self.sendfile: list[str | re.Pattern[str]] | None = config['sendfile']
|
|
281
|
+
self.static: bool = config['static']
|
|
282
|
+
self.waiting: tuple[str, float] | None = None
|
|
283
|
+
self.c = c
|
|
284
|
+
self.tx = tx
|
|
285
|
+
self.aborted = False
|
|
286
|
+
self.chunked = False
|
|
287
|
+
self.closed = False
|
|
288
|
+
self.done = False
|
|
289
|
+
self.head = bytearray()
|
|
290
|
+
self.headers: list[tuple[str, str]] | None = None
|
|
291
|
+
self.nph = False
|
|
292
|
+
self.offset = 0
|
|
293
|
+
self.parser: Any = None
|
|
294
|
+
self.pending: list[bytes] = []
|
|
295
|
+
self.sent = 0
|
|
296
|
+
self.started = False
|
|
297
|
+
self.stream: Any = None
|
|
298
|
+
|
|
299
|
+
# The chunks that wait to be written to the client, after which its connection closes when it is closing
|
|
300
|
+
self.writing = 0
|
|
301
|
+
self.closing = False
|
|
302
|
+
|
|
303
|
+
# The request of the transaction, or of the controller of a route that embeds the application
|
|
304
|
+
@classmethod
|
|
305
|
+
def run(cls, app: Any, config: dict[str, Any], tx: Any, *args: Any) -> None:
|
|
306
|
+
c = None
|
|
307
|
+
if not isinstance(tx, Pyjo.Transaction.object):
|
|
308
|
+
c = tx
|
|
309
|
+
tx = c.tx
|
|
310
|
+
if not Pyjo.App._within(c): # pyright: ignore[reportPrivateUsage]
|
|
311
|
+
return c.helpers.reply.not_found()
|
|
312
|
+
c.render_later()
|
|
313
|
+
cls(app, config, tx, c, *args).start()
|
|
314
|
+
|
|
315
|
+
def start(self) -> None:
|
|
316
|
+
self.tx.on('finish', self._finish)
|
|
317
|
+
params = {**Pyjo.App._cgi(self.tx, self.c), **_extra(self.tx)} # pyright: ignore[reportPrivateUsage]
|
|
318
|
+
|
|
319
|
+
# Not the header Proxy, whose HTTP_PROXY an application may take for the proxy of its own requests, as httpoxy
|
|
320
|
+
# (CVE-2016-5385) does, as nginx and Apache leave it out
|
|
321
|
+
params.pop('HTTP_PROXY', None)
|
|
322
|
+
|
|
323
|
+
# A file of the directory of the scripts, as try_files of nginx serves it
|
|
324
|
+
root, extensions = self.root, self.extensions
|
|
325
|
+
if (c := self.c) is not None and self.static and root is not None and extensions is not None:
|
|
326
|
+
if (file := _file(root, params['PATH_INFO'], extensions)) is not None:
|
|
327
|
+
self.done = True
|
|
328
|
+
return c.helpers.reply.file(file)
|
|
329
|
+
|
|
330
|
+
env = self.app.env
|
|
331
|
+
|
|
332
|
+
# The script of the path in the directory of the scripts, or the one script for a path that names none
|
|
333
|
+
if root is not None and 'SCRIPT_FILENAME' not in env:
|
|
334
|
+
if not self._script(params, root):
|
|
335
|
+
return self._not_found()
|
|
336
|
+
|
|
337
|
+
else:
|
|
338
|
+
# The one script for every path
|
|
339
|
+
if (script := self.script) is not None and 'SCRIPT_FILENAME' not in env:
|
|
340
|
+
params['SCRIPT_FILENAME'] = _chars(script)
|
|
341
|
+
if not params['PATH_INFO'] and params['SCRIPT_NAME']:
|
|
342
|
+
params['PATH_INFO'] = self.mount_path
|
|
343
|
+
params.update(self._defaults())
|
|
344
|
+
params.update({name: _chars(value) for name, value in env.items()})
|
|
345
|
+
|
|
346
|
+
# The connection of the client waits for the response without its inactivity timeout, which the one of the
|
|
347
|
+
# application takes the place of, so that an application that takes longer gives 502
|
|
348
|
+
if (connection := self.tx.connection) is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
|
|
349
|
+
self.waiting = (connection, stream.timeout)
|
|
350
|
+
stream.timeout = 0
|
|
351
|
+
|
|
352
|
+
self._open(params)
|
|
353
|
+
|
|
354
|
+
# The connection of the client, once what has been written to it is
|
|
355
|
+
def _close(self) -> None:
|
|
356
|
+
if (connection := self.tx.connection) is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
|
|
357
|
+
stream.close_gracefully()
|
|
358
|
+
|
|
359
|
+
# The variables of the environment of the application before the ones of the attribute env
|
|
360
|
+
def _defaults(self) -> dict[str, str]:
|
|
361
|
+
return {}
|
|
362
|
+
|
|
363
|
+
# End of the response
|
|
364
|
+
def _end(self) -> None:
|
|
365
|
+
self.done = True
|
|
366
|
+
content = self.tx.res.content
|
|
367
|
+
(content.write_chunk if self.chunked else content.write)(b'')
|
|
368
|
+
self._rendered()
|
|
369
|
+
self._stop()
|
|
370
|
+
|
|
371
|
+
def _failed(self, message: str) -> None:
|
|
372
|
+
if self.done:
|
|
373
|
+
return
|
|
374
|
+
self.aborted = True
|
|
375
|
+
|
|
376
|
+
# The response has started, so the connection is closed, as the response cannot change, once the chunks that
|
|
377
|
+
# wait have been written
|
|
378
|
+
if self.started:
|
|
379
|
+
self.done = True
|
|
380
|
+
if not self.closed:
|
|
381
|
+
self.app.log.error(message)
|
|
382
|
+
if self.writing:
|
|
383
|
+
self.closing = True
|
|
384
|
+
else:
|
|
385
|
+
self._close()
|
|
386
|
+
return self._stop()
|
|
387
|
+
|
|
388
|
+
self.done = True
|
|
389
|
+
self._stop()
|
|
390
|
+
self._waited()
|
|
391
|
+
self.app.log.error(message)
|
|
392
|
+
res = self.tx.res
|
|
393
|
+
res.code = 502
|
|
394
|
+
res.headers.content_type = 'text/plain'
|
|
395
|
+
res.body = b'Bad Gateway'
|
|
396
|
+
self._rendered()
|
|
397
|
+
|
|
398
|
+
# The end of the transaction, also when the connection of the client is gone before the end of the response
|
|
399
|
+
def _finish(self, *args: Any) -> None:
|
|
400
|
+
self.closed = True
|
|
401
|
+
if not self.done:
|
|
402
|
+
self.aborted = True
|
|
403
|
+
self._stop()
|
|
404
|
+
|
|
405
|
+
# The response of CGI of the output that has come
|
|
406
|
+
def _flush(self, ended: bool) -> None:
|
|
407
|
+
if self.headers is None:
|
|
408
|
+
if ended:
|
|
409
|
+
self._failed(f'{self.name} application ended without a response')
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Whole response
|
|
413
|
+
res = self.tx.res
|
|
414
|
+
if not self.started:
|
|
415
|
+
self.started = True
|
|
416
|
+
self._waited()
|
|
417
|
+
if self._sendfile():
|
|
418
|
+
return
|
|
419
|
+
_status(res, self.headers)
|
|
420
|
+
if ended:
|
|
421
|
+
self.done = True
|
|
422
|
+
res.body = b''.join(self.pending)
|
|
423
|
+
self.pending = []
|
|
424
|
+
self._rendered()
|
|
425
|
+
return self._stop()
|
|
426
|
+
|
|
427
|
+
# Response as it comes, with the headers only for a HEAD request
|
|
428
|
+
self.chunked = res.headers.content_length is None and self.tx.req.version == '1.1'
|
|
429
|
+
if self.tx.is_empty:
|
|
430
|
+
return self._end()
|
|
431
|
+
|
|
432
|
+
if len(self.pending):
|
|
433
|
+
chunk, self.pending = b''.join(self.pending), []
|
|
434
|
+
content = res.content
|
|
435
|
+
stream = self.stream
|
|
436
|
+
|
|
437
|
+
# The next output once the chunk has been written to the client
|
|
438
|
+
def drained(*args: Any) -> None:
|
|
439
|
+
self.writing -= 1
|
|
440
|
+
if self.closing and not self.writing:
|
|
441
|
+
return self._close()
|
|
442
|
+
if (stream := self.stream) is not None and not self.done:
|
|
443
|
+
stream.start()
|
|
444
|
+
|
|
445
|
+
if stream is not None and not ended:
|
|
446
|
+
stream.stop()
|
|
447
|
+
self.sent += len(chunk)
|
|
448
|
+
self.writing += 1
|
|
449
|
+
(content.write_chunk if self.chunked else content.write)(chunk, drained)
|
|
450
|
+
self._rendered()
|
|
451
|
+
if ended:
|
|
452
|
+
self._end()
|
|
453
|
+
|
|
454
|
+
def _not_found(self) -> None:
|
|
455
|
+
self.done = True
|
|
456
|
+
if (c := self.c) is not None:
|
|
457
|
+
return c.helpers.reply.not_found()
|
|
458
|
+
res = self.tx.res
|
|
459
|
+
res.code = 404
|
|
460
|
+
res.headers.content_type = 'text/plain'
|
|
461
|
+
res.body = b'Not Found'
|
|
462
|
+
self.tx.resume()
|
|
463
|
+
|
|
464
|
+
# The request to the application, with its parameters
|
|
465
|
+
def _open(self, params: dict[str, str]) -> None:
|
|
466
|
+
raise NotImplementedError('Method "_open" not implemented by subclass')
|
|
467
|
+
|
|
468
|
+
def _rendered(self) -> None:
|
|
469
|
+
if (c := self.c) is not None:
|
|
470
|
+
c.rendered()
|
|
471
|
+
else:
|
|
472
|
+
self.tx.resume()
|
|
473
|
+
|
|
474
|
+
# The script of the path in the directory of the scripts, as nginx has it with fastcgi_split_path_info and
|
|
475
|
+
# $document_root$fastcgi_script_name, or the one script for a path that names none
|
|
476
|
+
def _script(self, params: dict[str, str], root: str) -> bool:
|
|
477
|
+
extensions = self.extensions or []
|
|
478
|
+
if (script := self.script) is not None and not _names_script(params['PATH_INFO'], extensions):
|
|
479
|
+
params['PATH_INFO'] = f'/{script.lstrip("/")}'
|
|
480
|
+
return _script(params, root, extensions)
|
|
481
|
+
|
|
482
|
+
# The file of the header X-Sendfile of the application, which sendfile allows
|
|
483
|
+
def _sendfile(self) -> bool:
|
|
484
|
+
if (c := self.c) is None or not (places := self.sendfile) or (headers := self.headers) is None:
|
|
485
|
+
return False
|
|
486
|
+
if (value := next((value for name, value in headers if name.lower() == 'x-sendfile'), None)) is None:
|
|
487
|
+
return False
|
|
488
|
+
self.done = True
|
|
489
|
+
self._stop()
|
|
490
|
+
if (file := _allowed(places, value)) is None:
|
|
491
|
+
self.app.log.error(f'{self.name} application sent the file "{value}", which sendfile does not allow')
|
|
492
|
+
c.helpers.reply.not_found()
|
|
493
|
+
return True
|
|
494
|
+
_status(self.tx.res, [(name, value) for name, value in headers if name.lower() not in _SENT])
|
|
495
|
+
c.helpers.reply.file(file)
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
# The body of a response of non-parsed header mode, without its transfer encoding
|
|
499
|
+
def _body(self, content: Any, chunk: bytes) -> None:
|
|
500
|
+
if len(chunk):
|
|
501
|
+
self.pending.append(chunk)
|
|
502
|
+
|
|
503
|
+
# The HTTP response of non-parsed header mode, whose status takes the place of the header Status, and whose body is
|
|
504
|
+
# the one of its length, of its chunks, or up to the end of the output
|
|
505
|
+
def _nph(self, chunk: bytes) -> None:
|
|
506
|
+
if (parser := self.parser) is None:
|
|
507
|
+
self.parser = parser = Pyjo.Message.Response.new()
|
|
508
|
+
parser.content.unsubscribe('read').on('read', self._body)
|
|
509
|
+
if self.tx.is_empty:
|
|
510
|
+
parser.content.skip_body = True
|
|
511
|
+
parser.parse(chunk)
|
|
512
|
+
if (error := parser.error) is not None:
|
|
513
|
+
return self._failed(f'{self.name} application sent an invalid response: {error["message"]}')
|
|
514
|
+
if self.headers is None and (parser.content.is_parsing_body or parser.is_finished):
|
|
515
|
+
headers = parser.headers
|
|
516
|
+
self.headers = [('Status', f'{parser.code} {parser.message or ""}'.rstrip())] + [
|
|
517
|
+
(name, value) for name in headers.names() for value in headers.every_header(name)
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
# The headers of the response of CGI, and the start of its body
|
|
521
|
+
def _stdout(self, content: bytes) -> None:
|
|
522
|
+
if self.nph:
|
|
523
|
+
return self._nph(content)
|
|
524
|
+
if self.headers is not None:
|
|
525
|
+
if len(content):
|
|
526
|
+
self.pending.append(content)
|
|
527
|
+
return
|
|
528
|
+
self.head += content
|
|
529
|
+
if (m := re.search(rb'\r?\n\r?\n', self.head)) is None:
|
|
530
|
+
return
|
|
531
|
+
head, body = bytes(self.head[: m.start()]), bytes(self.head[m.end() :])
|
|
532
|
+
self.head = bytearray()
|
|
533
|
+
lines = re.split(r'\r?\n', head.decode('latin-1'))
|
|
534
|
+
self.headers = [
|
|
535
|
+
(name.strip(), value.strip()) for name, _, value in (line.partition(':') for line in lines) if name.strip()
|
|
536
|
+
]
|
|
537
|
+
if len(body):
|
|
538
|
+
self.pending.append(body)
|
|
539
|
+
|
|
540
|
+
# The end of the request to the application
|
|
541
|
+
def _stop(self) -> None:
|
|
542
|
+
raise NotImplementedError('Method "_stop" not implemented by subclass')
|
|
543
|
+
|
|
544
|
+
# The inactivity timeout of the connection of the client again, once the response starts
|
|
545
|
+
def _waited(self) -> None:
|
|
546
|
+
if (waiting := self.waiting) is not None:
|
|
547
|
+
self.waiting = None
|
|
548
|
+
connection, timeout = waiting
|
|
549
|
+
if (stream := Pyjo.IOLoop.stream(connection)) is not None:
|
|
550
|
+
stream.timeout = timeout
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# A request to a program, in a process of its own
|
|
554
|
+
class _Call(_Response):
|
|
555
|
+
def __init__(self, app: Pyjo_App_CGI, config: dict[str, Any], tx: Any, c: Any) -> None:
|
|
556
|
+
super().__init__(app, config, tx, c)
|
|
557
|
+
self.child: Pyjo.IOLoop.ChildProcess.object | None = None
|
|
558
|
+
self.command = app.command
|
|
559
|
+
|
|
560
|
+
def _closed(self, stream: Any) -> None:
|
|
561
|
+
self.stream = None
|
|
562
|
+
if self.done or self.closed:
|
|
563
|
+
return
|
|
564
|
+
self._flush(True)
|
|
565
|
+
|
|
566
|
+
def _defaults(self) -> dict[str, str]:
|
|
567
|
+
defaults = {'PATH': path} if (path := os.environ.get('PATH')) is not None else {}
|
|
568
|
+
if self.command is not None:
|
|
569
|
+
defaults['REDIRECT_STATUS'] = '200'
|
|
570
|
+
return defaults
|
|
571
|
+
|
|
572
|
+
def _error(self, stream: Any, err: Any) -> None:
|
|
573
|
+
self._failed(f'CGI application failed: {err}')
|
|
574
|
+
|
|
575
|
+
# The standard error of the program
|
|
576
|
+
def _log(self, stream: Any, chunk: bytes) -> None:
|
|
577
|
+
if message := chunk.decode('utf-8', 'replace').rstrip():
|
|
578
|
+
self.app.log.warn(f'CGI application: {message}')
|
|
579
|
+
|
|
580
|
+
# The program, or the command that runs the script, in the directory of its file
|
|
581
|
+
def _open(self, params: dict[str, str]) -> None:
|
|
582
|
+
file = params['SCRIPT_FILENAME'].encode('latin-1')
|
|
583
|
+
nph = self.app.nph
|
|
584
|
+
self.nph = os.path.basename(file).startswith(b'nph-') if nph is None else nph
|
|
585
|
+
env = {name.encode('latin-1'): value.encode('latin-1') for name, value in params.items()}
|
|
586
|
+
self.child = child = Pyjo.IOLoop.spawn(
|
|
587
|
+
[*(self.command or []), file], cwd=os.path.dirname(file) or None, detached=True, env=env
|
|
588
|
+
)
|
|
589
|
+
child.on('error', self._unstarted)
|
|
590
|
+
if (stream := child.stdout) is None or (errors := child.stderr) is None:
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
# The standard output, the response, and the standard error, which is logged until its end
|
|
594
|
+
self.stream = stream
|
|
595
|
+
stream.timeout = self.inactivity
|
|
596
|
+
stream.on('read', self._read)
|
|
597
|
+
stream.on('close', self._closed)
|
|
598
|
+
stream.on('error', self._error)
|
|
599
|
+
stream.on('timeout', self._timeout)
|
|
600
|
+
errors.on('read', self._log)
|
|
601
|
+
self._write()
|
|
602
|
+
|
|
603
|
+
def _read(self, stream: Any, chunk: bytes) -> None:
|
|
604
|
+
if self.done:
|
|
605
|
+
return
|
|
606
|
+
self._stdout(chunk)
|
|
607
|
+
if not self.done:
|
|
608
|
+
self._flush(False)
|
|
609
|
+
|
|
610
|
+
# The program of the first parts of the path that are a file of the directory, as cgi-bin, or of the path up to its
|
|
611
|
+
# first part with one of the extensions, which is a file of the directory, as the command runs no other file
|
|
612
|
+
def _script(self, params: dict[str, str], root: str) -> bool:
|
|
613
|
+
if (extensions := self.extensions) is not None:
|
|
614
|
+
if not super()._script(params, root):
|
|
615
|
+
return False
|
|
616
|
+
return _is_script(params['SCRIPT_FILENAME'], extensions) and _program(params, root)
|
|
617
|
+
for path in (params['PATH_INFO'], f'/{self.script.lstrip("/")}' if self.script is not None else None):
|
|
618
|
+
if path is None:
|
|
619
|
+
continue
|
|
620
|
+
parts = (path or '/').split('/')
|
|
621
|
+
if '..' in parts:
|
|
622
|
+
return False
|
|
623
|
+
for end in range(2, len(parts) + 1):
|
|
624
|
+
if _inside(root, os.fsdecode('/'.join(parts[1:end]).encode('latin-1'))) is not None:
|
|
625
|
+
_split(params, root, parts, end)
|
|
626
|
+
return True
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
# The end of the request, with the process of a response that has failed, or of a client that is gone, which the
|
|
630
|
+
# end of its output does not end
|
|
631
|
+
def _stop(self) -> None:
|
|
632
|
+
if (stream := self.stream) is not None:
|
|
633
|
+
self.stream = None
|
|
634
|
+
stream.close()
|
|
635
|
+
if (child := self.child) is not None:
|
|
636
|
+
self.child = None
|
|
637
|
+
if (stdin := child.stdin) is not None:
|
|
638
|
+
stdin.close()
|
|
639
|
+
if self.aborted:
|
|
640
|
+
child.kill(signal.SIGKILL)
|
|
641
|
+
|
|
642
|
+
def _timeout(self, stream: Any) -> None:
|
|
643
|
+
self._failed('CGI application inactive')
|
|
644
|
+
|
|
645
|
+
def _unstarted(self, child: Any, err: Any) -> None:
|
|
646
|
+
self._failed(f"Can't run CGI application: {getattr(err, 'strerror', None) or err}")
|
|
647
|
+
|
|
648
|
+
# The body of the request on the standard input, a chunk once the one before has been written, and its end
|
|
649
|
+
def _write(self, *args: Any) -> None:
|
|
650
|
+
if (child := self.child) is None or (stdin := child.stdin) is None:
|
|
651
|
+
return
|
|
652
|
+
chunk = self.tx.req.content.get_body_chunk(self.offset) or b''
|
|
653
|
+
if not len(chunk):
|
|
654
|
+
return stdin.close_gracefully()
|
|
655
|
+
self.offset += len(chunk)
|
|
656
|
+
stdin.write(chunk, self._write)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# A file that one of the directories has, with an absolute path or one in it, or that a regular expression matches,
|
|
660
|
+
# once its symbolic links are resolved
|
|
661
|
+
def _allowed(places: list[str | re.Pattern[str]], path: str) -> str | None:
|
|
662
|
+
for place in places:
|
|
663
|
+
if isinstance(place, str):
|
|
664
|
+
if (file := _inside(place, path)) is not None:
|
|
665
|
+
return file
|
|
666
|
+
elif os.path.isabs(path) and os.path.isfile(file := os.path.realpath(path)) and place.fullmatch(file):
|
|
667
|
+
return file
|
|
668
|
+
return None
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# Characters of the bytes of a string of the system, as the parameters have them
|
|
672
|
+
def _chars(value: str) -> str:
|
|
673
|
+
return os.fsencode(value).decode('latin-1')
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# The parameters that CGI servers add, which the applications of PHP need
|
|
677
|
+
def _extra(tx: Any) -> dict[str, str]:
|
|
678
|
+
req = tx.req
|
|
679
|
+
url = req.url
|
|
680
|
+
uri = url.path.to_abs_str()
|
|
681
|
+
if base := url.base.path.to_str().rstrip('/'):
|
|
682
|
+
uri = base + uri
|
|
683
|
+
if query := url.query.to_str():
|
|
684
|
+
uri += f'?{query}'
|
|
685
|
+
extra = {'GATEWAY_INTERFACE': 'CGI/1.1', 'REQUEST_URI': uri, 'SERVER_SOFTWARE': 'Pyjoyment (Python)'}
|
|
686
|
+
if req.is_secure:
|
|
687
|
+
extra['HTTPS'] = 'on'
|
|
688
|
+
return extra
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
# A file of the directory for a path that names no script and no directory, as try_files of nginx takes it
|
|
692
|
+
def _file(root: str, path: str, extensions: list[str]) -> str | None:
|
|
693
|
+
if not path or path.endswith('/') or _names_script(path, extensions) or '..' in path.split('/'):
|
|
694
|
+
return None
|
|
695
|
+
return _inside(root, os.fsdecode(path.lstrip('/').encode('latin-1')))
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# Directories or regular expressions, as a list
|
|
699
|
+
def _places(value: Sendfile | None) -> list[str | re.Pattern[str]] | None:
|
|
700
|
+
if value is None:
|
|
701
|
+
return None
|
|
702
|
+
places = [value] if isinstance(value, (str, os.PathLike, re.Pattern)) else list(value)
|
|
703
|
+
return [place if isinstance(place, re.Pattern) else os.fspath(place) for place in places]
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
# A file in a directory, also once its symbolic links are resolved, so that no link leads out of it
|
|
707
|
+
def _inside(directory: str, path: str) -> str | None:
|
|
708
|
+
base = os.path.realpath(directory)
|
|
709
|
+
file = os.path.realpath(os.path.join(base, path))
|
|
710
|
+
if os.path.commonpath([base, file]) != base or not os.path.isfile(file):
|
|
711
|
+
return None
|
|
712
|
+
return file
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# A program of the directory of the scripts
|
|
716
|
+
def _program(params: dict[str, str], root: str) -> bool:
|
|
717
|
+
return _inside(root, os.fsdecode(params['SCRIPT_FILENAME'].encode('latin-1'))) is not None
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _is_script(part: str, extensions: list[str]) -> bool:
|
|
721
|
+
return any(part.endswith(extension) for extension in extensions)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
# A path that names a script, by the extension of one of its parts
|
|
725
|
+
def _names_script(path: str, extensions: list[str]) -> bool:
|
|
726
|
+
return any(_is_script(part, extensions) for part in path.split('/'))
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
# The script of the path in the directory of the scripts, up to its first part with one of the extensions, unless the
|
|
730
|
+
# path has ..
|
|
731
|
+
def _script(params: dict[str, str], root: str, extensions: list[str]) -> bool:
|
|
732
|
+
parts = (params['PATH_INFO'] or '/').split('/')
|
|
733
|
+
if '..' in parts:
|
|
734
|
+
return False
|
|
735
|
+
end = next((i + 1 for i, part in enumerate(parts) if _is_script(part, extensions)), len(parts))
|
|
736
|
+
_split(params, root, parts, end)
|
|
737
|
+
if end == len(parts) and params['SCRIPT_FILENAME'].endswith('/') and extensions:
|
|
738
|
+
params['SCRIPT_FILENAME'] += f'index{extensions[0]}'
|
|
739
|
+
params['SCRIPT_NAME'] += f'index{extensions[0]}'
|
|
740
|
+
return True
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# The script of the first parts of the path, and the rest of the path
|
|
744
|
+
def _split(params: dict[str, str], root: str, parts: list[str], end: int) -> None:
|
|
745
|
+
script, rest = '/'.join(parts[:end]), '/'.join(parts[end:])
|
|
746
|
+
root = _chars(root)
|
|
747
|
+
params['SCRIPT_FILENAME'] = root.rstrip('/') + script
|
|
748
|
+
params['SCRIPT_NAME'] += script
|
|
749
|
+
params['PATH_INFO'] = f'/{rest}' if end < len(parts) else ''
|
|
750
|
+
params['DOCUMENT_ROOT'] = root
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
# The status of a response of CGI from its header Status, or a redirect for a header Location, and its other headers
|
|
754
|
+
def _status(res: Any, headers: list[tuple[str, str]]) -> None:
|
|
755
|
+
res.code = 200
|
|
756
|
+
location = False
|
|
757
|
+
for name, value in headers:
|
|
758
|
+
if name.lower() == 'status':
|
|
759
|
+
code, _, message = value.partition(' ')
|
|
760
|
+
res.code = int(code)
|
|
761
|
+
res.message = message or None
|
|
762
|
+
continue
|
|
763
|
+
if name.lower() == 'location':
|
|
764
|
+
location = True
|
|
765
|
+
if not wsgiref.util.is_hop_by_hop(name):
|
|
766
|
+
res.headers.add(name, value)
|
|
767
|
+
if location and not any(name.lower() == 'status' for name, _ in headers):
|
|
768
|
+
res.code = 302
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
new = Pyjo_App_CGI.new
|
|
772
|
+
object = Pyjo_App_CGI
|
|
773
|
+
CGI = Pyjo_App_CGI
|