sioscgi 4.1.0__tar.gz → 5.0.0__tar.gz
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.
- {sioscgi-4.1.0/src/sioscgi.egg-info → sioscgi-5.0.0}/PKG-INFO +1 -1
- {sioscgi-4.1.0 → sioscgi-5.0.0}/pyproject.toml +1 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/setup.cfg +1 -1
- sioscgi-5.0.0/src/sioscgi/__init__.py +6 -0
- sioscgi-5.0.0/src/sioscgi/request.py +627 -0
- sioscgi-5.0.0/src/sioscgi/response.py +312 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0/src/sioscgi.egg-info}/PKG-INFO +1 -1
- {sioscgi-4.1.0 → sioscgi-5.0.0}/src/sioscgi.egg-info/SOURCES.txt +4 -1
- sioscgi-5.0.0/tests/test_request.py +227 -0
- sioscgi-5.0.0/tests/test_response.py +173 -0
- sioscgi-4.1.0/src/sioscgi/__init__.py +0 -943
- sioscgi-4.1.0/tests/test_sioscgi.py +0 -479
- {sioscgi-4.1.0 → sioscgi-5.0.0}/CHANGELOG.rst +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/LICENSE +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/MANIFEST.in +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/README.rst +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/src/sioscgi/py.typed +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/src/sioscgi.egg-info/dependency_links.txt +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/src/sioscgi.egg-info/top_level.txt +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/src/sioscgi.egg-info/zip-safe +0 -0
- {sioscgi-4.1.0 → sioscgi-5.0.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Implements the request half of the SCGI protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections
|
|
6
|
+
import enum
|
|
7
|
+
import functools
|
|
8
|
+
import logging
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import ClassVar, NoReturn
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@enum.unique
|
|
14
|
+
class State(enum.Enum):
|
|
15
|
+
"""The possible states the receive half of the connection can be in."""
|
|
16
|
+
|
|
17
|
+
HEADER_LENGTH = enum.auto()
|
|
18
|
+
"""The length of the header block is being read."""
|
|
19
|
+
|
|
20
|
+
HEADERS = enum.auto()
|
|
21
|
+
"""The header block is being read."""
|
|
22
|
+
|
|
23
|
+
BODY = enum.auto()
|
|
24
|
+
"""The request body is being read."""
|
|
25
|
+
|
|
26
|
+
DONE = enum.auto()
|
|
27
|
+
"""The request body has finished being read."""
|
|
28
|
+
|
|
29
|
+
ERROR = enum.auto()
|
|
30
|
+
"""An error occurred."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Error(Exception):
|
|
34
|
+
"""
|
|
35
|
+
Raised when the remote peer violates protocol.
|
|
36
|
+
|
|
37
|
+
This is the base class of various specific errors and is never raised directly.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__slots__ = ()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HeadersError(Error):
|
|
44
|
+
"""
|
|
45
|
+
Raised when the remote peer sends an invalid environment block.
|
|
46
|
+
|
|
47
|
+
This is the base class of various specific errors and is never raised directly.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = ()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NetstringError(HeadersError):
|
|
54
|
+
"""
|
|
55
|
+
Raised when the remote peer sends an invalid netstring.
|
|
56
|
+
|
|
57
|
+
This is the base class of various specific errors and is never raised directly.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
__slots__ = ()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BadNetstringLengthError(NetstringError):
|
|
64
|
+
"""
|
|
65
|
+
Raised when the remote peer sends a netstring with bad length prefix.
|
|
66
|
+
|
|
67
|
+
This means that either the length is improperly encoded or that it is too large.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = ()
|
|
71
|
+
|
|
72
|
+
def __init__(self: BadNetstringLengthError) -> None:
|
|
73
|
+
"""Construct a new BadNetstringLengthError."""
|
|
74
|
+
super().__init__("Invalid netstring length prefix")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BadNetstringTerminatorError(NetstringError):
|
|
78
|
+
"""Raised when the remote peer sends a netstring with the wrong terminator."""
|
|
79
|
+
|
|
80
|
+
__slots__ = ()
|
|
81
|
+
|
|
82
|
+
def __init__(self: BadNetstringTerminatorError) -> None:
|
|
83
|
+
"""Construct a new BadNetstringTerminatorError."""
|
|
84
|
+
super().__init__("Invalid end-of-environment character")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HeadersStructuralError(HeadersError):
|
|
88
|
+
"""
|
|
89
|
+
Raised when the remote peer sends request headers with a structural problem.
|
|
90
|
+
|
|
91
|
+
This is the base class of various specific errors and is never raised directly.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
__slots__ = ()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HeadersNotNULTerminatedError(HeadersStructuralError):
|
|
98
|
+
"""Raised when the remote peer sends request headers whose last byte is not NUL."""
|
|
99
|
+
|
|
100
|
+
__slots__ = ()
|
|
101
|
+
|
|
102
|
+
def __init__(self: HeadersNotNULTerminatedError) -> None:
|
|
103
|
+
"""Construct a new HeadersNotNULTerminatedError."""
|
|
104
|
+
super().__init__("Environment block not NUL-terminated")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class HeadersOddStringCountError(HeadersStructuralError):
|
|
108
|
+
"""Raised when the remote peer sends an odd number of strings in the environment."""
|
|
109
|
+
|
|
110
|
+
__slots__ = ()
|
|
111
|
+
|
|
112
|
+
def __init__(self: HeadersOddStringCountError) -> None:
|
|
113
|
+
"""Construct a new HeadersOddStringCountError."""
|
|
114
|
+
super().__init__("Environment block missing final value")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class HeaderNotISO88591Error(HeadersStructuralError):
|
|
118
|
+
"""Raised when the remote peer sends a variable whose name is invalid ISO-8859-1."""
|
|
119
|
+
|
|
120
|
+
__slots__ = ()
|
|
121
|
+
|
|
122
|
+
def __init__(self: HeaderNotISO88591Error, name: bytes) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Construct a new HeaderNotISO88591Error.
|
|
125
|
+
|
|
126
|
+
:param name: The non-ISO-8859-1 variable name.
|
|
127
|
+
"""
|
|
128
|
+
super().__init__(f"Environment variable name {name!r} is not ISO-8859-1")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class HeaderEmptyError(HeadersStructuralError):
|
|
132
|
+
"""Raised when the remote peer sends a variable whose name is empty."""
|
|
133
|
+
|
|
134
|
+
__slots__ = ()
|
|
135
|
+
|
|
136
|
+
def __init__(self: HeaderEmptyError) -> None:
|
|
137
|
+
"""Construct a new HeaderEmptyError."""
|
|
138
|
+
super().__init__("Environment variable with empty name")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DuplicateHeaderError(HeadersStructuralError):
|
|
142
|
+
"""Raised when the remote peer sends two variables with the same name."""
|
|
143
|
+
|
|
144
|
+
__slots__ = ()
|
|
145
|
+
|
|
146
|
+
def __init__(self: DuplicateHeaderError, name: str) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Construct a new DuplicateHeaderError.
|
|
149
|
+
|
|
150
|
+
:param name: The name of the variable which appears multiple times.
|
|
151
|
+
"""
|
|
152
|
+
super().__init__(f"Duplicate environment variable {name}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class HeadersContentError(HeadersError):
|
|
156
|
+
"""
|
|
157
|
+
Raised when the remote peer sends request headers with a content problem.
|
|
158
|
+
|
|
159
|
+
This is the base class of various specific errors and is never raised directly.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
__slots__ = ()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class NoSCGIVariableError(HeadersContentError):
|
|
166
|
+
"""Raised when the remote peer does not send the SCGI variable."""
|
|
167
|
+
|
|
168
|
+
__slots__ = ()
|
|
169
|
+
|
|
170
|
+
def __init__(self: NoSCGIVariableError) -> None:
|
|
171
|
+
"""Construct a new NoSCGIVariableError."""
|
|
172
|
+
super().__init__("Mandatory variable SCGI missing")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class BadSCGIVersionError(HeadersContentError):
|
|
176
|
+
"""Raised when the remote peer is speaking an unsupported version of SCGI."""
|
|
177
|
+
|
|
178
|
+
__slots__ = ()
|
|
179
|
+
|
|
180
|
+
def __init__(self: BadSCGIVersionError, version: bytes) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Construct a new BadSCGIVersionError.
|
|
183
|
+
|
|
184
|
+
:param version: The version that the peer is speaking.
|
|
185
|
+
"""
|
|
186
|
+
super().__init__(f"SCGI variable is {version!r}, expected 1")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class NoContentLengthError(HeadersContentError):
|
|
190
|
+
"""Raised when the remote peer does not send the CONTENT_LENGTH variable."""
|
|
191
|
+
|
|
192
|
+
__slots__ = ()
|
|
193
|
+
|
|
194
|
+
def __init__(self: NoContentLengthError) -> None:
|
|
195
|
+
"""Construct a new NoContentLengthError."""
|
|
196
|
+
super().__init__("Mandatory variable CONTENT_LENGTH missing")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class BadContentLengthError(HeadersContentError):
|
|
200
|
+
"""Raised when the remote peer sends an invalid CONTENT_LENGTH value."""
|
|
201
|
+
|
|
202
|
+
__slots__ = ()
|
|
203
|
+
|
|
204
|
+
def __init__(self: BadContentLengthError, value: str) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Construct a new BadContentLengthError.
|
|
207
|
+
|
|
208
|
+
:param value: The invalid value.
|
|
209
|
+
"""
|
|
210
|
+
super().__init__(f"Invalid CONTENT_LENGTH {value}, expected a whole number")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RemotePrematureEOFError(Error):
|
|
214
|
+
"""Raised when the remote peer closes the connection before it should have."""
|
|
215
|
+
|
|
216
|
+
__slots__ = ()
|
|
217
|
+
|
|
218
|
+
def __init__(self: RemotePrematureEOFError) -> None:
|
|
219
|
+
"""Construct a new RemotePrematureEOFError."""
|
|
220
|
+
super().__init__("Premature EOF")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ReceiveAfterEOFError(Error):
|
|
224
|
+
"""Raised when data is received after EOF."""
|
|
225
|
+
|
|
226
|
+
__slots__ = ()
|
|
227
|
+
|
|
228
|
+
def __init__(self: ReceiveAfterEOFError) -> None:
|
|
229
|
+
"""Construct a new ReceiveAfterEOFError."""
|
|
230
|
+
super().__init__("Data received after EOF")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class Event:
|
|
234
|
+
"""The base class of all events returned by an SCGIReader."""
|
|
235
|
+
|
|
236
|
+
__slots__ = ()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Headers(Event):
|
|
240
|
+
"""Reports that a request has started and carries the environment data."""
|
|
241
|
+
|
|
242
|
+
__slots__ = {
|
|
243
|
+
"environment": """The environment variables, as a dict from name to value""",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
environment: dict[str, bytes]
|
|
247
|
+
|
|
248
|
+
def __init__(self: Headers, environment: dict[str, bytes]) -> None:
|
|
249
|
+
"""
|
|
250
|
+
Construct a new Headers.
|
|
251
|
+
|
|
252
|
+
:param environment: The environment variables, as a dict from name to value.
|
|
253
|
+
"""
|
|
254
|
+
self.environment = environment
|
|
255
|
+
|
|
256
|
+
def __repr__(self: Headers) -> str:
|
|
257
|
+
"""Return a representation of the environment."""
|
|
258
|
+
return f"Headers({self.environment})"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Body(Event):
|
|
262
|
+
"""
|
|
263
|
+
Transports some request body data.
|
|
264
|
+
|
|
265
|
+
In between a Headers and a End, zero or more Body events are delivered, each
|
|
266
|
+
carrying a chunk of the request body.
|
|
267
|
+
|
|
268
|
+
No Body event carries an empty chunk; consequently, a request without a body never
|
|
269
|
+
generates Body events.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
__slots__ = {
|
|
273
|
+
"data": """The body data chunk.""",
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
data: bytes
|
|
277
|
+
|
|
278
|
+
def __init__(self: Body, data: bytes) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Construct a new Body.
|
|
281
|
+
|
|
282
|
+
:param data: The body data chunk.
|
|
283
|
+
"""
|
|
284
|
+
self.data = data
|
|
285
|
+
|
|
286
|
+
def __repr__(self: Body) -> str:
|
|
287
|
+
"""Return a representation of the body data."""
|
|
288
|
+
return f"Body({self.data!r})"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class End(Event):
|
|
292
|
+
"""
|
|
293
|
+
Reports that a request has finished.
|
|
294
|
+
|
|
295
|
+
When this event occurs, both headers and all body data have been delivered in
|
|
296
|
+
preceding events. Once this event has been delivered, the application can start
|
|
297
|
+
sending the response.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
__slots__ = ()
|
|
301
|
+
|
|
302
|
+
def __repr__(self: End) -> str:
|
|
303
|
+
"""Return a representation of the end marker."""
|
|
304
|
+
return "End()"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SCGIReader:
|
|
308
|
+
"""
|
|
309
|
+
The read half of an SCGI connection.
|
|
310
|
+
|
|
311
|
+
This class implements the read half of the SCGI protocol as a sans-I/O state
|
|
312
|
+
machine, which simply translates chunks of bytes into sequences of protocol events.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
__slots__ = {
|
|
316
|
+
"_state": """
|
|
317
|
+
The state of the state machine.
|
|
318
|
+
|
|
319
|
+
This is the effective state after consuming all events in _event_queue but
|
|
320
|
+
before considering any bytes in _buffer.
|
|
321
|
+
""",
|
|
322
|
+
"_error": """A callable which, when called, raises the detected error.""",
|
|
323
|
+
"_event_queue": """
|
|
324
|
+
The decoded but not yet returned events.
|
|
325
|
+
|
|
326
|
+
These are events that have been received (via receive_data) but not yet
|
|
327
|
+
returned by next_event.
|
|
328
|
+
""",
|
|
329
|
+
"_buffer": """
|
|
330
|
+
The received but not yet decoded bytes.
|
|
331
|
+
|
|
332
|
+
These are bytes that have been received (via receive_data) but not yet
|
|
333
|
+
converted into events and pushed to _event_queue. In between calls to
|
|
334
|
+
receive_data, this is only the bytes making up a partial event (i.e. one for
|
|
335
|
+
which some, but not all, of the bytes have been received yet). During a call
|
|
336
|
+
to receive_data, it sometimes temporarily contains bytes making up one or
|
|
337
|
+
more complete events, plus possible extra residue at the end, until the
|
|
338
|
+
completed events are parsed and removed.
|
|
339
|
+
""",
|
|
340
|
+
"_buffer_length": """The total number of bytes in _buffer.""",
|
|
341
|
+
"_buffer_limit": """
|
|
342
|
+
The maximum size of _buffer in between calls to receive_bytes.
|
|
343
|
+
|
|
344
|
+
During a call to receive_bytes, the buffer may exceed this length, but only
|
|
345
|
+
temporarily until complete events are removed. Incomplete events must always
|
|
346
|
+
be within this limit.
|
|
347
|
+
""",
|
|
348
|
+
"_eof": """Whether an EOF has been reported via call to receive_bytes.""",
|
|
349
|
+
"_env_length": """The length of the headers block, once known.""",
|
|
350
|
+
"_body_remaining": """
|
|
351
|
+
The amount of request body not yet converted into events in _event_queue.
|
|
352
|
+
|
|
353
|
+
This includes both bytes in _buffer and bytes not yet received.
|
|
354
|
+
""",
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_state: State
|
|
358
|
+
_error: Callable[[], Error] | None
|
|
359
|
+
_event_queue: collections.deque[Event]
|
|
360
|
+
_buffer: collections.deque[bytes]
|
|
361
|
+
_buffer_length: int
|
|
362
|
+
_buffer_limit: int
|
|
363
|
+
_eof: bool
|
|
364
|
+
_env_length: int
|
|
365
|
+
_body_remaining: int
|
|
366
|
+
|
|
367
|
+
_MAX_NETSTRING_LENGTH_LENGTH: ClassVar[int] = 15
|
|
368
|
+
"""
|
|
369
|
+
The maximum length of the netstring length prefix.
|
|
370
|
+
|
|
371
|
+
If the length prefix is more than 15 characters long, then the netstring itself is
|
|
372
|
+
almost a TiB long or more, which is unreasonable.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(self: SCGIReader, rx_buffer_limit: int = 65536) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Construct a new SCGIReader.
|
|
378
|
+
|
|
379
|
+
:param rx_buffer_limit: The maximum number of received bytes that can be
|
|
380
|
+
buffered locally before being turned into an event; this value bounds the
|
|
381
|
+
size of request environment.
|
|
382
|
+
"""
|
|
383
|
+
super().__init__()
|
|
384
|
+
self._state = State.HEADER_LENGTH
|
|
385
|
+
self._error = None
|
|
386
|
+
self._event_queue = collections.deque()
|
|
387
|
+
self._buffer = collections.deque()
|
|
388
|
+
self._buffer_length = 0
|
|
389
|
+
self._buffer_limit = rx_buffer_limit
|
|
390
|
+
self._eof = False
|
|
391
|
+
self._env_length = 0
|
|
392
|
+
self._body_remaining = 0
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def state(self: SCGIReader) -> State:
|
|
396
|
+
"""
|
|
397
|
+
The state the state machine is currently in.
|
|
398
|
+
|
|
399
|
+
Events and state transitions are generated on receipt of data, not on call to
|
|
400
|
+
next_event, so this value reflects the state of the reader as it will be after
|
|
401
|
+
all events have been consumed.
|
|
402
|
+
"""
|
|
403
|
+
return self._state
|
|
404
|
+
|
|
405
|
+
def receive_data(self: SCGIReader, data: bytes) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Provide data received over the network to the SCGI reader.
|
|
408
|
+
|
|
409
|
+
:param data: The received bytes, or a zero-length bytes object if the remote
|
|
410
|
+
peer closed its end of the connection.
|
|
411
|
+
:raises ReceiveAfterEOFError: If this method is called again after first being
|
|
412
|
+
called with a zero-length parameter.
|
|
413
|
+
"""
|
|
414
|
+
if data:
|
|
415
|
+
if self._eof:
|
|
416
|
+
logging.getLogger(__name__).debug(
|
|
417
|
+
"Received %d bytes after EOF", len(data)
|
|
418
|
+
)
|
|
419
|
+
self._save_and_raise_error(ReceiveAfterEOFError)
|
|
420
|
+
if self._state is not State.ERROR:
|
|
421
|
+
logging.getLogger(__name__).debug("Received %d bytes", len(data))
|
|
422
|
+
self._buffer.append(data)
|
|
423
|
+
self._buffer_length += len(data)
|
|
424
|
+
else:
|
|
425
|
+
logging.getLogger(__name__).debug("Received EOF")
|
|
426
|
+
self._eof = True
|
|
427
|
+
# _parse_events raises an Error if the peer violates protocol. Such problems
|
|
428
|
+
# should not be reported via receive_data, but rather via next_event. Thus,
|
|
429
|
+
# should such an exception be raised, stash it away instead of propagating it.
|
|
430
|
+
try:
|
|
431
|
+
self._parse_events()
|
|
432
|
+
except Error:
|
|
433
|
+
# The error should already have been saved in self._error, so all we should
|
|
434
|
+
# need to do is swallow the exception to prevent it from coming out of the
|
|
435
|
+
# wrong method.
|
|
436
|
+
assert self._error is not None
|
|
437
|
+
|
|
438
|
+
def next_event(self: SCGIReader) -> Event | None:
|
|
439
|
+
"""
|
|
440
|
+
Return the next event in the event queue.
|
|
441
|
+
|
|
442
|
+
This method should generally be called repeatedly until it returns None after a
|
|
443
|
+
call to receive_data. However, it is legal to leave some events unprocessed
|
|
444
|
+
until a more convenient time, or even call receive_data again before receiving
|
|
445
|
+
all the events.
|
|
446
|
+
|
|
447
|
+
:raises Error: If an Error was previously raised by some other method of this
|
|
448
|
+
reader.
|
|
449
|
+
:raises Error: If the remote peer violated SCGI protocol rules.
|
|
450
|
+
"""
|
|
451
|
+
if self._state is State.ERROR:
|
|
452
|
+
assert self._error is not None # Implied by State.ERROR
|
|
453
|
+
raise self._error() # noqa: RSE102
|
|
454
|
+
if self._event_queue:
|
|
455
|
+
return self._event_queue.popleft()
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
def _parse_events(self: SCGIReader) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Remove bytes from the receive buffer and create events in the event queue.
|
|
461
|
+
|
|
462
|
+
:raises Error: If the remote peer violated SCGI protocol rules.
|
|
463
|
+
"""
|
|
464
|
+
# Throughout this method, we assume that at most one element has been added to
|
|
465
|
+
# the receive buffer; this is safe because this method is called from
|
|
466
|
+
# receive_data, so we eagerly parse as much as we can on every received chunk.
|
|
467
|
+
logger = logging.getLogger(__name__)
|
|
468
|
+
if self._state is State.HEADER_LENGTH:
|
|
469
|
+
logger.debug("In RX_HEADER_LENGTH")
|
|
470
|
+
if self._buffer:
|
|
471
|
+
# The length-of-environment integer ends with a colon.
|
|
472
|
+
index = self._buffer[-1].find(b":")
|
|
473
|
+
if (
|
|
474
|
+
index < 0
|
|
475
|
+
and self._buffer_length > self._MAX_NETSTRING_LENGTH_LENGTH
|
|
476
|
+
):
|
|
477
|
+
self._save_and_raise_error(BadNetstringLengthError)
|
|
478
|
+
if index >= 0:
|
|
479
|
+
logger.debug("Found : at %d", index)
|
|
480
|
+
# We have the full length-of-environment integer and its terminating
|
|
481
|
+
# colon. Split up received data into the length-of-environment
|
|
482
|
+
# integer, the colon (which we discard), and any bytes following the
|
|
483
|
+
# colon (residue).
|
|
484
|
+
residue = self._buffer[-1][index + 1 :]
|
|
485
|
+
if index == 0:
|
|
486
|
+
self._buffer.pop()
|
|
487
|
+
else:
|
|
488
|
+
self._buffer[-1] = self._buffer[-1][:index]
|
|
489
|
+
consumed = b"".join(self._buffer)
|
|
490
|
+
self._buffer.clear()
|
|
491
|
+
self._buffer_length = 0
|
|
492
|
+
# Parse the length-of-environment integer.
|
|
493
|
+
try:
|
|
494
|
+
self._env_length = int(consumed.decode("ASCII"))
|
|
495
|
+
except ValueError:
|
|
496
|
+
self._save_and_raise_error(BadNetstringLengthError)
|
|
497
|
+
# Sanity check the length-of-environment integer.
|
|
498
|
+
if self._env_length <= 0:
|
|
499
|
+
self._save_and_raise_error(BadNetstringLengthError)
|
|
500
|
+
if self._env_length > self._buffer_limit:
|
|
501
|
+
self._save_and_raise_error(BadNetstringLengthError)
|
|
502
|
+
# Advance the state machine, keeping any residual bytes.
|
|
503
|
+
self._state = State.HEADERS
|
|
504
|
+
if residue:
|
|
505
|
+
self._buffer.append(residue)
|
|
506
|
+
self._buffer_length += len(residue)
|
|
507
|
+
logger.debug(
|
|
508
|
+
"Length of headers is %d, residue is %d bytes",
|
|
509
|
+
self._env_length,
|
|
510
|
+
len(residue),
|
|
511
|
+
)
|
|
512
|
+
if self._state is State.HEADERS:
|
|
513
|
+
logger.debug("In RX_HEADERS")
|
|
514
|
+
if self._buffer_length > self._env_length:
|
|
515
|
+
# Split the receive buffer into the environment of the designated
|
|
516
|
+
# length, the comma, and any bytes following the comma (residue).
|
|
517
|
+
logger.debug("Got all headers")
|
|
518
|
+
last_chunk_start_pos = self._buffer_length - len(self._buffer[-1])
|
|
519
|
+
assert last_chunk_start_pos <= self._env_length
|
|
520
|
+
comma_pos = self._env_length - last_chunk_start_pos
|
|
521
|
+
comma = self._buffer[-1][comma_pos]
|
|
522
|
+
residue = self._buffer[-1][comma_pos + 1 :]
|
|
523
|
+
if last_chunk_start_pos == self._env_length:
|
|
524
|
+
self._buffer.pop()
|
|
525
|
+
else:
|
|
526
|
+
self._buffer[-1] = self._buffer[-1][:comma_pos]
|
|
527
|
+
environment = b"".join(self._buffer)
|
|
528
|
+
self._buffer.clear()
|
|
529
|
+
self._buffer_length = 0
|
|
530
|
+
# Check that the comma is a comma.
|
|
531
|
+
if comma != ord(","):
|
|
532
|
+
self._save_and_raise_error(BadNetstringTerminatorError)
|
|
533
|
+
# Check that the last byte of the environment block is a NUL
|
|
534
|
+
if environment[-1] != 0x00:
|
|
535
|
+
self._save_and_raise_error(HeadersNotNULTerminatedError)
|
|
536
|
+
# Split the environment block into NUL-terminated chunks.
|
|
537
|
+
split_environment = environment[:-1].split(b"\x00")
|
|
538
|
+
# Check that there are an even number of parts.
|
|
539
|
+
if len(split_environment) % 2 == 1:
|
|
540
|
+
self._save_and_raise_error(HeadersOddStringCountError)
|
|
541
|
+
# Build the dictionary.
|
|
542
|
+
env_dict: dict[str, bytes] = {}
|
|
543
|
+
for i in range(0, len(split_environment), 2):
|
|
544
|
+
try:
|
|
545
|
+
key = split_environment[i].decode("ISO-8859-1")
|
|
546
|
+
except UnicodeError:
|
|
547
|
+
self._save_and_raise_error(
|
|
548
|
+
functools.partial(
|
|
549
|
+
HeaderNotISO88591Error, split_environment[i]
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
if not key:
|
|
553
|
+
self._save_and_raise_error(HeaderEmptyError)
|
|
554
|
+
if key in env_dict:
|
|
555
|
+
self._save_and_raise_error(
|
|
556
|
+
functools.partial(DuplicateHeaderError, key)
|
|
557
|
+
)
|
|
558
|
+
env_dict[key] = split_environment[i + 1]
|
|
559
|
+
# Check for mandatory environment variables.
|
|
560
|
+
scgi_version = env_dict.get("SCGI", None)
|
|
561
|
+
if scgi_version is None:
|
|
562
|
+
self._save_and_raise_error(NoSCGIVariableError)
|
|
563
|
+
if scgi_version != b"1":
|
|
564
|
+
self._save_and_raise_error(
|
|
565
|
+
functools.partial(BadSCGIVersionError, scgi_version)
|
|
566
|
+
)
|
|
567
|
+
# Advance the state machine, keeping any residual bytes.
|
|
568
|
+
self._state = State.BODY
|
|
569
|
+
if residue:
|
|
570
|
+
self._buffer.append(residue)
|
|
571
|
+
self._buffer_length += len(residue)
|
|
572
|
+
content_length = env_dict.get("CONTENT_LENGTH", None)
|
|
573
|
+
if content_length is None:
|
|
574
|
+
self._save_and_raise_error(NoContentLengthError)
|
|
575
|
+
try:
|
|
576
|
+
self._body_remaining = int(content_length)
|
|
577
|
+
except ValueError:
|
|
578
|
+
self._body_remaining = -1
|
|
579
|
+
if self._body_remaining < 0:
|
|
580
|
+
self._save_and_raise_error(
|
|
581
|
+
functools.partial(BadContentLengthError, content_length)
|
|
582
|
+
)
|
|
583
|
+
self._event_queue.append(Headers(env_dict))
|
|
584
|
+
logger.debug(
|
|
585
|
+
"Retrieved %d headers, residue is %d bytes",
|
|
586
|
+
len(env_dict),
|
|
587
|
+
len(residue),
|
|
588
|
+
)
|
|
589
|
+
if self._state is State.BODY:
|
|
590
|
+
logger.debug(
|
|
591
|
+
"In RX_BODY, buffer length = %d, body remaining = %d",
|
|
592
|
+
self._buffer_length,
|
|
593
|
+
self._body_remaining,
|
|
594
|
+
)
|
|
595
|
+
while 0 < self._buffer_length <= self._body_remaining:
|
|
596
|
+
chunk = self._buffer.popleft()
|
|
597
|
+
self._event_queue.append(Body(chunk))
|
|
598
|
+
self._body_remaining -= len(chunk)
|
|
599
|
+
self._buffer_length -= len(chunk)
|
|
600
|
+
if 0 < self._body_remaining < self._buffer_length:
|
|
601
|
+
chunk = self._buffer.popleft()
|
|
602
|
+
self._event_queue.append(Body(chunk[: self._body_remaining]))
|
|
603
|
+
self._body_remaining = 0
|
|
604
|
+
self._buffer_length -= len(chunk)
|
|
605
|
+
if self._body_remaining == 0:
|
|
606
|
+
self._event_queue.append(End())
|
|
607
|
+
self._state = State.DONE
|
|
608
|
+
if self._state is State.DONE:
|
|
609
|
+
logger.debug("In RX_DONE")
|
|
610
|
+
self._buffer.clear()
|
|
611
|
+
self._buffer_length = 0
|
|
612
|
+
if self._eof and self._state in {
|
|
613
|
+
State.HEADER_LENGTH,
|
|
614
|
+
State.HEADERS,
|
|
615
|
+
State.BODY,
|
|
616
|
+
}:
|
|
617
|
+
self._save_and_raise_error(RemotePrematureEOFError)
|
|
618
|
+
|
|
619
|
+
def _save_and_raise_error(self: SCGIReader, error: Callable[[], Error]) -> NoReturn:
|
|
620
|
+
"""
|
|
621
|
+
Record and immediately raise a protocol error.
|
|
622
|
+
|
|
623
|
+
:param error: A callable that, when invoked, constructs the error.
|
|
624
|
+
"""
|
|
625
|
+
self._state = State.ERROR
|
|
626
|
+
self._error = error
|
|
627
|
+
raise error() # noqa: RSE102
|