Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
httputil.py1393 linesDownload Raw Back to tornado
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""HTTP utility code shared by clients and servers.
17
18This module also defines the `HTTPServerRequest` class which is exposed
19via `tornado.web.RequestHandler.request`.
20"""
21
22import calendar
23import collections.abc
24import copy
25import dataclasses
26import datetime
27import email.utils
28from functools import lru_cache
29from http.client import responses
30import http.cookies
31import re
32from ssl import SSLError
33import time
34import unicodedata
35from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
36
37from tornado.escape import native_str, parse_qs_bytes, utf8, to_unicode
38from tornado.util import ObjectDict, unicode_type
39
40
41# responses is unused in this file, but we re-export it to other files.
42# Reference it so pyflakes doesn't complain.
43responses
44
45import typing
46from typing import (
47    Tuple,
48    Iterable,
49    List,
50    Mapping,
51    Iterator,
52    Dict,
53    Union,
54    Optional,
55    Awaitable,
56    Generator,
57    AnyStr,
58)
59
60if typing.TYPE_CHECKING:
61    from typing import Deque  # noqa: F401
62    from asyncio import Future  # noqa: F401
63    import unittest  # noqa: F401
64
65    # This can be done unconditionally in the base class of HTTPHeaders
66    # after we drop support for Python 3.8.
67    StrMutableMapping = collections.abc.MutableMapping[str, str]
68else:
69    StrMutableMapping = collections.abc.MutableMapping
70
71# To be used with str.strip() and related methods.
72HTTP_WHITESPACE = " \t"
73
74# Roughly the inverse of RequestHandler._VALID_HEADER_CHARS, but permits
75# chars greater than \xFF (which may appear after decoding utf8).
76_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0A-\x1F\x7F]")
77
78
79class _ABNF:
80    """Class that holds a subset of ABNF rules from RFC 9110 and friends.
81
82    Class attributes are re.Pattern objects, with the same name as in the RFC
83    (with hyphens changed to underscores). Currently contains only the subset
84    we use (which is why this class is not public). Unfortunately the fields
85    cannot be alphabetized as they are in the RFCs because of dependencies.
86    """
87
88    # RFC 3986 (URI)
89    # The URI hostname ABNF is both complex (including detailed vaildation of IPv4 and IPv6
90    # literals) and not strict enough (a lot of punctuation is allowed by the ABNF even though
91    # it is not allowed by DNS). We simplify it by allowing square brackets and colons in any
92    # position, not only for their use in IPv6 literals.
93    uri_unreserved = re.compile(r"[A-Za-z0-9\-._~]")
94    uri_sub_delims = re.compile(r"[!$&'()*+,;=]")
95    uri_pct_encoded = re.compile(r"%[0-9A-Fa-f]{2}")
96    uri_host = re.compile(
97        rf"(?:[\[\]:]|{uri_unreserved.pattern}|{uri_sub_delims.pattern}|{uri_pct_encoded.pattern})*"
98    )
99    uri_port = re.compile(r"[0-9]*")
100
101    # RFC 5234 (ABNF)
102    VCHAR = re.compile(r"[\x21-\x7E]")
103
104    # RFC 9110 (HTTP Semantics)
105    obs_text = re.compile(r"[\x80-\xFF]")
106    field_vchar = re.compile(rf"(?:{VCHAR.pattern}|{obs_text.pattern})")
107    # Not exactly from the RFC to simplify and combine field-content and field-value.
108    field_value = re.compile(
109        rf"|"
110        rf"{field_vchar.pattern}|"
111        rf"{field_vchar.pattern}(?:{field_vchar.pattern}| |\t)*{field_vchar.pattern}"
112    )
113    tchar = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]")
114    token = re.compile(rf"{tchar.pattern}+")
115    field_name = token
116    method = token
117    host = re.compile(rf"(?:{uri_host.pattern})(?::{uri_port.pattern})?")
118
119    # RFC 9112 (HTTP/1.1)
120    HTTP_version = re.compile(r"HTTP/[0-9]\.[0-9]")
121    reason_phrase = re.compile(rf"(?:[\t ]|{VCHAR.pattern}|{obs_text.pattern})+")
122    # request_target delegates to the URI RFC 3986, which is complex and may be
123    # too restrictive (for example, the WHATWG version of the URL spec allows non-ASCII
124    # characters). Instead, we allow everything but control chars and whitespace.
125    request_target = re.compile(rf"{field_vchar.pattern}+")
126    request_line = re.compile(
127        rf"({method.pattern}) ({request_target.pattern}) ({HTTP_version.pattern})"
128    )
129    status_code = re.compile(r"[0-9]{3}")
130    status_line = re.compile(
131        rf"({HTTP_version.pattern}) ({status_code.pattern}) ({reason_phrase.pattern})?"
132    )
133
134
135@lru_cache(1000)
136def _normalize_header(name: str) -> str:
137    """Map a header name to Http-Header-Case.
138
139    >>> _normalize_header("coNtent-TYPE")
140    'Content-Type'
141    """
142    return "-".join([w.capitalize() for w in name.split("-")])
143
144
145class HTTPHeaders(StrMutableMapping):
146    """A dictionary that maintains ``Http-Header-Case`` for all keys.
147
148    Supports multiple values per key via a pair of new methods,
149    `add()` and `get_list()`.  The regular dictionary interface
150    returns a single value per key, with multiple values joined by a
151    comma.
152
153    >>> h = HTTPHeaders({"content-type": "text/html"})
154    >>> list(h.keys())
155    ['Content-Type']
156    >>> h["Content-Type"]
157    'text/html'
158
159    >>> h.add("Set-Cookie", "A=B")
160    >>> h.add("Set-Cookie", "C=D")
161    >>> h["set-cookie"]
162    'A=B,C=D'
163    >>> h.get_list("set-cookie")
164    ['A=B', 'C=D']
165
166    >>> for (k,v) in sorted(h.get_all()):
167    ...    print('%s: %s' % (k,v))
168    ...
169    Content-Type: text/html
170    Set-Cookie: A=B
171    Set-Cookie: C=D
172    """
173
174    @typing.overload
175    def __init__(self, __arg: Mapping[str, List[str]]) -> None:
176        pass
177
178    @typing.overload  # noqa: F811
179    def __init__(self, __arg: Mapping[str, str]) -> None:
180        pass
181
182    @typing.overload  # noqa: F811
183    def __init__(self, *args: Tuple[str, str]) -> None:
184        pass
185
186    @typing.overload  # noqa: F811
187    def __init__(self, **kwargs: str) -> None:
188        pass
189
190    def __init__(self, *args: typing.Any, **kwargs: str) -> None:  # noqa: F811
191        # Formally, HTTP headers are a mapping from a field name to a "combined field value",
192        # which may be constructed from multiple field lines by joining them with commas.
193        # In practice, however, some headers (notably Set-Cookie) do not follow this convention,
194        # so we maintain a mapping from field name to a list of field lines in self._as_list.
195        # self._combined_cache is a cache of the combined field values derived from self._as_list
196        # on demand (and cleared whenever the list is modified).
197        self._as_list: dict[str, list[str]] = {}
198        self._combined_cache: dict[str, str] = {}
199        self._last_key = None  # type: Optional[str]
200        if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
201            # Copy constructor
202            for k, v in args[0].get_all():
203                self.add(k, v)
204        else:
205            # Dict-style initialization
206            self.update(*args, **kwargs)
207
208    # new public methods
209
210    def add(self, name: str, value: str, *, _chars_are_bytes: bool = True) -> None:
211        """Adds a new value for the given key."""
212        if not _ABNF.field_name.fullmatch(name):
213            raise HTTPInputError("Invalid header name %r" % name)
214        if _chars_are_bytes:
215            if not _ABNF.field_value.fullmatch(to_unicode(value)):
216                # TODO: the fact we still support bytes here (contrary to type annotations)
217                # and still test for it should probably be changed.
218                raise HTTPInputError("Invalid header value %r" % value)
219        else:
220            if _FORBIDDEN_HEADER_CHARS_RE.search(value):
221                raise HTTPInputError("Invalid header value %r" % value)
222        norm_name = _normalize_header(name)
223        self._last_key = norm_name
224        if norm_name in self:
225            self._combined_cache.pop(norm_name, None)
226            self._as_list[norm_name].append(value)
227        else:
228            self[norm_name] = value
229
230    def get_list(self, name: str) -> List[str]:
231        """Returns all values for the given header as a list."""
232        norm_name = _normalize_header(name)
233        return self._as_list.get(norm_name, [])
234
235    def get_all(self) -> Iterable[Tuple[str, str]]:
236        """Returns an iterable of all (name, value) pairs.
237
238        If a header has multiple values, multiple pairs will be
239        returned with the same name.
240        """
241        for name, values in self._as_list.items():
242            for value in values:
243                yield (name, value)
244
245    def parse_line(self, line: str, *, _chars_are_bytes: bool = True) -> None:
246        r"""Updates the dictionary with a single header line.
247
248        >>> h = HTTPHeaders()
249        >>> h.parse_line("Content-Type: text/html")
250        >>> h.get('content-type')
251        'text/html'
252        >>> h.parse_line("Content-Length: 42\r\n")
253        >>> h.get('content-type')
254        'text/html'
255
256        .. versionchanged:: 6.5
257            Now supports lines with or without the trailing CRLF, making it possible
258            to pass lines from AsyncHTTPClient's header_callback directly to this method.
259
260        .. deprecated:: 6.5
261           In Tornado 7.0, certain deprecated features of HTTP will become errors.
262           Specifically, line folding and the use of LF (with CR) as a line separator
263           will be removed.
264        """
265        if m := re.search(r"\r?\n$", line):
266            # RFC 9112 section 2.2: a recipient MAY recognize a single LF as a line
267            # terminator and ignore any preceding CR.
268            # TODO(7.0): Remove this support for LF-only line endings.
269            line = line[: m.start()]
270        if not line:
271            # Empty line, or the final CRLF of a header block.
272            return
273        if line[0] in HTTP_WHITESPACE:
274            # continuation of a multi-line header
275            # TODO(7.0): Remove support for line folding.
276            if self._last_key is None:
277                raise HTTPInputError("first header line cannot start with whitespace")
278            new_part = " " + line.strip(HTTP_WHITESPACE)
279            if _chars_are_bytes:
280                if not _ABNF.field_value.fullmatch(new_part[1:]):
281                    raise HTTPInputError("Invalid header continuation %r" % new_part)
282            else:
283                if _FORBIDDEN_HEADER_CHARS_RE.search(new_part):
284                    raise HTTPInputError("Invalid header value %r" % new_part)
285            self._as_list[self._last_key][-1] += new_part
286            self._combined_cache.pop(self._last_key, None)
287        else:
288            try:
289                name, value = line.split(":", 1)
290            except ValueError:
291                raise HTTPInputError("no colon in header line")
292            self.add(
293                name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes
294            )
295
296    @classmethod
297    def parse(cls, headers: str, *, _chars_are_bytes: bool = True) -> "HTTPHeaders":
298        """Returns a dictionary from HTTP header text.
299
300        >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
301        >>> sorted(h.items())
302        [('Content-Length', '42'), ('Content-Type', 'text/html')]
303
304        .. versionchanged:: 5.1
305
306           Raises `HTTPInputError` on malformed headers instead of a
307           mix of `KeyError`, and `ValueError`.
308
309        """
310        # _chars_are_bytes is a hack. This method is used in two places, HTTP headers (in which
311        # non-ascii characters are to be interpreted as latin-1) and multipart/form-data (in which
312        # they are to be interpreted as utf-8). For historical reasons, this method handled this by
313        # expecting both callers to decode the headers to strings before parsing them. This wasn't a
314        # problem until we started doing stricter validation of the characters allowed in HTTP
315        # headers (using ABNF rules defined in terms of byte values), which inadvertently started
316        # disallowing non-latin1 characters in multipart/form-data filenames.
317        #
318        # This method should have accepted bytes and a desired encoding, but this change is being
319        # introduced in a patch release that shouldn't change the API. Instead, the _chars_are_bytes
320        # flag decides whether to use HTTP-style ABNF validation (treating the string as bytes
321        # smuggled through the latin1 encoding) or to accept any non-control unicode characters
322        # as required by multipart/form-data. This method will change to accept bytes in a future
323        # release.
324        h = cls()
325
326        start = 0
327        while True:
328            lf = headers.find("\n", start)
329            if lf == -1:
330                h.parse_line(headers[start:], _chars_are_bytes=_chars_are_bytes)
331                break
332            line = headers[start : lf + 1]
333            start = lf + 1
334            h.parse_line(line, _chars_are_bytes=_chars_are_bytes)
335        return h
336
337    # MutableMapping abstract method implementations.
338
339    def __setitem__(self, name: str, value: str) -> None:
340        norm_name = _normalize_header(name)
341        self._combined_cache[norm_name] = value
342        self._as_list[norm_name] = [value]
343
344    def __contains__(self, name: object) -> bool:
345        # This is an important optimization to avoid the expensive concatenation
346        # in __getitem__ when it's not needed.
347        if not isinstance(name, str):
348            return False
349        norm_name = _normalize_header(name)
350        return norm_name in self._as_list
351
352    def __getitem__(self, name: str) -> str:
353        header = _normalize_header(name)
354        if header not in self._combined_cache:
355            self._combined_cache[header] = ",".join(self._as_list[header])
356        return self._combined_cache[header]
357
358    def __delitem__(self, name: str) -> None:
359        norm_name = _normalize_header(name)
360        del self._combined_cache[norm_name]
361        del self._as_list[norm_name]
362
363    def __len__(self) -> int:
364        return len(self._as_list)
365
366    def __iter__(self) -> Iterator[typing.Any]:
367        return iter(self._as_list)
368
369    def copy(self) -> "HTTPHeaders":
370        # defined in dict but not in MutableMapping.
371        return HTTPHeaders(self)
372
373    # Use our overridden copy method for the copy.copy module.
374    # This makes shallow copies one level deeper, but preserves
375    # the appearance that HTTPHeaders is a single container.
376    __copy__ = copy
377
378    def __str__(self) -> str:
379        lines = []
380        for name, value in self.get_all():
381            lines.append(f"{name}: {value}\n")
382        return "".join(lines)
383
384    __unicode__ = __str__
385
386
387class HTTPServerRequest:
388    """A single HTTP request.
389
390    All attributes are type `str` unless otherwise noted.
391
392    .. attribute:: method
393
394       HTTP request method, e.g. "GET" or "POST"
395
396    .. attribute:: uri
397
398       The requested uri.
399
400    .. attribute:: path
401
402       The path portion of `uri`
403
404    .. attribute:: query
405
406       The query portion of `uri`
407
408    .. attribute:: version
409
410       HTTP version specified in request, e.g. "HTTP/1.1"
411
412    .. attribute:: headers
413
414       `.HTTPHeaders` dictionary-like object for request headers.  Acts like
415       a case-insensitive dictionary with additional methods for repeated
416       headers.
417
418    .. attribute:: body
419
420       Request body, if present, as a byte string.
421
422    .. attribute:: remote_ip
423
424       Client's IP address as a string.  If ``HTTPServer.xheaders`` is set,
425       will pass along the real IP address provided by a load balancer
426       in the ``X-Real-Ip`` or ``X-Forwarded-For`` header.
427
428    .. versionchanged:: 3.1
429       The list format of ``X-Forwarded-For`` is now supported.
430
431    .. attribute:: protocol
432
433       The protocol used, either "http" or "https".  If ``HTTPServer.xheaders``
434       is set, will pass along the protocol used by a load balancer if
435       reported via an ``X-Scheme`` header.
436
437    .. attribute:: host
438
439       The requested hostname, usually taken from the ``Host`` header.
440
441    .. attribute:: arguments
442
443       GET/POST arguments are available in the arguments property, which
444       maps arguments names to lists of values (to support multiple values
445       for individual names). Names are of type `str`, while arguments
446       are byte strings.  Note that this is different from
447       `.RequestHandler.get_argument`, which returns argument values as
448       unicode strings.
449
450    .. attribute:: query_arguments
451
452       Same format as ``arguments``, but contains only arguments extracted
453       from the query string.
454
455       .. versionadded:: 3.2
456
457    .. attribute:: body_arguments
458
459       Same format as ``arguments``, but contains only arguments extracted
460       from the request body.
461
462       .. versionadded:: 3.2
463
464    .. attribute:: files
465
466       File uploads are available in the files property, which maps file
467       names to lists of `.HTTPFile`.
468
469    .. attribute:: connection
470
471       An HTTP request is attached to a single HTTP connection, which can
472       be accessed through the "connection" attribute. Since connections
473       are typically kept open in HTTP/1.1, multiple requests can be handled
474       sequentially on a single connection.
475
476    .. versionchanged:: 4.0
477       Moved from ``tornado.httpserver.HTTPRequest``.
478
479    .. deprecated:: 6.5.2
480       The ``host`` argument to the ``HTTPServerRequest`` constructor is deprecated. Use
481       ``headers["Host"]`` instead. This argument was mistakenly removed in Tornado 6.5.0 and
482       temporarily restored in 6.5.2.
483    """
484
485    path = None  # type: str
486    query = None  # type: str
487
488    # HACK: Used for stream_request_body
489    _body_future = None  # type: Future[None]
490
491    def __init__(
492        self,
493        method: Optional[str] = None,
494        uri: Optional[str] = None,
495        version: str = "HTTP/1.0",
496        headers: Optional[HTTPHeaders] = None,
497        body: Optional[bytes] = None,
498        host: Optional[str] = None,
499        files: Optional[Dict[str, List["HTTPFile"]]] = None,
500        connection: Optional["HTTPConnection"] = None,
501        start_line: Optional["RequestStartLine"] = None,
502        server_connection: Optional[object] = None,
503    ) -> None:
504        if start_line is not None:
505            method, uri, version = start_line
506        self.method = method
507        self.uri = uri
508        self.version = version
509        self.headers = headers or HTTPHeaders()
510        self.body = body or b""
511
512        # set remote IP and protocol
513        context = getattr(connection, "context", None)
514        self.remote_ip = getattr(context, "remote_ip", None)
515        self.protocol = getattr(context, "protocol", "http")
516
517        try:
518            self.host = host or self.headers["Host"]
519        except KeyError:
520            if version == "HTTP/1.0":
521                # HTTP/1.0 does not require the Host header.
522                self.host = "127.0.0.1"
523            else:
524                raise HTTPInputError("Missing Host header")
525        if not _ABNF.host.fullmatch(self.host):
526            raise HTTPInputError("Invalid Host header: %r" % self.host)
527        if "," in self.host:
528            # https://www.rfc-editor.org/rfc/rfc9112.html#name-request-target
529            # Server MUST respond with 400 Bad Request if multiple
530            # Host headers are present.
531            #
532            # We test for the presence of a comma instead of the number of
533            # headers received because a proxy may have converted
534            # multiple headers into a single comma-separated value
535            # (per RFC 9110 section 5.3).
536            #
537            # This is technically a departure from the RFC since the ABNF
538            # does not forbid commas in the host header. However, since
539            # commas are not allowed in DNS names, it is appropriate to
540            # disallow them. (The same argument could be made for other special
541            # characters, but commas are the most problematic since they could
542            # be used to exploit differences between proxies when multiple headers
543            # are supplied).
544            raise HTTPInputError("Multiple host headers not allowed: %r" % self.host)
545        self.host_name = split_host_and_port(self.host.lower())[0]
546        self.files = files or {}
547        self.connection = connection
548        self.server_connection = server_connection
549        self._start_time = time.time()
550        self._finish_time = None
551
552        if uri is not None:
553            self.path, sep, self.query = uri.partition("?")
554        self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
555        self.query_arguments = copy.deepcopy(self.arguments)
556        self.body_arguments = {}  # type: Dict[str, List[bytes]]
557
558    @property
559    def cookies(self) -> Dict[str, http.cookies.Morsel]:
560        """A dictionary of ``http.cookies.Morsel`` objects."""
561        if not hasattr(self, "_cookies"):
562            self._cookies = (
563                http.cookies.SimpleCookie()
564            )  # type: http.cookies.SimpleCookie
565            if "Cookie" in self.headers:
566                try:
567                    parsed = parse_cookie(self.headers["Cookie"])
568                except Exception:
569                    pass
570                else:
571                    for k, v in parsed.items():
572                        try:
573                            self._cookies[k] = v
574                        except Exception:
575                            # SimpleCookie imposes some restrictions on keys;
576                            # parse_cookie does not. Discard any cookies
577                            # with disallowed keys.
578                            pass
579        return self._cookies
580
581    def full_url(self) -> str:
582        """Reconstructs the full URL for this request."""
583        return self.protocol + "://" + self.host + self.uri  # type: ignore[operator]
584
585    def request_time(self) -> float:
586        """Returns the amount of time it took for this request to execute."""
587        if self._finish_time is None:
588            return time.time() - self._start_time
589        else:
590            return self._finish_time - self._start_time
591
592    def get_ssl_certificate(
593        self, binary_form: bool = False
594    ) -> Union[None, Dict, bytes]:
595        """Returns the client's SSL certificate, if any.
596
597        To use client certificates, the HTTPServer's
598        `ssl.SSLContext.verify_mode` field must be set, e.g.::
599
600            ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
601            ssl_ctx.load_cert_chain("foo.crt", "foo.key")
602            ssl_ctx.load_verify_locations("cacerts.pem")
603            ssl_ctx.verify_mode = ssl.CERT_REQUIRED
604            server = HTTPServer(app, ssl_options=ssl_ctx)
605
606        By default, the return value is a dictionary (or None, if no
607        client certificate is present).  If ``binary_form`` is true, a
608        DER-encoded form of the certificate is returned instead.  See
609        SSLSocket.getpeercert() in the standard library for more
610        details.
611        http://docs.python.org/library/ssl.html#sslsocket-objects
612        """
613        try:
614            if self.connection is None:
615                return None
616            # TODO: add a method to HTTPConnection for this so it can work with HTTP/2
617            return self.connection.stream.socket.getpeercert(  # type: ignore
618                binary_form=binary_form
619            )
620        except SSLError:
621            return None
622
623    def _parse_body(self) -> None:
624        parse_body_arguments(
625            self.headers.get("Content-Type", ""),
626            self.body,
627            self.body_arguments,
628            self.files,
629            self.headers,
630        )
631
632        for k, v in self.body_arguments.items():
633            self.arguments.setdefault(k, []).extend(v)
634
635    def __repr__(self) -> str:
636        attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
637        args = ", ".join([f"{n}={getattr(self, n)!r}" for n in attrs])
638        return f"{self.__class__.__name__}({args})"
639
640
641class HTTPInputError(Exception):
642    """Exception class for malformed HTTP requests or responses
643    from remote sources.
644
645    .. versionadded:: 4.0
646    """
647
648    pass
649
650
651class HTTPOutputError(Exception):
652    """Exception class for errors in HTTP output.
653
654    .. versionadded:: 4.0
655    """
656
657    pass
658
659
660class HTTPServerConnectionDelegate:
661    """Implement this interface to handle requests from `.HTTPServer`.
662
663    .. versionadded:: 4.0
664    """
665
666    def start_request(
667        self, server_conn: object, request_conn: "HTTPConnection"
668    ) -> "HTTPMessageDelegate":
669        """This method is called by the server when a new request has started.
670
671        :arg server_conn: is an opaque object representing the long-lived
672            (e.g. tcp-level) connection.
673        :arg request_conn: is a `.HTTPConnection` object for a single
674            request/response exchange.
675
676        This method should return a `.HTTPMessageDelegate`.
677        """
678        raise NotImplementedError()
679
680    def on_close(self, server_conn: object) -> None:
681        """This method is called when a connection has been closed.
682
683        :arg server_conn: is a server connection that has previously been
684            passed to ``start_request``.
685        """
686        pass
687
688
689class HTTPMessageDelegate:
690    """Implement this interface to handle an HTTP request or response.
691
692    .. versionadded:: 4.0
693    """
694
695    # TODO: genericize this class to avoid exposing the Union.
696    def headers_received(
697        self,
698        start_line: Union["RequestStartLine", "ResponseStartLine"],
699        headers: HTTPHeaders,
700    ) -> Optional[Awaitable[None]]:
701        """Called when the HTTP headers have been received and parsed.
702
703        :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`
704            depending on whether this is a client or server message.
705        :arg headers: a `.HTTPHeaders` instance.
706
707        Some `.HTTPConnection` methods can only be called during
708        ``headers_received``.
709
710        May return a `.Future`; if it does the body will not be read
711        until it is done.
712        """
713        pass
714
715    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
716        """Called when a chunk of data has been received.
717
718        May return a `.Future` for flow control.
719        """
720        pass
721
722    def finish(self) -> None:
723        """Called after the last chunk of data has been received."""
724        pass
725
726    def on_connection_close(self) -> None:
727        """Called if the connection is closed without finishing the request.
728
729        If ``headers_received`` is called, either ``finish`` or
730        ``on_connection_close`` will be called, but not both.
731        """
732        pass
733
734
735class HTTPConnection:
736    """Applications use this interface to write their responses.
737
738    .. versionadded:: 4.0
739    """
740
741    def write_headers(
742        self,
743        start_line: Union["RequestStartLine", "ResponseStartLine"],
744        headers: HTTPHeaders,
745        chunk: Optional[bytes] = None,
746    ) -> "Future[None]":
747        """Write an HTTP header block.
748
749        :arg start_line: a `.RequestStartLine` or `.ResponseStartLine`.
750        :arg headers: a `.HTTPHeaders` instance.
751        :arg chunk: the first (optional) chunk of data.  This is an optimization
752            so that small responses can be written in the same call as their
753            headers.
754
755        The ``version`` field of ``start_line`` is ignored.
756
757        Returns a future for flow control.
758
759        .. versionchanged:: 6.0
760
761           The ``callback`` argument was removed.
762        """
763        raise NotImplementedError()
764
765    def write(self, chunk: bytes) -> "Future[None]":
766        """Writes a chunk of body data.
767
768        Returns a future for flow control.
769
770        .. versionchanged:: 6.0
771
772           The ``callback`` argument was removed.
773        """
774        raise NotImplementedError()
775
776    def finish(self) -> None:
777        """Indicates that the last body data has been written."""
778        raise NotImplementedError()
779
780
781def url_concat(
782    url: str,
783    args: Union[
784        None, Dict[str, str], List[Tuple[str, str]], Tuple[Tuple[str, str], ...]
785    ],
786) -> str:
787    """Concatenate url and arguments regardless of whether
788    url has existing query parameters.
789
790    ``args`` may be either a dictionary or a list of key-value pairs
791    (the latter allows for multiple values with the same key.
792
793    >>> url_concat("http://example.com/foo", dict(c="d"))
794    'http://example.com/foo?c=d'
795    >>> url_concat("http://example.com/foo?a=b", dict(c="d"))
796    'http://example.com/foo?a=b&c=d'
797    >>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")])
798    'http://example.com/foo?a=b&c=d&c=d2'
799    """
800    if args is None:
801        return url
802    parsed_url = urlparse(url)
803    if isinstance(args, dict):
804        parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True)
805        parsed_query.extend(args.items())
806    elif isinstance(args, list) or isinstance(args, tuple):
807        parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True)
808        parsed_query.extend(args)
809    else:
810        err = "'args' parameter should be dict, list or tuple. Not {0}".format(
811            type(args)
812        )
813        raise TypeError(err)
814    final_query = urlencode(parsed_query)
815    url = urlunparse(
816        (
817            parsed_url[0],
818            parsed_url[1],
819            parsed_url[2],
820            parsed_url[3],
821            final_query,
822            parsed_url[5],
823        )
824    )
825    return url
826
827
828class HTTPFile(ObjectDict):
829    """Represents a file uploaded via a form.
830
831    For backwards compatibility, its instance attributes are also
832    accessible as dictionary keys.
833
834    * ``filename``
835    * ``body``
836    * ``content_type``
837    """
838
839    filename: str
840    body: bytes
841    content_type: str
842
843
844def _parse_request_range(
845    range_header: str,
846) -> Optional[Tuple[Optional[int], Optional[int]]]:
847    """Parses a Range header.
848
849    Returns either ``None`` or tuple ``(start, end)``.
850    Note that while the HTTP headers use inclusive byte positions,
851    this method returns indexes suitable for use in slices.
852
853    >>> start, end = _parse_request_range("bytes=1-2")
854    >>> start, end
855    (1, 3)
856    >>> [0, 1, 2, 3, 4][start:end]
857    [1, 2]
858    >>> _parse_request_range("bytes=6-")
859    (6, None)
860    >>> _parse_request_range("bytes=-6")
861    (-6, None)
862    >>> _parse_request_range("bytes=-0")
863    (None, 0)
864    >>> _parse_request_range("bytes=")
865    (None, None)
866    >>> _parse_request_range("foo=42")
867    >>> _parse_request_range("bytes=1-2,6-10")
868
869    Note: only supports one range (ex, ``bytes=1-2,6-10`` is not allowed).
870
871    See [0] for the details of the range header.
872
873    [0]: http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges
874    """
875    unit, _, value = range_header.partition("=")
876    unit, value = unit.strip(), value.strip()
877    if unit != "bytes":
878        return None
879    start_b, _, end_b = value.partition("-")
880    try:
881        start = _int_or_none(start_b)
882        end = _int_or_none(end_b)
883    except ValueError:
884        return None
885    if end is not None:
886        if start is None:
887            if end != 0:
888                start = -end
889                end = None
890        else:
891            end += 1
892    return (start, end)
893
894
895def _get_content_range(start: Optional[int], end: Optional[int], total: int) -> str:
896    """Returns a suitable Content-Range header:
897
898    >>> print(_get_content_range(None, 1, 4))
899    bytes 0-0/4
900    >>> print(_get_content_range(1, 3, 4))
901    bytes 1-2/4
902    >>> print(_get_content_range(None, None, 4))
903    bytes 0-3/4
904    """
905    start = start or 0
906    end = (end or total) - 1
907    return f"bytes {start}-{end}/{total}"
908
909
910def _int_or_none(val: str) -> Optional[int]:
911    val = val.strip()
912    if val == "":
913        return None
914    return int(val)
915
916
917@dataclasses.dataclass
918class ParseMultipartConfig:
919    """This class configures the parsing of ``multipart/form-data`` request bodies.
920
921    Its primary purpose is to place limits on the size and complexity of request messages
922    to avoid potential denial-of-service attacks.
923
924    .. versionadded:: 6.5.5
925    """
926
927    enabled: bool = True
928    """Set this to false to disable the parsing of ``multipart/form-data`` requests entirely.
929
930    This may be desirable for applications that do not need to handle this format, since
931    multipart request have a history of DoS vulnerabilities in Tornado. Multipart requests
932    are used primarily for ``<input type="file">`` in HTML forms, or in APIs that mimic this
933    format. File uploads that use the HTTP ``PUT`` method generally do not use the multipart
934    format.
935    """
936
937    max_parts: int = 100
938    """The maximum number of parts accepted in a multipart request.
939
940    Each ``<input>`` element in an HTML form corresponds to at least one "part".
941    """
942
943    max_part_header_size: int = 10 * 1024
944    """The maximum size of the headers for each part of a multipart request.
945
946    The header for a part contains the name of the form field and optionally the filename
947    and content type of the uploaded file.
948    """
949
950
951@dataclasses.dataclass
952class ParseBodyConfig:
953    """This class configures the parsing of request bodies.
954
955    .. versionadded:: 6.5.5
956    """
957
958    multipart: ParseMultipartConfig = dataclasses.field(
959        default_factory=ParseMultipartConfig
960    )
961    """Configuration for ``multipart/form-data`` request bodies."""
962
963
964_DEFAULT_PARSE_BODY_CONFIG = ParseBodyConfig()
965
966
967def set_parse_body_config(config: ParseBodyConfig) -> None:
968    r"""Sets the **global** default configuration for parsing request bodies.
969
970    This global setting is provided as a stopgap for applications that need to raise the limits
971    introduced in Tornado 6.5.5, or who wish to disable the parsing of multipart/form-data bodies
972    entirely. Non-global configuration for this functionality will be introduced in a future
973    release.
974
975    >>> content_type = "multipart/form-data; boundary=foo"
976    >>> multipart_body = b"--foo--\r\n"
977    >>> parse_body_arguments(content_type, multipart_body, {}, {})
978    >>> multipart_config = ParseMultipartConfig(enabled=False)
979    >>> config = ParseBodyConfig(multipart=multipart_config)
980    >>> set_parse_body_config(config)
981    >>> parse_body_arguments(content_type, multipart_body, {}, {})
982    Traceback (most recent call last):
983        ...
984    tornado.httputil.HTTPInputError: ...: multipart/form-data parsing is disabled
985    >>> set_parse_body_config(ParseBodyConfig())  # reset to defaults
986
987    .. versionadded:: 6.5.5
988    """
989    global _DEFAULT_PARSE_BODY_CONFIG
990    _DEFAULT_PARSE_BODY_CONFIG = config
991
992
993def parse_body_arguments(
994    content_type: str,
995    body: bytes,
996    arguments: Dict[str, List[bytes]],
997    files: Dict[str, List[HTTPFile]],
998    headers: Optional[HTTPHeaders] = None,
999    *,
1000    config: Optional[ParseBodyConfig] = None,
1001) -> None:
1002    """Parses a form request body.
1003
1004    Supports ``application/x-www-form-urlencoded`` and
1005    ``multipart/form-data``.  The ``content_type`` parameter should be
1006    a string and ``body`` should be a byte string.  The ``arguments``
1007    and ``files`` parameters are dictionaries that will be updated
1008    with the parsed contents.
1009    """
1010    if config is None:
1011        config = _DEFAULT_PARSE_BODY_CONFIG
1012    if content_type.startswith("application/x-www-form-urlencoded"):
1013        if headers and "Content-Encoding" in headers:
1014            raise HTTPInputError(
1015                "Unsupported Content-Encoding: %s" % headers["Content-Encoding"]
1016            )
1017        try:
1018            # real charset decoding will happen in RequestHandler.decode_argument()
1019            uri_arguments = parse_qs_bytes(body, keep_blank_values=True)
1020        except Exception as e:
1021            raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) from e
1022        for name, values in uri_arguments.items():
1023            if values:
1024                arguments.setdefault(name, []).extend(values)
1025    elif content_type.startswith("multipart/form-data"):
1026        if headers and "Content-Encoding" in headers:
1027            raise HTTPInputError(
1028                "Unsupported Content-Encoding: %s" % headers["Content-Encoding"]
1029            )
1030        try:
1031            fields = content_type.split(";")
1032            if fields[0].strip() != "multipart/form-data":
1033                # This catches "Content-Type: multipart/form-dataxyz"
1034                raise HTTPInputError("Invalid content type")
1035            for field in fields:
1036                k, sep, v = field.strip().partition("=")
1037                if k == "boundary" and v:
1038                    parse_multipart_form_data(
1039                        utf8(v), body, arguments, files, config=config.multipart
1040                    )
1041                    break
1042            else:
1043                raise HTTPInputError("multipart boundary not found")
1044        except Exception as e:
1045            raise HTTPInputError("Invalid multipart/form-data: %s" % e) from e
1046
1047
1048def parse_multipart_form_data(
1049    boundary: bytes,
1050    data: bytes,
1051    arguments: Dict[str, List[bytes]],
1052    files: Dict[str, List[HTTPFile]],
1053    *,
1054    config: Optional[ParseMultipartConfig] = None,
1055) -> None:
1056    """Parses a ``multipart/form-data`` body.
1057
1058    The ``boundary`` and ``data`` parameters are both byte strings.
1059    The dictionaries given in the arguments and files parameters
1060    will be updated with the contents of the body.
1061
1062    .. versionchanged:: 5.1
1063
1064       Now recognizes non-ASCII filenames in RFC 2231/5987
1065       (``filename*=``) format.
1066    """
1067    if config is None:
1068        config = _DEFAULT_PARSE_BODY_CONFIG.multipart
1069    if not config.enabled:
1070        raise HTTPInputError("multipart/form-data parsing is disabled")
1071    # The standard allows for the boundary to be quoted in the header,
1072    # although it's rare (it happens at least for google app engine
1073    # xmpp).  I think we're also supposed to handle backslash-escapes
1074    # here but I'll save that until we see a client that uses them
1075    # in the wild.
1076    if boundary.startswith(b'"') and boundary.endswith(b'"'):
1077        boundary = boundary[1:-1]
1078    final_boundary_index = data.rfind(b"--" + boundary + b"--")
1079    if final_boundary_index == -1:
1080        raise HTTPInputError("Invalid multipart/form-data: no final boundary found")
1081    parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
1082    if len(parts) > config.max_parts:
1083        raise HTTPInputError("multipart/form-data has too many parts")
1084    for part in parts:
1085        if not part:
1086            continue
1087        eoh = part.find(b"\r\n\r\n")
1088        if eoh == -1:
1089            raise HTTPInputError("multipart/form-data missing headers")
1090        if eoh > config.max_part_header_size:
1091            raise HTTPInputError("multipart/form-data part header too large")
1092        headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"), _chars_are_bytes=False)
1093        disp_header = headers.get("Content-Disposition", "")
1094        disposition, disp_params = _parse_header(disp_header)
1095        if disposition != "form-data" or not part.endswith(b"\r\n"):
1096            raise HTTPInputError("Invalid multipart/form-data")
1097        value = part[eoh + 4 : -2]
1098        if not disp_params.get("name"):
1099            raise HTTPInputError("multipart/form-data missing name")
1100        name = disp_params["name"]
1101        if disp_params.get("filename"):
1102            ctype = headers.get("Content-Type", "application/unknown")
1103            files.setdefault(name, []).append(
1104                HTTPFile(
1105                    filename=disp_params["filename"], body=value, content_type=ctype
1106                )
1107            )
1108        else:
1109            arguments.setdefault(name, []).append(value)
1110
1111
1112def format_timestamp(
1113    ts: Union[int, float, tuple, time.struct_time, datetime.datetime],
1114) -> str:
1115    """Formats a timestamp in the format used by HTTP.
1116
1117    The argument may be a numeric timestamp as returned by `time.time`,
1118    a time tuple as returned by `time.gmtime`, or a `datetime.datetime`
1119    object. Naive `datetime.datetime` objects are assumed to represent
1120    UTC; aware objects are converted to UTC before formatting.
1121
1122    >>> format_timestamp(1359312200)
1123    'Sun, 27 Jan 2013 18:43:20 GMT'
1124    """
1125    if isinstance(ts, (int, float)):
1126        time_num = ts
1127    elif isinstance(ts, (tuple, time.struct_time)):
1128        time_num = calendar.timegm(ts)
1129    elif isinstance(ts, datetime.datetime):
1130        time_num = calendar.timegm(ts.utctimetuple())
1131    else:
1132        raise TypeError("unknown timestamp type: %r" % ts)
1133    return email.utils.formatdate(time_num, usegmt=True)
1134
1135
1136class RequestStartLine(typing.NamedTuple):
1137    method: str
1138    path: str
1139    version: str
1140
1141
1142def parse_request_start_line(line: str) -> RequestStartLine:
1143    """Returns a (method, path, version) tuple for an HTTP 1.x request line.
1144
1145    The response is a `typing.NamedTuple`.
1146
1147    >>> parse_request_start_line("GET /foo HTTP/1.1")
1148    RequestStartLine(method='GET', path='/foo', version='HTTP/1.1')
1149    """
1150    match = _ABNF.request_line.fullmatch(line)
1151    if not match:
1152        # https://tools.ietf.org/html/rfc7230#section-3.1.1
1153        # invalid request-line SHOULD respond with a 400 (Bad Request)
1154        raise HTTPInputError("Malformed HTTP request line")
1155    r = RequestStartLine(match.group(1), match.group(2), match.group(3))
1156    if not r.version.startswith("HTTP/1"):
1157        # HTTP/2 and above doesn't use parse_request_start_line.
1158        # This could be folded into the regex but we don't want to deviate
1159        # from the ABNF in the RFCs.
1160        raise HTTPInputError("Unexpected HTTP version %r" % r.version)
1161    return r
1162
1163
1164class ResponseStartLine(typing.NamedTuple):
1165    version: str
1166    code: int
1167    reason: str
1168
1169
1170def parse_response_start_line(line: str) -> ResponseStartLine:
1171    """Returns a (version, code, reason) tuple for an HTTP 1.x response line.
1172
1173    The response is a `typing.NamedTuple`.
1174
1175    >>> parse_response_start_line("HTTP/1.1 200 OK")
1176    ResponseStartLine(version='HTTP/1.1', code=200, reason='OK')
1177    """
1178    match = _ABNF.status_line.fullmatch(line)
1179    if not match:
1180        raise HTTPInputError("Error parsing response start line")
1181    r = ResponseStartLine(match.group(1), int(match.group(2)), match.group(3))
1182    if not r.version.startswith("HTTP/1"):
1183        # HTTP/2 and above doesn't use parse_response_start_line.
1184        raise HTTPInputError("Unexpected HTTP version %r" % r.version)
1185    return r
1186
1187
1188# _parseparam and _parse_header are copied and modified from python2.7's cgi.py
1189# The original 2.7 version of this code did not correctly support some
1190# combinations of semicolons and double quotes.
1191# It has also been modified to support valueless parameters as seen in
1192# websocket extension negotiations, and to support non-ascii values in
1193# RFC 2231/5987 format.
1194#
1195# _parseparam has been further modified with the logic from
1196# https://github.com/python/cpython/pull/136072/files
1197# to avoid quadratic behavior when parsing semicolons in quoted strings.
1198#
1199# TODO: See if we can switch to email.message.Message for this functionality.
1200# This is the suggested replacement for the cgi.py module now that cgi has

Showing the first 1,200 of 1393 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai