diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2025-02-09 00:00:59 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2025-02-09 00:13:15 +0300 |
commit | 41e6f263ca838bea7b80defd749f859c860c5338 (patch) | |
tree | 547d5946e4292f99dad508a94c4cfdaaa8835444 /contrib/python | |
parent | d2703b22bd88ed7b99b83327594ef7cc3dc27f88 (diff) | |
download | ydb-41e6f263ca838bea7b80defd749f859c860c5338.tar.gz |
Intermediate changes
commit_hash:9f477c101838068c76e0d37a313a5e79a2888982
Diffstat (limited to 'contrib/python')
26 files changed, 233 insertions, 127 deletions
diff --git a/contrib/python/Werkzeug/py3/.dist-info/METADATA b/contrib/python/Werkzeug/py3/.dist-info/METADATA index a40cd1ba9f..647bfc8001 100644 --- a/contrib/python/Werkzeug/py3/.dist-info/METADATA +++ b/contrib/python/Werkzeug/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: Werkzeug -Version: 2.2.2 +Version: 2.2.3 Summary: The comprehensive WSGI web application library. Home-page: https://palletsprojects.com/p/werkzeug/ Author: Armin Ronacher diff --git a/contrib/python/Werkzeug/py3/werkzeug/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/__init__.py index fd7f8d229a..c20ac29bcb 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/__init__.py +++ b/contrib/python/Werkzeug/py3/werkzeug/__init__.py @@ -3,4 +3,4 @@ from .test import Client as Client from .wrappers import Request as Request from .wrappers import Response as Response -__version__ = "2.2.2" +__version__ = "2.2.3" diff --git a/contrib/python/Werkzeug/py3/werkzeug/_internal.py b/contrib/python/Werkzeug/py3/werkzeug/_internal.py index 4636647db3..f95207ab21 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/_internal.py +++ b/contrib/python/Werkzeug/py3/werkzeug/_internal.py @@ -34,7 +34,7 @@ _quote_re = re.compile(rb"[\\].") _legal_cookie_chars_re = rb"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" _cookie_re = re.compile( rb""" - (?P<key>[^=;]+) + (?P<key>[^=;]*) (?:\s*=\s* (?P<val> "(?:[^\\"]|\\.)*" | @@ -382,16 +382,21 @@ def _cookie_parse_impl(b: bytes) -> t.Iterator[t.Tuple[bytes, bytes]]: """Lowlevel cookie parsing facility that operates on bytes.""" i = 0 n = len(b) + b += b";" while i < n: - match = _cookie_re.search(b + b";", i) + match = _cookie_re.match(b, i) + if not match: break - key = match.group("key").strip() - value = match.group("val") or b"" i = match.end(0) + key = match.group("key").strip() + + if not key: + continue + value = match.group("val") or b"" yield key, _cookie_unquote(value) diff --git a/contrib/python/Werkzeug/py3/werkzeug/_reloader.py b/contrib/python/Werkzeug/py3/werkzeug/_reloader.py index 21ceb88d38..67614a78a7 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/_reloader.py +++ b/contrib/python/Werkzeug/py3/werkzeug/_reloader.py @@ -20,7 +20,7 @@ prefix = {*_ignore_always, sys.prefix, sys.exec_prefix} if hasattr(sys, "real_prefix"): # virtualenv < 20 - prefix.add(sys.real_prefix) # type: ignore[attr-defined] + prefix.add(sys.real_prefix) _stat_ignore_scan = tuple(prefix) del prefix @@ -309,7 +309,7 @@ class WatchdogReloaderLoop(ReloaderLoop): super().__init__(*args, **kwargs) trigger_reload = self.trigger_reload - class EventHandler(PatternMatchingEventHandler): # type: ignore + class EventHandler(PatternMatchingEventHandler): def on_any_event(self, event): # type: ignore trigger_reload(event.src_path) diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures.py index 43ee8c7545..a293dfddb5 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/datastructures.py +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures.py @@ -1226,7 +1226,7 @@ class Headers: (_unicodify_header_value(k), _unicodify_header_value(v)) for (k, v) in value ] - for (_, v) in value: + for _, v in value: self._validate_value(v) if isinstance(key, int): self._list[key] = value[0] diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py index e0dcc65fb4..24d19bbdad 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py @@ -329,7 +329,7 @@ class DebuggedApplication: app_iter = self.app(environ, start_response) yield from app_iter if hasattr(app_iter, "close"): - app_iter.close() # type: ignore + app_iter.close() except Exception as e: if hasattr(app_iter, "close"): app_iter.close() # type: ignore diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py b/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py index c0872f1808..d9c28da41b 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py @@ -132,7 +132,7 @@ class DebugReprGenerator: def regex_repr(self, obj: t.Pattern) -> str: pattern = repr(obj.pattern) - pattern = codecs.decode(pattern, "unicode-escape", "ignore") # type: ignore + pattern = codecs.decode(pattern, "unicode-escape", "ignore") pattern = f"r{pattern}" return f're.compile(<span class="string regex">{pattern}</span>)' diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py b/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py index ea90de9254..d56f7390a5 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py @@ -184,7 +184,7 @@ def _process_traceback( } if hasattr(fs, "colno"): - frame_args["colno"] = fs.colno # type: ignore[attr-defined] + frame_args["colno"] = fs.colno frame_args["end_colno"] = fs.end_colno # type: ignore[attr-defined] new_stack.append(DebugFrameSummary(**frame_args)) diff --git a/contrib/python/Werkzeug/py3/werkzeug/exceptions.py b/contrib/python/Werkzeug/py3/werkzeug/exceptions.py index 013df72bd3..739bd905ba 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/exceptions.py +++ b/contrib/python/Werkzeug/py3/werkzeug/exceptions.py @@ -205,7 +205,7 @@ class BadRequestKeyError(BadRequest, KeyError): KeyError.__init__(self, arg) @property # type: ignore - def description(self) -> str: # type: ignore + def description(self) -> str: if self.show_exception: return ( f"{self._description}\n" diff --git a/contrib/python/Werkzeug/py3/werkzeug/formparser.py b/contrib/python/Werkzeug/py3/werkzeug/formparser.py index 10d58ca3fa..bebb2fc8fe 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/formparser.py +++ b/contrib/python/Werkzeug/py3/werkzeug/formparser.py @@ -179,6 +179,8 @@ class FormDataParser: :param cls: an optional dict class to use. If this is not specified or `None` the default :class:`MultiDict` is used. :param silent: If set to False parsing errors will not be caught. + :param max_form_parts: The maximum number of parts to be parsed. If this is + exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised. """ def __init__( @@ -190,6 +192,8 @@ class FormDataParser: max_content_length: t.Optional[int] = None, cls: t.Optional[t.Type[MultiDict]] = None, silent: bool = True, + *, + max_form_parts: t.Optional[int] = None, ) -> None: if stream_factory is None: stream_factory = default_stream_factory @@ -199,6 +203,7 @@ class FormDataParser: self.errors = errors self.max_form_memory_size = max_form_memory_size self.max_content_length = max_content_length + self.max_form_parts = max_form_parts if cls is None: cls = MultiDict @@ -281,6 +286,7 @@ class FormDataParser: self.errors, max_form_memory_size=self.max_form_memory_size, cls=self.cls, + max_form_parts=self.max_form_parts, ) boundary = options.get("boundary", "").encode("ascii") @@ -346,10 +352,12 @@ class MultiPartParser: max_form_memory_size: t.Optional[int] = None, cls: t.Optional[t.Type[MultiDict]] = None, buffer_size: int = 64 * 1024, + max_form_parts: t.Optional[int] = None, ) -> None: self.charset = charset self.errors = errors self.max_form_memory_size = max_form_memory_size + self.max_form_parts = max_form_parts if stream_factory is None: stream_factory = default_stream_factory @@ -409,7 +417,9 @@ class MultiPartParser: [None], ) - parser = MultipartDecoder(boundary, self.max_form_memory_size) + parser = MultipartDecoder( + boundary, self.max_form_memory_size, max_parts=self.max_form_parts + ) fields = [] files = [] diff --git a/contrib/python/Werkzeug/py3/werkzeug/http.py b/contrib/python/Werkzeug/py3/werkzeug/http.py index 97776855d7..0a7bc739c5 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/http.py +++ b/contrib/python/Werkzeug/py3/werkzeug/http.py @@ -190,6 +190,15 @@ class COOP(Enum): SAME_ORIGIN = "same-origin" +def _is_extended_parameter(key: str) -> bool: + """Per RFC 5987/8187, "extended" values may *not* be quoted. + This is in keeping with browser implementations. So we test + using this function to see if the key indicates this parameter + follows the `ext-parameter` syntax (using a trailing '*'). + """ + return key.strip().endswith("*") + + def quote_header_value( value: t.Union[str, int], extra_chars: str = "", allow_token: bool = True ) -> str: @@ -254,6 +263,8 @@ def dump_options_header( for key, value in options.items(): if value is None: segments.append(key) + elif _is_extended_parameter(key): + segments.append(f"{key}={value}") else: segments.append(f"{key}={quote_header_value(value)}") return "; ".join(segments) @@ -282,6 +293,8 @@ def dump_header( for key, value in iterable.items(): if value is None: items.append(key) + elif _is_extended_parameter(key): + items.append(f"{key}={value}") else: items.append( f"{key}={quote_header_value(value, allow_token=allow_token)}" @@ -818,6 +831,9 @@ def parse_content_range_header( return None if rng == "*": + if not is_byte_range_valid(None, None, length): + return None + return ds.ContentRange(units, None, None, length, on_update=on_update) elif "-" not in rng: return None diff --git a/contrib/python/Werkzeug/py3/werkzeug/local.py b/contrib/python/Werkzeug/py3/werkzeug/local.py index 70e9bf72f2..9927a0a1f3 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/local.py +++ b/contrib/python/Werkzeug/py3/werkzeug/local.py @@ -291,7 +291,7 @@ class _ProxyLookup: # A C function, use partial to bind the first argument. def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: - return partial(f, obj) # type: ignore + return partial(f, obj) else: # Use getattr, which will produce a bound method. @@ -313,7 +313,7 @@ class _ProxyLookup: return self try: - obj = instance._get_current_object() # type: ignore[misc] + obj = instance._get_current_object() except RuntimeError: if self.fallback is None: raise diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py index 6b54630e6e..fcf3b41314 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py @@ -164,7 +164,7 @@ class GuardedIterator: self.closed = True if hasattr(self._iterator, "close"): - self._iterator.close() # type: ignore + self._iterator.close() if self.headers_set: status_code, headers = self.headers_set diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py index 200dae0341..f91e33b825 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py @@ -106,7 +106,7 @@ class ProfilerMiddleware: response_body.extend(app_iter) if hasattr(app_iter, "close"): - app_iter.close() # type: ignore + app_iter.close() profile = Profile() start = time.time() diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py b/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py index d22b05a5c9..05370c3e07 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py @@ -127,7 +127,14 @@ class StateMachineMatcher: remaining = [] match = re.compile(test_part.content).match(target) if match is not None: - rv = _match(new_state, remaining, values + list(match.groups())) + groups = list(match.groups()) + if test_part.suffixed: + # If a part_isolating=False part has a slash suffix, remove the + # suffix from the match and check for the slash redirect next. + suffix = groups.pop() + if suffix == "/": + remaining = [""] + rv = _match(new_state, remaining, values + groups) if rv is not None: return rv diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py b/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py index a61717ade8..7b37890abd 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py @@ -36,6 +36,7 @@ class RulePart: content: str final: bool static: bool + suffixed: bool weight: Weighting @@ -631,7 +632,11 @@ class Rule(RuleFactory): argument_weights, ) yield RulePart( - content=content, final=final, static=static, weight=weight + content=content, + final=final, + static=static, + suffixed=False, + weight=weight, ) content = "" static = True @@ -641,6 +646,12 @@ class Rule(RuleFactory): pos = match.end() + suffixed = False + if final and content[-1] == "/": + # If a converter is part_isolating=False (matches slashes) and ends with a + # slash, augment the regex to support slash redirects. + suffixed = True + content = content[:-1] + "(?<!/)(/?)" if not static: content += r"\Z" weight = Weighting( @@ -649,7 +660,17 @@ class Rule(RuleFactory): -len(argument_weights), argument_weights, ) - yield RulePart(content=content, final=final, static=static, weight=weight) + yield RulePart( + content=content, + final=final, + static=static, + suffixed=suffixed, + weight=weight, + ) + if suffixed: + yield RulePart( + content="", final=False, static=True, suffixed=False, weight=weight + ) def compile(self) -> None: """Compiles the regular expression and stores it.""" @@ -665,7 +686,11 @@ class Rule(RuleFactory): if domain_rule == "": self._parts = [ RulePart( - content="", final=False, static=True, weight=Weighting(0, [], 0, []) + content="", + final=False, + static=True, + suffixed=False, + weight=Weighting(0, [], 0, []), ) ] else: diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py index 8288882b72..6b22738328 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py @@ -126,10 +126,6 @@ def parse_cookie( def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]: for key, val in _cookie_parse_impl(cookie): # type: ignore key_str = _to_str(key, charset, errors, allow_none_charset=True) - - if not key_str: - continue - val_str = _to_str(val, charset, errors, allow_none_charset=True) yield key_str, val_str diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py index d8abeb3543..2684e5dd64 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py @@ -87,10 +87,13 @@ class MultipartDecoder: self, boundary: bytes, max_form_memory_size: Optional[int] = None, + *, + max_parts: Optional[int] = None, ) -> None: self.buffer = bytearray() self.complete = False self.max_form_memory_size = max_form_memory_size + self.max_parts = max_parts self.state = State.PREAMBLE self.boundary = boundary @@ -118,6 +121,7 @@ class MultipartDecoder: re.MULTILINE, ) self._search_position = 0 + self._parts_decoded = 0 def last_newline(self) -> int: try: @@ -191,6 +195,10 @@ class MultipartDecoder: ) self.state = State.DATA self._search_position = 0 + self._parts_decoded += 1 + + if self.max_parts is not None and self._parts_decoded > self.max_parts: + raise RequestEntityTooLarge() else: # Update the search start position to be equal to the # current buffer length (already searched) minus a diff --git a/contrib/python/Werkzeug/py3/werkzeug/security.py b/contrib/python/Werkzeug/py3/werkzeug/security.py index 18d0919f83..4599fb3e1d 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/security.py +++ b/contrib/python/Werkzeug/py3/werkzeug/security.py @@ -12,7 +12,7 @@ SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" DEFAULT_PBKDF2_ITERATIONS = 260000 _os_alt_seps: t.List[str] = list( - sep for sep in [os.path.sep, os.path.altsep] if sep is not None and sep != "/" + sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/" ) diff --git a/contrib/python/Werkzeug/py3/werkzeug/serving.py b/contrib/python/Werkzeug/py3/werkzeug/serving.py index c482469863..2a2e74de2c 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/serving.py +++ b/contrib/python/Werkzeug/py3/werkzeug/serving.py @@ -221,9 +221,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): try: # binary_form=False gives nicer information, but wouldn't be compatible with # what Nginx or Apache could return. - peer_cert = self.connection.getpeercert( # type: ignore[attr-defined] - binary_form=True - ) + peer_cert = self.connection.getpeercert(binary_form=True) if peer_cert is not None: # Nginx and Apache use PEM format. environ["SSL_CLIENT_CERT"] = ssl.DER_cert_to_PEM_cert(peer_cert) @@ -329,7 +327,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): self.wfile.write(b"0\r\n\r\n") finally: if hasattr(application_iter, "close"): - application_iter.close() # type: ignore + application_iter.close() try: execute(self.server.app) @@ -659,6 +657,7 @@ class BaseWSGIServer(HTTPServer): multithread = False multiprocess = False request_queue_size = LISTEN_QUEUE + allow_reuse_address = True def __init__( self, @@ -710,10 +709,36 @@ class BaseWSGIServer(HTTPServer): try: self.server_bind() self.server_activate() + except OSError as e: + # Catch connection issues and show them without the traceback. Show + # extra instructions for address not found, and for macOS. + self.server_close() + print(e.strerror, file=sys.stderr) + + if e.errno == errno.EADDRINUSE: + print( + f"Port {port} is in use by another program. Either identify and" + " stop that program, or start the server with a different" + " port.", + file=sys.stderr, + ) + + if sys.platform == "darwin" and port == 5000: + print( + "On macOS, try disabling the 'AirPlay Receiver' service" + " from System Preferences -> Sharing.", + file=sys.stderr, + ) + + sys.exit(1) except BaseException: self.server_close() raise else: + # TCPServer automatically opens a socket even if bind_and_activate is False. + # Close it to silence a ResourceWarning. + self.server_close() + # Use the passed in socket directly. self.socket = socket.fromfd(fd, address_family, socket.SOCK_STREAM) self.server_address = self.socket.getsockname() @@ -879,60 +904,6 @@ def is_running_from_reloader() -> bool: return os.environ.get("WERKZEUG_RUN_MAIN") == "true" -def prepare_socket(hostname: str, port: int) -> socket.socket: - """Prepare a socket for use by the WSGI server and reloader. - - The socket is marked inheritable so that it can be kept across - reloads instead of breaking connections. - - Catch errors during bind and show simpler error messages. For - "address already in use", show instructions for resolving the issue, - with special instructions for macOS. - - This is called from :func:`run_simple`, but can be used separately - to control server creation with :func:`make_server`. - """ - address_family = select_address_family(hostname, port) - server_address = get_sockaddr(hostname, port, address_family) - s = socket.socket(address_family, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.set_inheritable(True) - - # Remove the socket file if it already exists. - if address_family == af_unix: - server_address = t.cast(str, server_address) - - if os.path.exists(server_address): - os.unlink(server_address) - - # Catch connection issues and show them without the traceback. Show - # extra instructions for address not found, and for macOS. - try: - s.bind(server_address) - except OSError as e: - print(e.strerror, file=sys.stderr) - - if e.errno == errno.EADDRINUSE: - print( - f"Port {port} is in use by another program. Either" - " identify and stop that program, or start the" - " server with a different port.", - file=sys.stderr, - ) - - if sys.platform == "darwin" and port == 5000: - print( - "On macOS, try disabling the 'AirPlay Receiver'" - " service from System Preferences -> Sharing.", - file=sys.stderr, - ) - - sys.exit(1) - - s.listen(LISTEN_QUEUE) - return s - - def run_simple( hostname: str, port: int, @@ -1059,12 +1030,7 @@ def run_simple( application = DebuggedApplication(application, evalex=use_evalex) if not is_running_from_reloader(): - s = prepare_socket(hostname, port) - fd = s.fileno() - # Silence a ResourceWarning about an unclosed socket. This object is no longer - # used, the server will create another with fromfd. - s.detach() - os.environ["WERKZEUG_SERVER_FD"] = str(fd) + fd = None else: fd = int(os.environ["WERKZEUG_SERVER_FD"]) @@ -1079,6 +1045,8 @@ def run_simple( ssl_context, fd=fd, ) + srv.socket.set_inheritable(True) + os.environ["WERKZEUG_SERVER_FD"] = str(srv.fileno()) if not is_running_from_reloader(): srv.log_startup() @@ -1087,12 +1055,15 @@ def run_simple( if use_reloader: from ._reloader import run_with_reloader - run_with_reloader( - srv.serve_forever, - extra_files=extra_files, - exclude_patterns=exclude_patterns, - interval=reloader_interval, - reloader_type=reloader_type, - ) + try: + run_with_reloader( + srv.serve_forever, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + interval=reloader_interval, + reloader_type=reloader_type, + ) + finally: + srv.server_close() else: srv.serve_forever() diff --git a/contrib/python/Werkzeug/py3/werkzeug/test.py b/contrib/python/Werkzeug/py3/werkzeug/test.py index 866aa97e53..be67979b58 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/test.py +++ b/contrib/python/Werkzeug/py3/werkzeug/test.py @@ -107,7 +107,8 @@ def stream_encode_multipart( and mimetypes.guess_type(filename)[0] or "application/octet-stream" ) - headers = Headers([("Content-Type", content_type)]) + headers = value.headers + headers.update([("Content-Type", content_type)]) if filename is None: write_binary(encoder.send_event(Field(name=key, headers=headers))) else: @@ -441,7 +442,7 @@ class EnvironBuilder: if input_stream is not None: raise TypeError("can't provide input stream and data") if hasattr(data, "read"): - data = data.read() # type: ignore + data = data.read() if isinstance(data, str): data = data.encode(self.charset) if isinstance(data, bytes): @@ -449,7 +450,7 @@ class EnvironBuilder: if self.content_length is None: self.content_length = len(data) else: - for key, value in _iter_data(data): # type: ignore + for key, value in _iter_data(data): if isinstance(value, (tuple, dict)) or hasattr(value, "read"): self._add_file_from_data(key, value) else: diff --git a/contrib/python/Werkzeug/py3/werkzeug/utils.py b/contrib/python/Werkzeug/py3/werkzeug/utils.py index 672e6e5ade..4ef5837137 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/utils.py +++ b/contrib/python/Werkzeug/py3/werkzeug/utils.py @@ -221,7 +221,7 @@ def secure_filename(filename: str) -> str: filename = unicodedata.normalize("NFKD", filename) filename = filename.encode("ascii", "ignore").decode("ascii") - for sep in os.path.sep, os.path.altsep: + for sep in os.sep, os.path.altsep: if sep: filename = filename.replace(sep, " ") filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip( @@ -352,7 +352,7 @@ def send_file( Never pass file paths provided by a user. The path is assumed to be trusted, so a user could craft a path to access a file you didn't - intend. + intend. Use :func:`send_from_directory` to safely serve user-provided paths. If the WSGI server sets a ``file_wrapper`` in ``environ``, it is used, otherwise Werkzeug's built-in wrapper is used. Alternatively, @@ -562,9 +562,10 @@ def send_from_directory( If the final path does not point to an existing regular file, returns a 404 :exc:`~werkzeug.exceptions.NotFound` error. - :param directory: The directory that ``path`` must be located under. - :param path: The path to the file to send, relative to - ``directory``. + :param directory: The directory that ``path`` must be located under. This *must not* + be a value provided by the client, otherwise it becomes insecure. + :param path: The path to the file to send, relative to ``directory``. This is the + part of the path provided by the client, which is checked for security. :param environ: The WSGI environ for the current request. :param kwargs: Arguments to pass to :func:`send_file`. diff --git a/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py b/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py index 57b739cc5f..2de77df42a 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py @@ -83,6 +83,13 @@ class Request(_SansIORequest): #: .. versionadded:: 0.5 max_form_memory_size: t.Optional[int] = None + #: The maximum number of multipart parts to parse, passed to + #: :attr:`form_data_parser_class`. Parsing form data with more than this + #: many parts will raise :exc:`~.RequestEntityTooLarge`. + #: + #: .. versionadded:: 2.2.3 + max_form_parts = 1000 + #: The form data parser that should be used. Can be replaced to customize #: the form date parsing. form_data_parser_class: t.Type[FormDataParser] = FormDataParser @@ -246,6 +253,7 @@ class Request(_SansIORequest): self.max_form_memory_size, self.max_content_length, self.parameter_storage_class, + max_form_parts=self.max_form_parts, ) def _load_form_data(self) -> None: @@ -543,6 +551,18 @@ class Request(_SansIORequest): # with sentinel values. _cached_json: t.Tuple[t.Any, t.Any] = (Ellipsis, Ellipsis) + @t.overload + def get_json( + self, force: bool = ..., silent: "te.Literal[False]" = ..., cache: bool = ... + ) -> t.Any: + ... + + @t.overload + def get_json( + self, force: bool = ..., silent: bool = ..., cache: bool = ... + ) -> t.Optional[t.Any]: + ... + def get_json( self, force: bool = False, silent: bool = False, cache: bool = True ) -> t.Optional[t.Any]: diff --git a/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py b/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py index 7e888cba5e..454208c773 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py @@ -439,7 +439,7 @@ class Response(_SansIOResponse): Can now be used in a with statement. """ if hasattr(self.response, "close"): - self.response.close() # type: ignore + self.response.close() for func in self._on_close: func() @@ -645,6 +645,14 @@ class Response(_SansIOResponse): """ return self.get_json() + @t.overload + def get_json(self, force: bool = ..., silent: "te.Literal[False]" = ...) -> t.Any: + ... + + @t.overload + def get_json(self, force: bool = ..., silent: bool = ...) -> t.Optional[t.Any]: + ... + def get_json(self, force: bool = False, silent: bool = False) -> t.Optional[t.Any]: """Parse :attr:`data` as JSON. Useful during testing. diff --git a/contrib/python/Werkzeug/py3/werkzeug/wsgi.py b/contrib/python/Werkzeug/py3/werkzeug/wsgi.py index 24ece0b19e..d74430d8bb 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wsgi.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wsgi.py @@ -611,9 +611,7 @@ class _RangeWrapper: self.end_byte = start_byte + byte_range self.read_length = 0 - self.seekable = ( - hasattr(iterable, "seekable") and iterable.seekable() # type: ignore - ) + self.seekable = hasattr(iterable, "seekable") and iterable.seekable() self.end_reached = False def __iter__(self) -> "_RangeWrapper": @@ -665,7 +663,7 @@ class _RangeWrapper: def close(self) -> None: if hasattr(self.iterable, "close"): - self.iterable.close() # type: ignore + self.iterable.close() def _make_chunk_iter( @@ -930,37 +928,77 @@ class LimitedStream(io.IOBase): raise ClientDisconnected() - def exhaust(self, chunk_size: int = 1024 * 64) -> None: - """Exhaust the stream. This consumes all the data left until the - limit is reached. + def _exhaust_chunks(self, chunk_size: int = 1024 * 64) -> t.Iterator[bytes]: + """Exhaust the stream by reading until the limit is reached or the client + disconnects, yielding each chunk. + + :param chunk_size: How many bytes to read at a time. - :param chunk_size: the size for a chunk. It will read the chunk - until the stream is exhausted and throw away - the results. + :meta private: + + .. versionadded:: 2.2.3 """ to_read = self.limit - self._pos - chunk = chunk_size + while to_read > 0: - chunk = min(to_read, chunk) - self.read(chunk) - to_read -= chunk + chunk = self.read(min(to_read, chunk_size)) + yield chunk + to_read -= len(chunk) + + def exhaust(self, chunk_size: int = 1024 * 64) -> None: + """Exhaust the stream by reading until the limit is reached or the client + disconnects, discarding the data. + + :param chunk_size: How many bytes to read at a time. + + .. versionchanged:: 2.2.3 + Handle case where wrapped stream returns fewer bytes than requested. + """ + for _ in self._exhaust_chunks(chunk_size): + pass def read(self, size: t.Optional[int] = None) -> bytes: - """Read `size` bytes or if size is not provided everything is read. + """Read up to ``size`` bytes from the underlying stream. If size is not + provided, read until the limit. - :param size: the number of bytes read. + If the limit is reached, :meth:`on_exhausted` is called, which returns empty + bytes. + + If no bytes are read and the limit is not reached, or if an error occurs during + the read, :meth:`on_disconnect` is called, which raises + :exc:`.ClientDisconnected`. + + :param size: The number of bytes to read. ``None``, default, reads until the + limit is reached. + + .. versionchanged:: 2.2.3 + Handle case where wrapped stream returns fewer bytes than requested. """ if self._pos >= self.limit: return self.on_exhausted() - if size is None or size == -1: # -1 is for consistence with file - size = self.limit + + if size is None or size == -1: # -1 is for consistency with file + # Keep reading from the wrapped stream until the limit is reached. Can't + # rely on stream.read(size) because it's not guaranteed to return size. + buf = bytearray() + + for chunk in self._exhaust_chunks(): + buf.extend(chunk) + + return bytes(buf) + to_read = min(self.limit - self._pos, size) + try: read = self._read(to_read) except (OSError, ValueError): return self.on_disconnect() - if to_read and len(read) != to_read: + + if to_read and not len(read): + # If no data was read, treat it as a disconnect. As long as some data was + # read, a subsequent call can still return more before reaching the limit. return self.on_disconnect() + self._pos += len(read) return read diff --git a/contrib/python/Werkzeug/py3/ya.make b/contrib/python/Werkzeug/py3/ya.make index 823e9d5e5e..0d9e451aa9 100644 --- a/contrib/python/Werkzeug/py3/ya.make +++ b/contrib/python/Werkzeug/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.2.2) +VERSION(2.2.3) LICENSE(BSD-3-Clause) |