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