536 lines
20 KiB
Python
536 lines
20 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
from datetime import datetime
|
||
|
from urllib.parse import parse_qsl
|
||
|
|
||
|
from ..datastructures import Accept
|
||
|
from ..datastructures import Authorization
|
||
|
from ..datastructures import CharsetAccept
|
||
|
from ..datastructures import ETags
|
||
|
from ..datastructures import Headers
|
||
|
from ..datastructures import HeaderSet
|
||
|
from ..datastructures import IfRange
|
||
|
from ..datastructures import ImmutableList
|
||
|
from ..datastructures import ImmutableMultiDict
|
||
|
from ..datastructures import LanguageAccept
|
||
|
from ..datastructures import MIMEAccept
|
||
|
from ..datastructures import MultiDict
|
||
|
from ..datastructures import Range
|
||
|
from ..datastructures import RequestCacheControl
|
||
|
from ..http import parse_accept_header
|
||
|
from ..http import parse_cache_control_header
|
||
|
from ..http import parse_date
|
||
|
from ..http import parse_etags
|
||
|
from ..http import parse_if_range_header
|
||
|
from ..http import parse_list_header
|
||
|
from ..http import parse_options_header
|
||
|
from ..http import parse_range_header
|
||
|
from ..http import parse_set_header
|
||
|
from ..user_agent import UserAgent
|
||
|
from ..utils import cached_property
|
||
|
from ..utils import header_property
|
||
|
from .http import parse_cookie
|
||
|
from .utils import get_content_length
|
||
|
from .utils import get_current_url
|
||
|
from .utils import get_host
|
||
|
|
||
|
|
||
|
class Request:
|
||
|
"""Represents the non-IO parts of a HTTP request, including the
|
||
|
method, URL info, and headers.
|
||
|
|
||
|
This class is not meant for general use. It should only be used when
|
||
|
implementing WSGI, ASGI, or another HTTP application spec. Werkzeug
|
||
|
provides a WSGI implementation at :cls:`werkzeug.wrappers.Request`.
|
||
|
|
||
|
:param method: The method the request was made with, such as
|
||
|
``GET``.
|
||
|
:param scheme: The URL scheme of the protocol the request used, such
|
||
|
as ``https`` or ``wss``.
|
||
|
:param server: The address of the server. ``(host, port)``,
|
||
|
``(path, None)`` for unix sockets, or ``None`` if not known.
|
||
|
:param root_path: The prefix that the application is mounted under.
|
||
|
This is prepended to generated URLs, but is not part of route
|
||
|
matching.
|
||
|
:param path: The path part of the URL after ``root_path``.
|
||
|
:param query_string: The part of the URL after the "?".
|
||
|
:param headers: The headers received with the request.
|
||
|
:param remote_addr: The address of the client sending the request.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
The ``charset``, ``url_charset``, and ``encoding_errors`` attributes
|
||
|
were removed.
|
||
|
|
||
|
.. versionadded:: 2.0
|
||
|
"""
|
||
|
|
||
|
#: the class to use for `args` and `form`. The default is an
|
||
|
#: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports
|
||
|
#: multiple values per key. alternatively it makes sense to use an
|
||
|
#: :class:`~werkzeug.datastructures.ImmutableOrderedMultiDict` which
|
||
|
#: preserves order or a :class:`~werkzeug.datastructures.ImmutableDict`
|
||
|
#: which is the fastest but only remembers the last key. It is also
|
||
|
#: possible to use mutable structures, but this is not recommended.
|
||
|
#:
|
||
|
#: .. versionadded:: 0.6
|
||
|
parameter_storage_class: type[MultiDict] = ImmutableMultiDict
|
||
|
|
||
|
#: The type to be used for dict values from the incoming WSGI
|
||
|
#: environment. (For example for :attr:`cookies`.) By default an
|
||
|
#: :class:`~werkzeug.datastructures.ImmutableMultiDict` is used.
|
||
|
#:
|
||
|
#: .. versionchanged:: 1.0.0
|
||
|
#: Changed to ``ImmutableMultiDict`` to support multiple values.
|
||
|
#:
|
||
|
#: .. versionadded:: 0.6
|
||
|
dict_storage_class: type[MultiDict] = ImmutableMultiDict
|
||
|
|
||
|
#: the type to be used for list values from the incoming WSGI environment.
|
||
|
#: By default an :class:`~werkzeug.datastructures.ImmutableList` is used
|
||
|
#: (for example for :attr:`access_list`).
|
||
|
#:
|
||
|
#: .. versionadded:: 0.6
|
||
|
list_storage_class: type[list] = ImmutableList
|
||
|
|
||
|
user_agent_class: type[UserAgent] = UserAgent
|
||
|
"""The class used and returned by the :attr:`user_agent` property to
|
||
|
parse the header. Defaults to
|
||
|
:class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An
|
||
|
extension can provide a subclass that uses a parser to provide other
|
||
|
data.
|
||
|
|
||
|
.. versionadded:: 2.0
|
||
|
"""
|
||
|
|
||
|
#: Valid host names when handling requests. By default all hosts are
|
||
|
#: trusted, which means that whatever the client says the host is
|
||
|
#: will be accepted.
|
||
|
#:
|
||
|
#: Because ``Host`` and ``X-Forwarded-Host`` headers can be set to
|
||
|
#: any value by a malicious client, it is recommended to either set
|
||
|
#: this property or implement similar validation in the proxy (if
|
||
|
#: the application is being run behind one).
|
||
|
#:
|
||
|
#: .. versionadded:: 0.9
|
||
|
trusted_hosts: list[str] | None = None
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
method: str,
|
||
|
scheme: str,
|
||
|
server: tuple[str, int | None] | None,
|
||
|
root_path: str,
|
||
|
path: str,
|
||
|
query_string: bytes,
|
||
|
headers: Headers,
|
||
|
remote_addr: str | None,
|
||
|
) -> None:
|
||
|
#: The method the request was made with, such as ``GET``.
|
||
|
self.method = method.upper()
|
||
|
#: The URL scheme of the protocol the request used, such as
|
||
|
#: ``https`` or ``wss``.
|
||
|
self.scheme = scheme
|
||
|
#: The address of the server. ``(host, port)``, ``(path, None)``
|
||
|
#: for unix sockets, or ``None`` if not known.
|
||
|
self.server = server
|
||
|
#: The prefix that the application is mounted under, without a
|
||
|
#: trailing slash. :attr:`path` comes after this.
|
||
|
self.root_path = root_path.rstrip("/")
|
||
|
#: The path part of the URL after :attr:`root_path`. This is the
|
||
|
#: path used for routing within the application.
|
||
|
self.path = "/" + path.lstrip("/")
|
||
|
#: The part of the URL after the "?". This is the raw value, use
|
||
|
#: :attr:`args` for the parsed values.
|
||
|
self.query_string = query_string
|
||
|
#: The headers received with the request.
|
||
|
self.headers = headers
|
||
|
#: The address of the client sending the request.
|
||
|
self.remote_addr = remote_addr
|
||
|
|
||
|
def __repr__(self) -> str:
|
||
|
try:
|
||
|
url = self.url
|
||
|
except Exception as e:
|
||
|
url = f"(invalid URL: {e})"
|
||
|
|
||
|
return f"<{type(self).__name__} {url!r} [{self.method}]>"
|
||
|
|
||
|
@cached_property
|
||
|
def args(self) -> MultiDict[str, str]:
|
||
|
"""The parsed URL parameters (the part in the URL after the question
|
||
|
mark).
|
||
|
|
||
|
By default an
|
||
|
:class:`~werkzeug.datastructures.ImmutableMultiDict`
|
||
|
is returned from this function. This can be changed by setting
|
||
|
:attr:`parameter_storage_class` to a different type. This might
|
||
|
be necessary if the order of the form data is important.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Invalid bytes remain percent encoded.
|
||
|
"""
|
||
|
return self.parameter_storage_class(
|
||
|
parse_qsl(
|
||
|
self.query_string.decode(),
|
||
|
keep_blank_values=True,
|
||
|
errors="werkzeug.url_quote",
|
||
|
)
|
||
|
)
|
||
|
|
||
|
@cached_property
|
||
|
def access_route(self) -> list[str]:
|
||
|
"""If a forwarded header exists this is a list of all ip addresses
|
||
|
from the client ip to the last proxy server.
|
||
|
"""
|
||
|
if "X-Forwarded-For" in self.headers:
|
||
|
return self.list_storage_class(
|
||
|
parse_list_header(self.headers["X-Forwarded-For"])
|
||
|
)
|
||
|
elif self.remote_addr is not None:
|
||
|
return self.list_storage_class([self.remote_addr])
|
||
|
return self.list_storage_class()
|
||
|
|
||
|
@cached_property
|
||
|
def full_path(self) -> str:
|
||
|
"""Requested path, including the query string."""
|
||
|
return f"{self.path}?{self.query_string.decode()}"
|
||
|
|
||
|
@property
|
||
|
def is_secure(self) -> bool:
|
||
|
"""``True`` if the request was made with a secure protocol
|
||
|
(HTTPS or WSS).
|
||
|
"""
|
||
|
return self.scheme in {"https", "wss"}
|
||
|
|
||
|
@cached_property
|
||
|
def url(self) -> str:
|
||
|
"""The full request URL with the scheme, host, root path, path,
|
||
|
and query string."""
|
||
|
return get_current_url(
|
||
|
self.scheme, self.host, self.root_path, self.path, self.query_string
|
||
|
)
|
||
|
|
||
|
@cached_property
|
||
|
def base_url(self) -> str:
|
||
|
"""Like :attr:`url` but without the query string."""
|
||
|
return get_current_url(self.scheme, self.host, self.root_path, self.path)
|
||
|
|
||
|
@cached_property
|
||
|
def root_url(self) -> str:
|
||
|
"""The request URL scheme, host, and root path. This is the root
|
||
|
that the application is accessed from.
|
||
|
"""
|
||
|
return get_current_url(self.scheme, self.host, self.root_path)
|
||
|
|
||
|
@cached_property
|
||
|
def host_url(self) -> str:
|
||
|
"""The request URL scheme and host only."""
|
||
|
return get_current_url(self.scheme, self.host)
|
||
|
|
||
|
@cached_property
|
||
|
def host(self) -> str:
|
||
|
"""The host name the request was made to, including the port if
|
||
|
it's non-standard. Validated with :attr:`trusted_hosts`.
|
||
|
"""
|
||
|
return get_host(
|
||
|
self.scheme, self.headers.get("host"), self.server, self.trusted_hosts
|
||
|
)
|
||
|
|
||
|
@cached_property
|
||
|
def cookies(self) -> ImmutableMultiDict[str, str]:
|
||
|
"""A :class:`dict` with the contents of all cookies transmitted with
|
||
|
the request."""
|
||
|
wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie"))
|
||
|
return parse_cookie( # type: ignore
|
||
|
wsgi_combined_cookie, cls=self.dict_storage_class
|
||
|
)
|
||
|
|
||
|
# Common Descriptors
|
||
|
|
||
|
content_type = header_property[str](
|
||
|
"Content-Type",
|
||
|
doc="""The Content-Type entity-header field indicates the media
|
||
|
type of the entity-body sent to the recipient or, in the case of
|
||
|
the HEAD method, the media type that would have been sent had
|
||
|
the request been a GET.""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
|
||
|
@cached_property
|
||
|
def content_length(self) -> int | None:
|
||
|
"""The Content-Length entity-header field indicates the size of the
|
||
|
entity-body in bytes or, in the case of the HEAD method, the size of
|
||
|
the entity-body that would have been sent had the request been a
|
||
|
GET.
|
||
|
"""
|
||
|
return get_content_length(
|
||
|
http_content_length=self.headers.get("Content-Length"),
|
||
|
http_transfer_encoding=self.headers.get("Transfer-Encoding"),
|
||
|
)
|
||
|
|
||
|
content_encoding = header_property[str](
|
||
|
"Content-Encoding",
|
||
|
doc="""The Content-Encoding entity-header field is used as a
|
||
|
modifier to the media-type. When present, its value indicates
|
||
|
what additional content codings have been applied to the
|
||
|
entity-body, and thus what decoding mechanisms must be applied
|
||
|
in order to obtain the media-type referenced by the Content-Type
|
||
|
header field.
|
||
|
|
||
|
.. versionadded:: 0.9""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
content_md5 = header_property[str](
|
||
|
"Content-MD5",
|
||
|
doc="""The Content-MD5 entity-header field, as defined in
|
||
|
RFC 1864, is an MD5 digest of the entity-body for the purpose of
|
||
|
providing an end-to-end message integrity check (MIC) of the
|
||
|
entity-body. (Note: a MIC is good for detecting accidental
|
||
|
modification of the entity-body in transit, but is not proof
|
||
|
against malicious attacks.)
|
||
|
|
||
|
.. versionadded:: 0.9""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
referrer = header_property[str](
|
||
|
"Referer",
|
||
|
doc="""The Referer[sic] request-header field allows the client
|
||
|
to specify, for the server's benefit, the address (URI) of the
|
||
|
resource from which the Request-URI was obtained (the
|
||
|
"referrer", although the header field is misspelled).""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
date = header_property(
|
||
|
"Date",
|
||
|
None,
|
||
|
parse_date,
|
||
|
doc="""The Date general-header field represents the date and
|
||
|
time at which the message was originated, having the same
|
||
|
semantics as orig-date in RFC 822.
|
||
|
|
||
|
.. versionchanged:: 2.0
|
||
|
The datetime object is timezone-aware.
|
||
|
""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
max_forwards = header_property(
|
||
|
"Max-Forwards",
|
||
|
None,
|
||
|
int,
|
||
|
doc="""The Max-Forwards request-header field provides a
|
||
|
mechanism with the TRACE and OPTIONS methods to limit the number
|
||
|
of proxies or gateways that can forward the request to the next
|
||
|
inbound server.""",
|
||
|
read_only=True,
|
||
|
)
|
||
|
|
||
|
def _parse_content_type(self) -> None:
|
||
|
if not hasattr(self, "_parsed_content_type"):
|
||
|
self._parsed_content_type = parse_options_header(
|
||
|
self.headers.get("Content-Type", "")
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def mimetype(self) -> str:
|
||
|
"""Like :attr:`content_type`, but without parameters (eg, without
|
||
|
charset, type etc.) and always lowercase. For example if the content
|
||
|
type is ``text/HTML; charset=utf-8`` the mimetype would be
|
||
|
``'text/html'``.
|
||
|
"""
|
||
|
self._parse_content_type()
|
||
|
return self._parsed_content_type[0].lower()
|
||
|
|
||
|
@property
|
||
|
def mimetype_params(self) -> dict[str, str]:
|
||
|
"""The mimetype parameters as dict. For example if the content
|
||
|
type is ``text/html; charset=utf-8`` the params would be
|
||
|
``{'charset': 'utf-8'}``.
|
||
|
"""
|
||
|
self._parse_content_type()
|
||
|
return self._parsed_content_type[1]
|
||
|
|
||
|
@cached_property
|
||
|
def pragma(self) -> HeaderSet:
|
||
|
"""The Pragma general-header field is used to include
|
||
|
implementation-specific directives that might apply to any recipient
|
||
|
along the request/response chain. All pragma directives specify
|
||
|
optional behavior from the viewpoint of the protocol; however, some
|
||
|
systems MAY require that behavior be consistent with the directives.
|
||
|
"""
|
||
|
return parse_set_header(self.headers.get("Pragma", ""))
|
||
|
|
||
|
# Accept
|
||
|
|
||
|
@cached_property
|
||
|
def accept_mimetypes(self) -> MIMEAccept:
|
||
|
"""List of mimetypes this client supports as
|
||
|
:class:`~werkzeug.datastructures.MIMEAccept` object.
|
||
|
"""
|
||
|
return parse_accept_header(self.headers.get("Accept"), MIMEAccept)
|
||
|
|
||
|
@cached_property
|
||
|
def accept_charsets(self) -> CharsetAccept:
|
||
|
"""List of charsets this client supports as
|
||
|
:class:`~werkzeug.datastructures.CharsetAccept` object.
|
||
|
"""
|
||
|
return parse_accept_header(self.headers.get("Accept-Charset"), CharsetAccept)
|
||
|
|
||
|
@cached_property
|
||
|
def accept_encodings(self) -> Accept:
|
||
|
"""List of encodings this client accepts. Encodings in a HTTP term
|
||
|
are compression encodings such as gzip. For charsets have a look at
|
||
|
:attr:`accept_charset`.
|
||
|
"""
|
||
|
return parse_accept_header(self.headers.get("Accept-Encoding"))
|
||
|
|
||
|
@cached_property
|
||
|
def accept_languages(self) -> LanguageAccept:
|
||
|
"""List of languages this client accepts as
|
||
|
:class:`~werkzeug.datastructures.LanguageAccept` object.
|
||
|
|
||
|
.. versionchanged 0.5
|
||
|
In previous versions this was a regular
|
||
|
:class:`~werkzeug.datastructures.Accept` object.
|
||
|
"""
|
||
|
return parse_accept_header(self.headers.get("Accept-Language"), LanguageAccept)
|
||
|
|
||
|
# ETag
|
||
|
|
||
|
@cached_property
|
||
|
def cache_control(self) -> RequestCacheControl:
|
||
|
"""A :class:`~werkzeug.datastructures.RequestCacheControl` object
|
||
|
for the incoming cache control headers.
|
||
|
"""
|
||
|
cache_control = self.headers.get("Cache-Control")
|
||
|
return parse_cache_control_header(cache_control, None, RequestCacheControl)
|
||
|
|
||
|
@cached_property
|
||
|
def if_match(self) -> ETags:
|
||
|
"""An object containing all the etags in the `If-Match` header.
|
||
|
|
||
|
:rtype: :class:`~werkzeug.datastructures.ETags`
|
||
|
"""
|
||
|
return parse_etags(self.headers.get("If-Match"))
|
||
|
|
||
|
@cached_property
|
||
|
def if_none_match(self) -> ETags:
|
||
|
"""An object containing all the etags in the `If-None-Match` header.
|
||
|
|
||
|
:rtype: :class:`~werkzeug.datastructures.ETags`
|
||
|
"""
|
||
|
return parse_etags(self.headers.get("If-None-Match"))
|
||
|
|
||
|
@cached_property
|
||
|
def if_modified_since(self) -> datetime | None:
|
||
|
"""The parsed `If-Modified-Since` header as a datetime object.
|
||
|
|
||
|
.. versionchanged:: 2.0
|
||
|
The datetime object is timezone-aware.
|
||
|
"""
|
||
|
return parse_date(self.headers.get("If-Modified-Since"))
|
||
|
|
||
|
@cached_property
|
||
|
def if_unmodified_since(self) -> datetime | None:
|
||
|
"""The parsed `If-Unmodified-Since` header as a datetime object.
|
||
|
|
||
|
.. versionchanged:: 2.0
|
||
|
The datetime object is timezone-aware.
|
||
|
"""
|
||
|
return parse_date(self.headers.get("If-Unmodified-Since"))
|
||
|
|
||
|
@cached_property
|
||
|
def if_range(self) -> IfRange:
|
||
|
"""The parsed ``If-Range`` header.
|
||
|
|
||
|
.. versionchanged:: 2.0
|
||
|
``IfRange.date`` is timezone-aware.
|
||
|
|
||
|
.. versionadded:: 0.7
|
||
|
"""
|
||
|
return parse_if_range_header(self.headers.get("If-Range"))
|
||
|
|
||
|
@cached_property
|
||
|
def range(self) -> Range | None:
|
||
|
"""The parsed `Range` header.
|
||
|
|
||
|
.. versionadded:: 0.7
|
||
|
|
||
|
:rtype: :class:`~werkzeug.datastructures.Range`
|
||
|
"""
|
||
|
return parse_range_header(self.headers.get("Range"))
|
||
|
|
||
|
# User Agent
|
||
|
|
||
|
@cached_property
|
||
|
def user_agent(self) -> UserAgent:
|
||
|
"""The user agent. Use ``user_agent.string`` to get the header
|
||
|
value. Set :attr:`user_agent_class` to a subclass of
|
||
|
:class:`~werkzeug.user_agent.UserAgent` to provide parsing for
|
||
|
the other properties or other extended data.
|
||
|
|
||
|
.. versionchanged:: 2.1
|
||
|
The built-in parser was removed. Set ``user_agent_class`` to a ``UserAgent``
|
||
|
subclass to parse data from the string.
|
||
|
"""
|
||
|
return self.user_agent_class(self.headers.get("User-Agent", ""))
|
||
|
|
||
|
# Authorization
|
||
|
|
||
|
@cached_property
|
||
|
def authorization(self) -> Authorization | None:
|
||
|
"""The ``Authorization`` header parsed into an :class:`.Authorization` object.
|
||
|
``None`` if the header is not present.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
:class:`Authorization` is no longer a ``dict``. The ``token`` attribute
|
||
|
was added for auth schemes that use a token instead of parameters.
|
||
|
"""
|
||
|
return Authorization.from_header(self.headers.get("Authorization"))
|
||
|
|
||
|
# CORS
|
||
|
|
||
|
origin = header_property[str](
|
||
|
"Origin",
|
||
|
doc=(
|
||
|
"The host that the request originated from. Set"
|
||
|
" :attr:`~CORSResponseMixin.access_control_allow_origin` on"
|
||
|
" the response to indicate which origins are allowed."
|
||
|
),
|
||
|
read_only=True,
|
||
|
)
|
||
|
|
||
|
access_control_request_headers = header_property(
|
||
|
"Access-Control-Request-Headers",
|
||
|
load_func=parse_set_header,
|
||
|
doc=(
|
||
|
"Sent with a preflight request to indicate which headers"
|
||
|
" will be sent with the cross origin request. Set"
|
||
|
" :attr:`~CORSResponseMixin.access_control_allow_headers`"
|
||
|
" on the response to indicate which headers are allowed."
|
||
|
),
|
||
|
read_only=True,
|
||
|
)
|
||
|
|
||
|
access_control_request_method = header_property[str](
|
||
|
"Access-Control-Request-Method",
|
||
|
doc=(
|
||
|
"Sent with a preflight request to indicate which method"
|
||
|
" will be used for the cross origin request. Set"
|
||
|
" :attr:`~CORSResponseMixin.access_control_allow_methods`"
|
||
|
" on the response to indicate which methods are allowed."
|
||
|
),
|
||
|
read_only=True,
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def is_json(self) -> bool:
|
||
|
"""Check if the mimetype indicates JSON data, either
|
||
|
:mimetype:`application/json` or :mimetype:`application/*+json`.
|
||
|
"""
|
||
|
mt = self.mimetype
|
||
|
return (
|
||
|
mt == "application/json"
|
||
|
or mt.startswith("application/")
|
||
|
and mt.endswith("+json")
|
||
|
)
|