1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
|
# Copyright 2013 by Rackspace Hosting, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""WSGI BoundedStream class."""
from __future__ import annotations
import io
from typing import BinaryIO, Callable, List, Optional, TypeVar, Union
from falcon.util import deprecated
__all__ = ('BoundedStream',)
Result = TypeVar('Result', bound=Union[bytes, List[bytes]])
class BoundedStream(io.IOBase):
"""Wrap *wsgi.input* streams to make them more robust.
``socket._fileobject`` and ``io.BufferedReader`` are sometimes used
to implement *wsgi.input*. However, app developers are often burned
by the fact that the `read()` method for these objects block
indefinitely if either no size is passed, or a size greater than
the request's content length is passed to the method.
This class normalizes *wsgi.input* behavior between WSGI servers
by implementing non-blocking behavior for the cases mentioned
above. The caller is not allowed to read more than the number of
bytes specified by the Content-Length header in the request.
Args:
stream: Instance of ``socket._fileobject`` from
``environ['wsgi.input']``
stream_len: Expected content length of the stream.
"""
def __init__(self, stream: BinaryIO, stream_len: int) -> None:
self.stream = stream
self.stream_len = stream_len
self._bytes_remaining = self.stream_len
def __iter__(self) -> BoundedStream:
return self
def __next__(self) -> bytes:
return next(self.stream)
next = __next__
def _read(self, size: Optional[int], target: Callable[[int], Result]) -> Result:
"""Proxy reads to the underlying stream.
Args:
size (int): Maximum number of bytes to read. Will be
coerced, if None or -1, to the number of remaining bytes
in the stream. Will likewise be coerced if greater than
the number of remaining bytes, to avoid making a
blocking call to the wrapped stream.
target (callable): Once `size` has been fixed up, this function
will be called to actually do the work.
Returns:
bytes: Data read from the stream, as returned by `target`.
"""
# NOTE(kgriffs): Default to reading all remaining bytes if the
# size is not specified or is out of bounds. This behaves
# similarly to the IO streams passed in by non-wsgiref servers.
if size is None or size == -1 or size > self._bytes_remaining:
size = self._bytes_remaining
self._bytes_remaining -= size
return target(size)
def readable(self) -> bool:
"""Return ``True`` always."""
return True
def seekable(self) -> bool:
"""Return ``False`` always."""
return False
def writable(self) -> bool:
"""Return ``False`` always."""
return False
def read(self, size: Optional[int] = None) -> bytes:
"""Read from the stream.
Args:
size (int): Maximum number of bytes/characters to read.
Defaults to reading until EOF.
Returns:
bytes: Data read from the stream.
"""
return self._read(size, self.stream.read)
def readline(self, limit: Optional[int] = None) -> bytes:
"""Read a line from the stream.
Args:
limit (int): Maximum number of bytes/characters to read.
Defaults to reading until EOF.
Returns:
bytes: Data read from the stream.
"""
return self._read(limit, self.stream.readline)
def readlines(self, hint: Optional[int] = None) -> List[bytes]:
"""Read lines from the stream.
Args:
hint (int): Maximum number of bytes/characters to read.
Defaults to reading until EOF.
Returns:
bytes: Data read from the stream.
"""
return self._read(hint, self.stream.readlines)
def write(self, data: bytes) -> None:
"""Raise IOError always; writing is not supported."""
raise IOError('Stream is not writeable')
def exhaust(self, chunk_size: int = 64 * 1024) -> None:
"""Exhaust the stream.
This consumes all the data left until the limit is reached.
Args:
chunk_size (int): The size for a chunk (default: 64 KB).
It will read the chunk until the stream is exhausted.
"""
while True:
chunk = self.read(chunk_size)
if not chunk:
break
@property
def eof(self) -> bool:
"""``True`` if there is no more data to read from the stream,
otherwise ``False``.
""" # noqa: D205
return self._bytes_remaining <= 0
@property
# NOTE(caselit): Deprecated long ago. Warns since 4.0.
@deprecated(
'Use `eof` instead. '
'(This compatibility alias will be removed in Falcon 5.0.)',
is_property=True,
)
def is_exhausted(self) -> bool:
"""Deprecated alias for `eof`."""
return self.eof
# NOTE(kgriffs): Alias for backwards-compat
Body = BoundedStream
|