1from __future__ import annotations
2
3import collections.abc as cabc
4import typing as t
5from datetime import datetime
6from urllib.parse import parse_qsl
7
8from ..datastructures import Accept
9from ..datastructures import Authorization
10from ..datastructures import ETags
11from ..datastructures import Headers
12from ..datastructures import HeaderSet
13from ..datastructures import IfRange
14from ..datastructures import ImmutableMultiDict
15from ..datastructures import LanguageAccept
16from ..datastructures import MIMEAccept
17from ..datastructures import Range
18from ..datastructures import RequestCacheControl
19from ..http import parse_date
20from ..http import parse_list_header
21from ..http import parse_options_header
22from ..http import SecFetchDest
23from ..http import SecFetchMode
24from ..http import SecFetchSite
25from ..user_agent import UserAgent
26from ..utils import cached_property
27from ..utils import header_property
28from .http import parse_cookie
29from .utils import get_content_length
30from .utils import get_current_url
31from .utils import get_host
32
33
34class Request:
35 """Represents the non-IO parts of a HTTP request, including the
36 method, URL info, and headers.
37
38 This class is not meant for general use. It should only be used when
39 implementing WSGI, ASGI, or another HTTP application spec. Werkzeug
40 provides a WSGI implementation at :cls:`werkzeug.wrappers.Request`.
41
42 :param method: The method the request was made with, such as
43 ``GET``.
44 :param scheme: The URL scheme of the protocol the request used, such
45 as ``https`` or ``wss``.
46 :param server: The address of the server. ``(host, port)``,
47 ``(path, None)`` for unix sockets, or ``None`` if not known.
48 :param root_path: The prefix that the application is mounted under.
49 This is prepended to generated URLs, but is not part of route
50 matching.
51 :param path: The path part of the URL after ``root_path``.
52 :param query_string: The part of the URL after the "?".
53 :param headers: The headers received with the request.
54 :param remote_addr: The address of the client sending the request.
55
56 .. versionchanged:: 3.0
57 The ``charset``, ``url_charset``, and ``encoding_errors`` attributes
58 were removed.
59
60 .. versionadded:: 2.0
61 """
62
63 #: The class to use for :attr:`args`, :attr:`form`, and :attr:`files`.
64 #:
65 #: .. deprecated:: 3.2
66 #: Will be removed in Werkzeug 3.3. It will always be ``ImmutableMultiDict``.
67 #:
68 #: .. versionadded:: 0.6
69 parameter_storage_class: None = None
70
71 #: The class to use for parsed dict values, such as :attr:`cookies`.
72 #:
73 #: .. deprecated:: 3.2
74 #: Will be removed in Werkzeug 3.3. It will always be ``ImmutableMultiDict``.
75 #:
76 #: .. versionchanged:: 1.0.0
77 #: Changed to ``ImmutableMultiDict`` to support multiple values.
78 #:
79 #: .. versionadded:: 0.6
80 dict_storage_class: None = None
81
82 #: The class to use for parsed list values, such as :attr:`access_route`.
83 #:
84 #: .. deprecated:: 3.2
85 #: Will be removed in Werkzeug 3.3. It will always be ``Sequence``.
86 #:
87 #: .. versionadded:: 0.6
88 list_storage_class: None = None
89
90 user_agent_class: type[UserAgent] = UserAgent
91 """The class used and returned by the :attr:`user_agent` property to
92 parse the header. Defaults to
93 :class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An
94 extension can provide a subclass that uses a parser to provide other
95 data.
96
97 .. versionadded:: 2.0
98 """
99
100 #: Valid host names when handling requests. By default all hosts are
101 #: trusted, which means that whatever the client says the host is
102 #: will be accepted.
103 #:
104 #: Because ``Host`` and ``X-Forwarded-Host`` headers can be set to
105 #: any value by a malicious client, it is recommended to either set
106 #: this property or implement similar validation in the proxy (if
107 #: the application is being run behind one).
108 #:
109 #: .. versionadded:: 0.9
110 trusted_hosts: list[str] | None = None
111
112 def __init__(
113 self,
114 method: str,
115 scheme: str,
116 server: tuple[str, int | None] | None,
117 root_path: str,
118 path: str,
119 query_string: bytes,
120 headers: Headers,
121 remote_addr: str | None,
122 ) -> None:
123 #: The method the request was made with, such as ``GET``.
124 self.method = method.upper()
125 #: The URL scheme of the protocol the request used, such as
126 #: ``https`` or ``wss``.
127 self.scheme = scheme
128 #: The address of the server. ``(host, port)``, ``(path, None)``
129 #: for unix sockets, or ``None`` if not known.
130 self.server = server
131 #: The prefix that the application is mounted under, without a
132 #: trailing slash. :attr:`path` comes after this.
133 self.root_path = root_path.rstrip("/")
134 #: The path part of the URL after :attr:`root_path`. This is the
135 #: path used for routing within the application.
136 self.path = "/" + path.lstrip("/")
137 #: The part of the URL after the "?". This is the raw value, use
138 #: :attr:`args` for the parsed values.
139 self.query_string = query_string
140 #: The headers received with the request.
141 self.headers = headers
142 #: The address of the client sending the request.
143 self.remote_addr = remote_addr
144
145 def __repr__(self) -> str:
146 try:
147 url = self.url
148 except Exception as e:
149 url = f"(invalid URL: {e})"
150
151 return f"<{type(self).__name__} {url!r} [{self.method}]>"
152
153 @cached_property
154 def args(self) -> ImmutableMultiDict[str, str]:
155 """The parsed URL query parameters (the ``?key=value&a=b`` part of a
156 URL) as an :class:`ImmutableMultiDict`.
157
158 .. versionchanged:: 2.3
159 Invalid bytes remain percent encoded.
160 """
161 items = parse_qsl(
162 self.query_string.decode(),
163 keep_blank_values=True,
164 errors="werkzeug.url_quote",
165 )
166
167 if self.parameter_storage_class is not None:
168 import warnings
169
170 warnings.warn(
171 "Setting 'Request.parameter_storage_class' is deprecated and will be"
172 " removed in Werkzeug 3.3. It will always be 'ImmutableMultiDict'.",
173 DeprecationWarning,
174 stacklevel=2,
175 )
176 return self.parameter_storage_class(items)
177
178 return ImmutableMultiDict(items)
179
180 @cached_property
181 def access_route(self) -> cabc.Sequence[str]:
182 """The route taken from the client to the application.
183
184 This is ``X-Forwarded-For`` if it is set. Remember to only trust the
185 last N values, where N is the number of servers setting this header in
186 front of the application.
187
188 Otherwise, this only contains :attr:`remote_addr`, or is empty.
189 """
190 if "X-Forwarded-For" in self.headers:
191 items = parse_list_header(self.headers["X-Forwarded-For"])
192 elif self.remote_addr is not None:
193 items = [self.remote_addr]
194 else:
195 items = []
196
197 if self.list_storage_class is not None:
198 import warnings
199
200 warnings.warn(
201 "Setting 'Request.list_storage_class' is deprecated and will be"
202 " removed in Werkzeug 3.3. It will always be 'Sequence'.",
203 DeprecationWarning,
204 stacklevel=2,
205 )
206 return self.list_storage_class(items)
207
208 return items
209
210 @cached_property
211 def full_path(self) -> str:
212 """Requested path, including the query string."""
213 return f"{self.path}?{self.query_string.decode()}"
214
215 @property
216 def is_secure(self) -> bool:
217 """``True`` if the request was made with a secure protocol
218 (HTTPS or WSS).
219 """
220 return self.scheme in {"https", "wss"}
221
222 @cached_property
223 def url(self) -> str:
224 """The full request URL with the scheme, host, root path, path,
225 and query string."""
226 return get_current_url(
227 self.scheme, self.host, self.root_path, self.path, self.query_string
228 )
229
230 @cached_property
231 def base_url(self) -> str:
232 """Like :attr:`url` but without the query string."""
233 return get_current_url(self.scheme, self.host, self.root_path, self.path)
234
235 @cached_property
236 def root_url(self) -> str:
237 """The request URL scheme, host, and root path. This is the root
238 that the application is accessed from.
239 """
240 return get_current_url(self.scheme, self.host, self.root_path)
241
242 @cached_property
243 def host_url(self) -> str:
244 """The request URL scheme and host only."""
245 return get_current_url(self.scheme, self.host)
246
247 @cached_property
248 def host(self) -> str:
249 """The host name the request was made to, including the port if
250 it's non-standard. Validated with :attr:`trusted_hosts`.
251
252 See :func:`.get_host` for a detailed explanation.
253 """
254 return get_host(
255 self.scheme, self.headers.get("Host"), self.server, self.trusted_hosts
256 )
257
258 @cached_property
259 def cookies(self) -> ImmutableMultiDict[str, str]:
260 """A :class:`dict` with the contents of all cookies transmitted with
261 the request."""
262 wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie"))
263 kwargs: dict[str, t.Any] = {}
264
265 if self.dict_storage_class is not None:
266 kwargs["cls"] = self.dict_storage_class
267
268 return parse_cookie(wsgi_combined_cookie, **kwargs)
269
270 # Common Descriptors
271
272 content_type = header_property[str](
273 "Content-Type",
274 doc="""The Content-Type entity-header field indicates the media
275 type of the entity-body sent to the recipient or, in the case of
276 the HEAD method, the media type that would have been sent had
277 the request been a GET.""",
278 read_only=True,
279 )
280
281 @cached_property
282 def content_length(self) -> int | None:
283 """The Content-Length entity-header field indicates the size of the
284 entity-body in bytes or, in the case of the HEAD method, the size of
285 the entity-body that would have been sent had the request been a
286 GET.
287 """
288 return get_content_length(
289 http_content_length=self.headers.get("Content-Length"),
290 http_transfer_encoding=self.headers.get("Transfer-Encoding"),
291 )
292
293 content_encoding = header_property[str](
294 "Content-Encoding",
295 doc="""The Content-Encoding entity-header field is used as a
296 modifier to the media-type. When present, its value indicates
297 what additional content codings have been applied to the
298 entity-body, and thus what decoding mechanisms must be applied
299 in order to obtain the media-type referenced by the Content-Type
300 header field.
301
302 .. versionadded:: 0.9""",
303 read_only=True,
304 )
305
306 @property
307 def content_md5(self) -> str | None:
308 """The ``Content-MD5`` header, an MD5 digest of the request body.
309
310 .. deprecated:: 3.2
311 The header has not been used for a long time. Will be removed
312 in Werkzeug 3.3.
313
314 .. versionadded:: 0.9
315 """
316 import warnings
317
318 warnings.warn(
319 "The 'content_md5' attribute is deprecated and will be removed in"
320 " Werkzeug 3.3. The header has not been used for a long time.",
321 DeprecationWarning,
322 stacklevel=2,
323 )
324 return self.headers.get("Content-MD5")
325
326 referrer = header_property[str](
327 "Referer",
328 doc="""The Referer[sic] request-header field allows the client
329 to specify, for the server's benefit, the address (URI) of the
330 resource from which the Request-URI was obtained (the
331 "referrer", although the header field is misspelled).""",
332 read_only=True,
333 )
334 date = header_property(
335 "Date",
336 None,
337 parse_date,
338 doc="""The Date general-header field represents the date and
339 time at which the message was originated, having the same
340 semantics as orig-date in RFC 822.
341
342 .. versionchanged:: 2.0
343 The datetime object is timezone-aware.
344 """,
345 read_only=True,
346 )
347 max_forwards = header_property(
348 "Max-Forwards",
349 None,
350 int,
351 doc="""The Max-Forwards request-header field provides a
352 mechanism with the TRACE and OPTIONS methods to limit the number
353 of proxies or gateways that can forward the request to the next
354 inbound server.""",
355 read_only=True,
356 )
357
358 def _parse_content_type(self) -> None:
359 if not hasattr(self, "_parsed_content_type"):
360 self._parsed_content_type = parse_options_header(
361 self.headers.get("Content-Type", "")
362 )
363
364 @property
365 def mimetype(self) -> str:
366 """Like :attr:`content_type`, but without parameters (eg, without
367 charset, type etc.) and always lowercase. For example if the content
368 type is ``text/HTML; charset=utf-8`` the mimetype would be
369 ``'text/html'``.
370 """
371 self._parse_content_type()
372 return self._parsed_content_type[0].lower()
373
374 @property
375 def mimetype_params(self) -> dict[str, str]:
376 """The mimetype parameters as dict. For example if the content
377 type is ``text/html; charset=utf-8`` the params would be
378 ``{'charset': 'utf-8'}``.
379 """
380 self._parse_content_type()
381 return self._parsed_content_type[1]
382
383 @property
384 def pragma(self) -> HeaderSet:
385 """The ``Pragma`` header.
386
387 .. deprecated:: 3.2
388 Use ``cache_control`` instead. Will be removed in Werkzeug 3.3.
389 """
390 import warnings
391
392 warnings.warn(
393 "The 'pragma' attribute is deprecated and will be removed in"
394 " Werkzeug 3.3. Use 'cache_control' instead.",
395 DeprecationWarning,
396 stacklevel=2,
397 )
398 return HeaderSet.from_header(self.headers.get("Pragma"))
399
400 # Accept
401
402 @cached_property
403 def accept_mimetypes(self) -> MIMEAccept:
404 """List of content types (MIME types) the client supports, from the
405 ``Accept`` header.
406 """
407 return MIMEAccept.from_header(self.headers.get("Accept"))
408
409 @cached_property
410 def accept_charsets(self) -> Accept:
411 """Text encodings (charsets) the client accepts, from the
412 ``Accept-Charset`` header.
413
414 .. deprecated:: 3.2
415 The header has not been used for a long time. Clients do not send
416 it. Assume UTF-8. Will be removed in Werkzeug 3.3.
417 """
418 import warnings
419
420 from ..datastructures.accept import _CharsetAccept
421
422 warnings.warn(
423 "The 'accept_charsets' attribute is deprecated and will be removed"
424 " in Werkzeug 3.3. The header is not sent by browsers, and UTF-8 is"
425 " assumed.",
426 DeprecationWarning,
427 stacklevel=2,
428 )
429 return _CharsetAccept.from_header(self.headers.get("Accept-Charset"))
430
431 @cached_property
432 def accept_encodings(self) -> Accept:
433 """Content encodings (compression) the client accepts, from the
434 ``Accept-Encoding`` header.
435 """
436 return Accept.from_header(self.headers.get("Accept-Encoding"))
437
438 @cached_property
439 def accept_languages(self) -> LanguageAccept:
440 """Languages the client accepts, from the ``Accept-Language`` header.
441
442 .. versionchanged 0.5
443 Returns ``LanguageAccept`` instead of ``Accept``.
444 """
445 return LanguageAccept.from_header(self.headers.get("Accept-Language"))
446
447 # ETag
448
449 @cached_property
450 def cache_control(self) -> RequestCacheControl:
451 """A :class:`~werkzeug.datastructures.RequestCacheControl` object
452 for the incoming cache control headers.
453 """
454 return RequestCacheControl.from_header(self.headers.get("Cache-Control"))
455
456 @cached_property
457 def if_match(self) -> ETags:
458 """ETags parsed from the ``If-Match`` header."""
459 return ETags.from_header(self.headers.get("If-Match"))
460
461 @cached_property
462 def if_none_match(self) -> ETags:
463 """ETags parsed from the ``If-None-Match`` header."""
464 return ETags.from_header(self.headers.get("If-None-Match"))
465
466 @cached_property
467 def if_modified_since(self) -> datetime | None:
468 """The parsed `If-Modified-Since` header as a datetime object.
469
470 .. versionchanged:: 2.0
471 The datetime object is timezone-aware.
472 """
473 return parse_date(self.headers.get("If-Modified-Since"))
474
475 @cached_property
476 def if_unmodified_since(self) -> datetime | None:
477 """The parsed `If-Unmodified-Since` header as a datetime object.
478
479 .. versionchanged:: 2.0
480 The datetime object is timezone-aware.
481 """
482 return parse_date(self.headers.get("If-Unmodified-Since"))
483
484 @cached_property
485 def if_range(self) -> IfRange:
486 """The parsed ``If-Range`` header.
487
488 .. versionchanged:: 3.2
489 A weak ETag is discarded.
490
491 .. versionchanged:: 2.0
492 ``IfRange.date`` is timezone-aware.
493
494 .. versionadded:: 0.7
495 """
496 return IfRange.from_header(self.headers.get("If-Range"))
497
498 @cached_property
499 def range(self) -> Range | None:
500 """The parsed `Range` header.
501
502 .. versionadded:: 0.7
503 """
504 return Range.from_header(self.headers.get("Range"))
505
506 # User Agent
507
508 @cached_property
509 def user_agent(self) -> UserAgent:
510 """The user agent. Use ``user_agent.string`` to get the header
511 value. Set :attr:`user_agent_class` to a subclass of
512 :class:`~werkzeug.user_agent.UserAgent` to provide parsing for
513 the other properties or other extended data.
514
515 .. versionchanged:: 2.1
516 The built-in parser was removed. Set ``user_agent_class`` to a ``UserAgent``
517 subclass to parse data from the string.
518 """
519 return self.user_agent_class(self.headers.get("User-Agent", ""))
520
521 # Authorization
522
523 @cached_property
524 def authorization(self) -> Authorization | None:
525 """The ``Authorization`` header parsed into an :class:`.Authorization` object.
526 ``None`` if the header is not present.
527
528 .. versionchanged:: 2.3
529 :class:`Authorization` is no longer a ``dict``. The ``token`` attribute
530 was added for auth schemes that use a token instead of parameters.
531 """
532 return Authorization.from_header(self.headers.get("Authorization"))
533
534 # CORS
535
536 origin = header_property[str](
537 "Origin",
538 doc=(
539 "The host that the request originated from. Set"
540 " :attr:`~CORSResponseMixin.access_control_allow_origin` on"
541 " the response to indicate which origins are allowed."
542 ),
543 read_only=True,
544 )
545
546 access_control_request_headers = header_property[HeaderSet](
547 "Access-Control-Request-Headers",
548 load_func=HeaderSet.from_header,
549 doc=(
550 "Sent with a preflight request to indicate which headers"
551 " will be sent with the cross origin request. Set"
552 " :attr:`~CORSResponseMixin.access_control_allow_headers`"
553 " on the response to indicate which headers are allowed."
554 ),
555 read_only=True,
556 )
557
558 access_control_request_method = header_property[str](
559 "Access-Control-Request-Method",
560 doc=(
561 "Sent with a preflight request to indicate which method"
562 " will be used for the cross origin request. Set"
563 " :attr:`~CORSResponseMixin.access_control_allow_methods`"
564 " on the response to indicate which methods are allowed."
565 ),
566 read_only=True,
567 )
568
569 sec_fetch_site = header_property[SecFetchSite](
570 "Sec-Fetch-Site",
571 load_func=SecFetchSite,
572 read_only=True,
573 doc="""Indicates the relationship between a request initiator's origin
574 and the origin of the requested resource.
575
576 Values are members of the :class:`.SecFetchSite` enum.
577
578 .. versionadded:: 3.2
579 """,
580 )
581
582 sec_fetch_mode = header_property[SecFetchMode](
583 "Sec-Fetch-Mode",
584 load_func=SecFetchMode,
585 read_only=True,
586 doc="""Distinguishes between requests originating from a user navigating
587 between HTML pages, and requests to load images and other resources.
588
589 Values are members of the :class:`.SecFetchMode` enum.
590
591 .. versionadded:: 3.2
592 """,
593 )
594
595 sec_fetch_user = header_property[bool](
596 "Sec-Fetch-User",
597 load_func=lambda value: value == "?1",
598 read_only=True,
599 doc="""Indicates whether a navigation request was originated by the user.
600
601 .. versionadded:: 3.2
602 """,
603 )
604
605 sec_fetch_dest = header_property[SecFetchDest](
606 "Sec-Fetch-Dest",
607 load_func=SecFetchDest,
608 read_only=True,
609 doc="""Indicates how the response to the request is expected to be used.
610
611 Values are members of the :class:`.SecFetchDest` enum.
612
613 .. versionadded:: 3.2
614 """,
615 )
616
617 @property
618 def is_json(self) -> bool:
619 """Check if the mimetype indicates JSON data, either
620 :mimetype:`application/json` or :mimetype:`application/*+json`.
621 """
622 mt = self.mimetype
623 return (
624 mt == "application/json"
625 or mt.startswith("application/")
626 and mt.endswith("+json")
627 )