Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/http.py: 52%
476 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
1from __future__ import annotations
3import email.utils
4import re
5import typing as t
6import warnings
7from datetime import date
8from datetime import datetime
9from datetime import time
10from datetime import timedelta
11from datetime import timezone
12from enum import Enum
13from hashlib import sha1
14from time import mktime
15from time import struct_time
16from urllib.parse import quote
17from urllib.parse import unquote
18from urllib.request import parse_http_list as _parse_list_header
20from ._internal import _dt_as_utc
21from ._internal import _plain_float
22from ._internal import _plain_int
24if t.TYPE_CHECKING:
25 from _typeshed.wsgi import WSGIEnvironment
27_token_chars = frozenset(
28 "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
29)
30_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
31_entity_headers = frozenset(
32 [
33 "allow",
34 "content-encoding",
35 "content-language",
36 "content-length",
37 "content-location",
38 "content-md5",
39 "content-range",
40 "content-type",
41 "expires",
42 "last-modified",
43 ]
44)
45_hop_by_hop_headers = frozenset(
46 [
47 "connection",
48 "keep-alive",
49 "proxy-authenticate",
50 "proxy-authorization",
51 "te",
52 "trailer",
53 "transfer-encoding",
54 "upgrade",
55 ]
56)
57HTTP_STATUS_CODES = {
58 100: "Continue",
59 101: "Switching Protocols",
60 102: "Processing",
61 103: "Early Hints", # see RFC 8297
62 200: "OK",
63 201: "Created",
64 202: "Accepted",
65 203: "Non Authoritative Information",
66 204: "No Content",
67 205: "Reset Content",
68 206: "Partial Content",
69 207: "Multi Status",
70 208: "Already Reported", # see RFC 5842
71 226: "IM Used", # see RFC 3229
72 300: "Multiple Choices",
73 301: "Moved Permanently",
74 302: "Found",
75 303: "See Other",
76 304: "Not Modified",
77 305: "Use Proxy",
78 306: "Switch Proxy", # unused
79 307: "Temporary Redirect",
80 308: "Permanent Redirect",
81 400: "Bad Request",
82 401: "Unauthorized",
83 402: "Payment Required", # unused
84 403: "Forbidden",
85 404: "Not Found",
86 405: "Method Not Allowed",
87 406: "Not Acceptable",
88 407: "Proxy Authentication Required",
89 408: "Request Timeout",
90 409: "Conflict",
91 410: "Gone",
92 411: "Length Required",
93 412: "Precondition Failed",
94 413: "Request Entity Too Large",
95 414: "Request URI Too Long",
96 415: "Unsupported Media Type",
97 416: "Requested Range Not Satisfiable",
98 417: "Expectation Failed",
99 418: "I'm a teapot", # see RFC 2324
100 421: "Misdirected Request", # see RFC 7540
101 422: "Unprocessable Entity",
102 423: "Locked",
103 424: "Failed Dependency",
104 425: "Too Early", # see RFC 8470
105 426: "Upgrade Required",
106 428: "Precondition Required", # see RFC 6585
107 429: "Too Many Requests",
108 431: "Request Header Fields Too Large",
109 449: "Retry With", # proprietary MS extension
110 451: "Unavailable For Legal Reasons",
111 500: "Internal Server Error",
112 501: "Not Implemented",
113 502: "Bad Gateway",
114 503: "Service Unavailable",
115 504: "Gateway Timeout",
116 505: "HTTP Version Not Supported",
117 506: "Variant Also Negotiates", # see RFC 2295
118 507: "Insufficient Storage",
119 508: "Loop Detected", # see RFC 5842
120 510: "Not Extended",
121 511: "Network Authentication Failed",
122}
125class COEP(Enum):
126 """Cross Origin Embedder Policies"""
128 UNSAFE_NONE = "unsafe-none"
129 REQUIRE_CORP = "require-corp"
132class COOP(Enum):
133 """Cross Origin Opener Policies"""
135 UNSAFE_NONE = "unsafe-none"
136 SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups"
137 SAME_ORIGIN = "same-origin"
140def quote_header_value(
141 value: t.Any,
142 extra_chars: str | None = None,
143 allow_token: bool = True,
144) -> str:
145 """Add double quotes around a header value. If the header contains only ASCII token
146 characters, it will be returned unchanged. If the header contains ``"`` or ``\\``
147 characters, they will be escaped with an additional ``\\`` character.
149 This is the reverse of :func:`unquote_header_value`.
151 :param value: The value to quote. Will be converted to a string.
152 :param allow_token: Disable to quote the value even if it only has token characters.
154 .. versionchanged:: 2.3
155 The value is quoted if it is the empty string.
157 .. versionchanged:: 2.3
158 Passing bytes is deprecated and will not be supported in Werkzeug 3.0.
160 .. versionchanged:: 2.3
161 The ``extra_chars`` parameter is deprecated and will be removed in Werkzeug 3.0.
163 .. versionadded:: 0.5
164 """
165 if isinstance(value, bytes):
166 warnings.warn(
167 "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.",
168 DeprecationWarning,
169 stacklevel=2,
170 )
171 value = value.decode("latin1")
173 if extra_chars is not None:
174 warnings.warn(
175 "The 'extra_chars' parameter is deprecated and will be"
176 " removed in Werkzeug 3.0.",
177 DeprecationWarning,
178 stacklevel=2,
179 )
181 value = str(value)
183 if not value:
184 return '""'
186 if allow_token:
187 token_chars = _token_chars
189 if extra_chars:
190 token_chars |= set(extra_chars)
192 if token_chars.issuperset(value):
193 return value
195 value = value.replace("\\", "\\\\").replace('"', '\\"')
196 return f'"{value}"'
199def unquote_header_value(value: str, is_filename: bool | None = None) -> str:
200 """Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a
201 header value.
203 This is the reverse of :func:`quote_header_value`.
205 :param value: The header value to unquote.
207 .. versionchanged:: 2.3
208 The ``is_filename`` parameter is deprecated and will be removed in Werkzeug 3.0.
209 """
210 if is_filename is not None:
211 warnings.warn(
212 "The 'is_filename' parameter is deprecated and will be"
213 " removed in Werkzeug 3.0.",
214 DeprecationWarning,
215 stacklevel=2,
216 )
218 if len(value) >= 2 and value[0] == value[-1] == '"':
219 value = value[1:-1]
221 if not is_filename:
222 return value.replace("\\\\", "\\").replace('\\"', '"')
224 return value
227def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str:
228 """Produce a header value and ``key=value`` parameters separated by semicolons
229 ``;``. For example, the ``Content-Type`` header.
231 .. code-block:: python
233 dump_options_header("text/html", {"charset": "UTF-8"})
234 'text/html; charset=UTF-8'
236 This is the reverse of :func:`parse_options_header`.
238 If a value contains non-token characters, it will be quoted.
240 If a value is ``None``, the parameter is skipped.
242 In some keys for some headers, a UTF-8 value can be encoded using a special
243 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
244 not produce that format automatically, but if a given key ends with an asterisk
245 ``*``, the value is assumed to have that form and will not be quoted further.
247 :param header: The primary header value.
248 :param options: Parameters to encode as ``key=value`` pairs.
250 .. versionchanged:: 2.3
251 Keys with ``None`` values are skipped rather than treated as a bare key.
253 .. versionchanged:: 2.2.3
254 If a key ends with ``*``, its value will not be quoted.
255 """
256 segments = []
258 if header is not None:
259 segments.append(header)
261 for key, value in options.items():
262 if value is None:
263 continue
265 if key[-1] == "*":
266 segments.append(f"{key}={value}")
267 else:
268 segments.append(f"{key}={quote_header_value(value)}")
270 return "; ".join(segments)
273def dump_header(
274 iterable: dict[str, t.Any] | t.Iterable[t.Any],
275 allow_token: bool | None = None,
276) -> str:
277 """Produce a header value from a list of items or ``key=value`` pairs, separated by
278 commas ``,``.
280 This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and
281 :func:`parse_set_header`.
283 If a value contains non-token characters, it will be quoted.
285 If a value is ``None``, the key is output alone.
287 In some keys for some headers, a UTF-8 value can be encoded using a special
288 ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will
289 not produce that format automatically, but if a given key ends with an asterisk
290 ``*``, the value is assumed to have that form and will not be quoted further.
292 .. code-block:: python
294 dump_header(["foo", "bar baz"])
295 'foo, "bar baz"'
297 dump_header({"foo": "bar baz"})
298 'foo="bar baz"'
300 :param iterable: The items to create a header from.
302 .. versionchanged:: 2.3
303 The ``allow_token`` parameter is deprecated and will be removed in Werkzeug 3.0.
305 .. versionchanged:: 2.2.3
306 If a key ends with ``*``, its value will not be quoted.
307 """
308 if allow_token is not None:
309 warnings.warn(
310 "'The 'allow_token' parameter is deprecated and will be"
311 " removed in Werkzeug 3.0.",
312 DeprecationWarning,
313 stacklevel=2,
314 )
315 else:
316 allow_token = True
318 if isinstance(iterable, dict):
319 items = []
321 for key, value in iterable.items():
322 if value is None:
323 items.append(key)
324 elif key[-1] == "*":
325 items.append(f"{key}={value}")
326 else:
327 items.append(
328 f"{key}={quote_header_value(value, allow_token=allow_token)}"
329 )
330 else:
331 items = [quote_header_value(x, allow_token=allow_token) for x in iterable]
333 return ", ".join(items)
336def dump_csp_header(header: ds.ContentSecurityPolicy) -> str:
337 """Dump a Content Security Policy header.
339 These are structured into policies such as "default-src 'self';
340 script-src 'self'".
342 .. versionadded:: 1.0.0
343 Support for Content Security Policy headers was added.
345 """
346 return "; ".join(f"{key} {value}" for key, value in header.items())
349def parse_list_header(value: str) -> list[str]:
350 """Parse a header value that consists of a list of comma separated items according
351 to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.
353 This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes
354 from values.
356 .. code-block:: python
358 parse_list_header('token, "quoted value"')
359 ['token', 'quoted value']
361 This is the reverse of :func:`dump_header`.
363 :param value: The header value to parse.
364 """
365 result = []
367 for item in _parse_list_header(value):
368 if len(item) >= 2 and item[0] == item[-1] == '"':
369 item = item[1:-1]
371 result.append(item)
373 return result
376def parse_dict_header(value: str, cls: type[dict] | None = None) -> dict[str, str]:
377 """Parse a list header using :func:`parse_list_header`, then parse each item as a
378 ``key=value`` pair.
380 .. code-block:: python
382 parse_dict_header('a=b, c="d, e", f')
383 {"a": "b", "c": "d, e", "f": None}
385 This is the reverse of :func:`dump_header`.
387 If a key does not have a value, it is ``None``.
389 This handles charsets for values as described in
390 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,
391 and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.
393 :param value: The header value to parse.
395 .. versionchanged:: 2.3
396 Added support for ``key*=charset''value`` encoded items.
398 .. versionchanged:: 2.3
399 Passing bytes is deprecated, support will be removed in Werkzeug 3.0.
401 .. versionchanged:: 2.3
402 The ``cls`` argument is deprecated and will be removed in Werkzeug 3.0.
404 .. versionchanged:: 0.9
405 The ``cls`` argument was added.
406 """
407 if cls is None:
408 cls = dict
409 else:
410 warnings.warn(
411 "The 'cls' parameter is deprecated and will be removed in Werkzeug 3.0.",
412 DeprecationWarning,
413 stacklevel=2,
414 )
416 result = cls()
418 if isinstance(value, bytes):
419 warnings.warn(
420 "Passing bytes is deprecated and will be removed in Werkzeug 3.0.",
421 DeprecationWarning,
422 stacklevel=2,
423 )
424 value = value.decode("latin1")
426 for item in parse_list_header(value):
427 key, has_value, value = item.partition("=")
428 key = key.strip()
430 if not has_value:
431 result[key] = None
432 continue
434 value = value.strip()
435 encoding: str | None = None
437 if key[-1] == "*":
438 # key*=charset''value becomes key=value, where value is percent encoded
439 # adapted from parse_options_header, without the continuation handling
440 key = key[:-1]
441 match = _charset_value_re.match(value)
443 if match:
444 # If there is a charset marker in the value, split it off.
445 encoding, value = match.groups()
446 encoding = encoding.lower()
448 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
449 # This list will not be extended further. An invalid encoding will leave the
450 # value quoted.
451 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
452 # invalid bytes are replaced during unquoting
453 value = unquote(value, encoding=encoding)
455 if len(value) >= 2 and value[0] == value[-1] == '"':
456 value = value[1:-1]
458 result[key] = value
460 return result
463# https://httpwg.org/specs/rfc9110.html#parameter
464_parameter_re = re.compile(
465 r"""
466 # don't match multiple empty parts, that causes backtracking
467 \s*;\s* # find the part delimiter
468 (?:
469 ([\w!#$%&'*+\-.^`|~]+) # key, one or more token chars
470 = # equals, with no space on either side
471 ( # value, token or quoted string
472 [\w!#$%&'*+\-.^`|~]+ # one or more token chars
473 |
474 "(?:\\\\|\\"|.)*?" # quoted string, consuming slash escapes
475 )
476 )? # optionally match key=value, to account for empty parts
477 """,
478 re.ASCII | re.VERBOSE,
479)
480# https://www.rfc-editor.org/rfc/rfc2231#section-4
481_charset_value_re = re.compile(
482 r"""
483 ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty
484 [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty
485 ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding
486 """,
487 re.ASCII | re.VERBOSE,
488)
489# https://www.rfc-editor.org/rfc/rfc2231#section-3
490_continuation_re = re.compile(r"\*(\d+)$", re.ASCII)
493def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]:
494 """Parse a header that consists of a value with ``key=value`` parameters separated
495 by semicolons ``;``. For example, the ``Content-Type`` header.
497 .. code-block:: python
499 parse_options_header("text/html; charset=UTF-8")
500 ('text/html', {'charset': 'UTF-8'})
502 parse_options_header("")
503 ("", {})
505 This is the reverse of :func:`dump_options_header`.
507 This parses valid parameter parts as described in
508 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are
509 skipped.
511 This handles continuations and charsets as described in
512 `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as
513 strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted,
514 otherwise the value remains quoted.
516 Clients may not be consistent in how they handle a quote character within a quoted
517 value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__
518 replaces it with ``%22`` in multipart form data.
519 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash
520 escapes in HTTP headers. Both are decoded to the ``"`` character.
522 Clients may not be consistent in how they handle non-ASCII characters. HTML
523 documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with
524 HTML character references, which can be decoded using :func:`html.unescape`.
526 :param value: The header value to parse.
527 :return: ``(value, options)``, where ``options`` is a dict
529 .. versionchanged:: 2.3
530 Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted
531 values, are discarded instead of treating as ``None``.
533 .. versionchanged:: 2.3
534 Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values.
536 .. versionchanged:: 2.3
537 Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled.
539 .. versionchanged:: 2.2
540 Option names are always converted to lowercase.
542 .. versionchanged:: 2.2
543 The ``multiple`` parameter was removed.
545 .. versionchanged:: 0.15
546 :rfc:`2231` parameter continuations are handled.
548 .. versionadded:: 0.5
549 """
550 if value is None:
551 return "", {}
553 value, _, rest = value.partition(";")
554 value = value.strip()
555 rest = rest.strip()
557 if not value or not rest:
558 # empty (invalid) value, or value without options
559 return value, {}
561 rest = f";{rest}"
562 options: dict[str, str] = {}
563 encoding: str | None = None
564 continued_encoding: str | None = None
566 for pk, pv in _parameter_re.findall(rest):
567 if not pk:
568 # empty or invalid part
569 continue
571 pk = pk.lower()
573 if pk[-1] == "*":
574 # key*=charset''value becomes key=value, where value is percent encoded
575 pk = pk[:-1]
576 match = _charset_value_re.match(pv)
578 if match:
579 # If there is a valid charset marker in the value, split it off.
580 encoding, pv = match.groups()
581 # This might be the empty string, handled next.
582 encoding = encoding.lower()
584 # No charset marker, or marker with empty charset value.
585 if not encoding:
586 encoding = continued_encoding
588 # A safe list of encodings. Modern clients should only send ASCII or UTF-8.
589 # This list will not be extended further. An invalid encoding will leave the
590 # value quoted.
591 if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
592 # Continuation parts don't require their own charset marker. This is
593 # looser than the RFC, it will persist across different keys and allows
594 # changing the charset during a continuation. But this implementation is
595 # much simpler than tracking the full state.
596 continued_encoding = encoding
597 # invalid bytes are replaced during unquoting
598 pv = unquote(pv, encoding=encoding)
600 # Remove quotes. At this point the value cannot be empty or a single quote.
601 if pv[0] == pv[-1] == '"':
602 # HTTP headers use slash, multipart form data uses percent
603 pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"')
605 match = _continuation_re.search(pk)
607 if match:
608 # key*0=a; key*1=b becomes key=ab
609 pk = pk[: match.start()]
610 options[pk] = options.get(pk, "") + pv
611 else:
612 options[pk] = pv
614 return value, options
617_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept")
620@t.overload
621def parse_accept_header(value: str | None) -> ds.Accept:
622 ...
625@t.overload
626def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept:
627 ...
630def parse_accept_header(
631 value: str | None, cls: type[_TAnyAccept] | None = None
632) -> _TAnyAccept:
633 """Parse an ``Accept`` header according to
634 `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__.
636 Returns an :class:`.Accept` instance, which can sort and inspect items based on
637 their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or
638 ``Accept-Language``, pass the appropriate :class:`.Accept` subclass.
640 :param value: The header value to parse.
641 :param cls: The :class:`.Accept` class to wrap the result in.
642 :return: An instance of ``cls``.
644 .. versionchanged:: 2.3
645 Parse according to RFC 9110. Items with invalid ``q`` values are skipped.
646 """
647 if cls is None:
648 cls = t.cast(t.Type[_TAnyAccept], ds.Accept)
650 if not value:
651 return cls(None)
653 result = []
655 for item in parse_list_header(value):
656 item, options = parse_options_header(item)
658 if "q" in options:
659 try:
660 # pop q, remaining options are reconstructed
661 q = _plain_float(options.pop("q"))
662 except ValueError:
663 # ignore an invalid q
664 continue
666 if q < 0 or q > 1:
667 # ignore an invalid q
668 continue
669 else:
670 q = 1
672 if options:
673 # reconstruct the media type with any options
674 item = dump_options_header(item, options)
676 result.append((item, q))
678 return cls(result)
681_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl")
682_t_cc_update = t.Optional[t.Callable[[_TAnyCC], None]]
685@t.overload
686def parse_cache_control_header(
687 value: str | None, on_update: _t_cc_update, cls: None = None
688) -> ds.RequestCacheControl:
689 ...
692@t.overload
693def parse_cache_control_header(
694 value: str | None, on_update: _t_cc_update, cls: type[_TAnyCC]
695) -> _TAnyCC:
696 ...
699def parse_cache_control_header(
700 value: str | None,
701 on_update: _t_cc_update = None,
702 cls: type[_TAnyCC] | None = None,
703) -> _TAnyCC:
704 """Parse a cache control header. The RFC differs between response and
705 request cache control, this method does not. It's your responsibility
706 to not use the wrong control statements.
708 .. versionadded:: 0.5
709 The `cls` was added. If not specified an immutable
710 :class:`~werkzeug.datastructures.RequestCacheControl` is returned.
712 :param value: a cache control header to be parsed.
713 :param on_update: an optional callable that is called every time a value
714 on the :class:`~werkzeug.datastructures.CacheControl`
715 object is changed.
716 :param cls: the class for the returned object. By default
717 :class:`~werkzeug.datastructures.RequestCacheControl` is used.
718 :return: a `cls` object.
719 """
720 if cls is None:
721 cls = t.cast(t.Type[_TAnyCC], ds.RequestCacheControl)
723 if not value:
724 return cls((), on_update)
726 return cls(parse_dict_header(value), on_update)
729_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy")
730_t_csp_update = t.Optional[t.Callable[[_TAnyCSP], None]]
733@t.overload
734def parse_csp_header(
735 value: str | None, on_update: _t_csp_update, cls: None = None
736) -> ds.ContentSecurityPolicy:
737 ...
740@t.overload
741def parse_csp_header(
742 value: str | None, on_update: _t_csp_update, cls: type[_TAnyCSP]
743) -> _TAnyCSP:
744 ...
747def parse_csp_header(
748 value: str | None,
749 on_update: _t_csp_update = None,
750 cls: type[_TAnyCSP] | None = None,
751) -> _TAnyCSP:
752 """Parse a Content Security Policy header.
754 .. versionadded:: 1.0.0
755 Support for Content Security Policy headers was added.
757 :param value: a csp header to be parsed.
758 :param on_update: an optional callable that is called every time a value
759 on the object is changed.
760 :param cls: the class for the returned object. By default
761 :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used.
762 :return: a `cls` object.
763 """
764 if cls is None:
765 cls = t.cast(t.Type[_TAnyCSP], ds.ContentSecurityPolicy)
767 if value is None:
768 return cls((), on_update)
770 items = []
772 for policy in value.split(";"):
773 policy = policy.strip()
775 # Ignore badly formatted policies (no space)
776 if " " in policy:
777 directive, value = policy.strip().split(" ", 1)
778 items.append((directive.strip(), value.strip()))
780 return cls(items, on_update)
783def parse_set_header(
784 value: str | None,
785 on_update: t.Callable[[ds.HeaderSet], None] | None = None,
786) -> ds.HeaderSet:
787 """Parse a set-like header and return a
788 :class:`~werkzeug.datastructures.HeaderSet` object:
790 >>> hs = parse_set_header('token, "quoted value"')
792 The return value is an object that treats the items case-insensitively
793 and keeps the order of the items:
795 >>> 'TOKEN' in hs
796 True
797 >>> hs.index('quoted value')
798 1
799 >>> hs
800 HeaderSet(['token', 'quoted value'])
802 To create a header from the :class:`HeaderSet` again, use the
803 :func:`dump_header` function.
805 :param value: a set header to be parsed.
806 :param on_update: an optional callable that is called every time a
807 value on the :class:`~werkzeug.datastructures.HeaderSet`
808 object is changed.
809 :return: a :class:`~werkzeug.datastructures.HeaderSet`
810 """
811 if not value:
812 return ds.HeaderSet(None, on_update)
813 return ds.HeaderSet(parse_list_header(value), on_update)
816def parse_authorization_header(
817 value: str | None,
818) -> ds.Authorization | None:
819 """Parse an HTTP basic/digest authorization header transmitted by the web
820 browser. The return value is either `None` if the header was invalid or
821 not given, otherwise an :class:`~werkzeug.datastructures.Authorization`
822 object.
824 :param value: the authorization header to parse.
825 :return: a :class:`~werkzeug.datastructures.Authorization` object or `None`.
827 .. deprecated:: 2.3
828 Will be removed in Werkzeug 3.0. Use :meth:`.Authorization.from_header` instead.
829 """
830 from .datastructures import Authorization
832 warnings.warn(
833 "'parse_authorization_header' is deprecated and will be removed in Werkzeug"
834 " 2.4. Use 'Authorization.from_header' instead.",
835 DeprecationWarning,
836 stacklevel=2,
837 )
838 return Authorization.from_header(value)
841def parse_www_authenticate_header(
842 value: str | None,
843 on_update: t.Callable[[ds.WWWAuthenticate], None] | None = None,
844) -> ds.WWWAuthenticate:
845 """Parse an HTTP WWW-Authenticate header into a
846 :class:`~werkzeug.datastructures.WWWAuthenticate` object.
848 :param value: a WWW-Authenticate header to parse.
849 :param on_update: an optional callable that is called every time a value
850 on the :class:`~werkzeug.datastructures.WWWAuthenticate`
851 object is changed.
852 :return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object.
854 .. deprecated:: 2.3
855 Will be removed in Werkzeug 3.0. Use :meth:`.WWWAuthenticate.from_header`
856 instead.
857 """
858 from .datastructures.auth import WWWAuthenticate
860 warnings.warn(
861 "'parse_www_authenticate_header' is deprecated and will be removed in Werkzeug"
862 " 2.4. Use 'WWWAuthenticate.from_header' instead.",
863 DeprecationWarning,
864 stacklevel=2,
865 )
866 rv = WWWAuthenticate.from_header(value)
868 if rv is None:
869 rv = WWWAuthenticate("basic")
871 rv._on_update = on_update
872 return rv
875def parse_if_range_header(value: str | None) -> ds.IfRange:
876 """Parses an if-range header which can be an etag or a date. Returns
877 a :class:`~werkzeug.datastructures.IfRange` object.
879 .. versionchanged:: 2.0
880 If the value represents a datetime, it is timezone-aware.
882 .. versionadded:: 0.7
883 """
884 if not value:
885 return ds.IfRange()
886 date = parse_date(value)
887 if date is not None:
888 return ds.IfRange(date=date)
889 # drop weakness information
890 return ds.IfRange(unquote_etag(value)[0])
893def parse_range_header(
894 value: str | None, make_inclusive: bool = True
895) -> ds.Range | None:
896 """Parses a range header into a :class:`~werkzeug.datastructures.Range`
897 object. If the header is missing or malformed `None` is returned.
898 `ranges` is a list of ``(start, stop)`` tuples where the ranges are
899 non-inclusive.
901 .. versionadded:: 0.7
902 """
903 if not value or "=" not in value:
904 return None
906 ranges = []
907 last_end = 0
908 units, rng = value.split("=", 1)
909 units = units.strip().lower()
911 for item in rng.split(","):
912 item = item.strip()
913 if "-" not in item:
914 return None
915 if item.startswith("-"):
916 if last_end < 0:
917 return None
918 try:
919 begin = _plain_int(item)
920 except ValueError:
921 return None
922 end = None
923 last_end = -1
924 elif "-" in item:
925 begin_str, end_str = item.split("-", 1)
926 begin_str = begin_str.strip()
927 end_str = end_str.strip()
929 try:
930 begin = _plain_int(begin_str)
931 except ValueError:
932 return None
934 if begin < last_end or last_end < 0:
935 return None
936 if end_str:
937 try:
938 end = _plain_int(end_str) + 1
939 except ValueError:
940 return None
942 if begin >= end:
943 return None
944 else:
945 end = None
946 last_end = end if end is not None else -1
947 ranges.append((begin, end))
949 return ds.Range(units, ranges)
952def parse_content_range_header(
953 value: str | None,
954 on_update: t.Callable[[ds.ContentRange], None] | None = None,
955) -> ds.ContentRange | None:
956 """Parses a range header into a
957 :class:`~werkzeug.datastructures.ContentRange` object or `None` if
958 parsing is not possible.
960 .. versionadded:: 0.7
962 :param value: a content range header to be parsed.
963 :param on_update: an optional callable that is called every time a value
964 on the :class:`~werkzeug.datastructures.ContentRange`
965 object is changed.
966 """
967 if value is None:
968 return None
969 try:
970 units, rangedef = (value or "").strip().split(None, 1)
971 except ValueError:
972 return None
974 if "/" not in rangedef:
975 return None
976 rng, length_str = rangedef.split("/", 1)
977 if length_str == "*":
978 length = None
979 else:
980 try:
981 length = _plain_int(length_str)
982 except ValueError:
983 return None
985 if rng == "*":
986 if not is_byte_range_valid(None, None, length):
987 return None
989 return ds.ContentRange(units, None, None, length, on_update=on_update)
990 elif "-" not in rng:
991 return None
993 start_str, stop_str = rng.split("-", 1)
994 try:
995 start = _plain_int(start_str)
996 stop = _plain_int(stop_str) + 1
997 except ValueError:
998 return None
1000 if is_byte_range_valid(start, stop, length):
1001 return ds.ContentRange(units, start, stop, length, on_update=on_update)
1003 return None
1006def quote_etag(etag: str, weak: bool = False) -> str:
1007 """Quote an etag.
1009 :param etag: the etag to quote.
1010 :param weak: set to `True` to tag it "weak".
1011 """
1012 if '"' in etag:
1013 raise ValueError("invalid etag")
1014 etag = f'"{etag}"'
1015 if weak:
1016 etag = f"W/{etag}"
1017 return etag
1020def unquote_etag(
1021 etag: str | None,
1022) -> tuple[str, bool] | tuple[None, None]:
1023 """Unquote a single etag:
1025 >>> unquote_etag('W/"bar"')
1026 ('bar', True)
1027 >>> unquote_etag('"bar"')
1028 ('bar', False)
1030 :param etag: the etag identifier to unquote.
1031 :return: a ``(etag, weak)`` tuple.
1032 """
1033 if not etag:
1034 return None, None
1035 etag = etag.strip()
1036 weak = False
1037 if etag.startswith(("W/", "w/")):
1038 weak = True
1039 etag = etag[2:]
1040 if etag[:1] == etag[-1:] == '"':
1041 etag = etag[1:-1]
1042 return etag, weak
1045def parse_etags(value: str | None) -> ds.ETags:
1046 """Parse an etag header.
1048 :param value: the tag header to parse
1049 :return: an :class:`~werkzeug.datastructures.ETags` object.
1050 """
1051 if not value:
1052 return ds.ETags()
1053 strong = []
1054 weak = []
1055 end = len(value)
1056 pos = 0
1057 while pos < end:
1058 match = _etag_re.match(value, pos)
1059 if match is None:
1060 break
1061 is_weak, quoted, raw = match.groups()
1062 if raw == "*":
1063 return ds.ETags(star_tag=True)
1064 elif quoted:
1065 raw = quoted
1066 if is_weak:
1067 weak.append(raw)
1068 else:
1069 strong.append(raw)
1070 pos = match.end()
1071 return ds.ETags(strong, weak)
1074def generate_etag(data: bytes) -> str:
1075 """Generate an etag for some data.
1077 .. versionchanged:: 2.0
1078 Use SHA-1. MD5 may not be available in some environments.
1079 """
1080 return sha1(data).hexdigest()
1083def parse_date(value: str | None) -> datetime | None:
1084 """Parse an :rfc:`2822` date into a timezone-aware
1085 :class:`datetime.datetime` object, or ``None`` if parsing fails.
1087 This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It
1088 returns ``None`` if parsing fails instead of raising an exception,
1089 and always returns a timezone-aware datetime object. If the string
1090 doesn't have timezone information, it is assumed to be UTC.
1092 :param value: A string with a supported date format.
1094 .. versionchanged:: 2.0
1095 Return a timezone-aware datetime object. Use
1096 ``email.utils.parsedate_to_datetime``.
1097 """
1098 if value is None:
1099 return None
1101 try:
1102 dt = email.utils.parsedate_to_datetime(value)
1103 except (TypeError, ValueError):
1104 return None
1106 if dt.tzinfo is None:
1107 return dt.replace(tzinfo=timezone.utc)
1109 return dt
1112def http_date(
1113 timestamp: datetime | date | int | float | struct_time | None = None,
1114) -> str:
1115 """Format a datetime object or timestamp into an :rfc:`2822` date
1116 string.
1118 This is a wrapper for :func:`email.utils.format_datetime`. It
1119 assumes naive datetime objects are in UTC instead of raising an
1120 exception.
1122 :param timestamp: The datetime or timestamp to format. Defaults to
1123 the current time.
1125 .. versionchanged:: 2.0
1126 Use ``email.utils.format_datetime``. Accept ``date`` objects.
1127 """
1128 if isinstance(timestamp, date):
1129 if not isinstance(timestamp, datetime):
1130 # Assume plain date is midnight UTC.
1131 timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc)
1132 else:
1133 # Ensure datetime is timezone-aware.
1134 timestamp = _dt_as_utc(timestamp)
1136 return email.utils.format_datetime(timestamp, usegmt=True)
1138 if isinstance(timestamp, struct_time):
1139 timestamp = mktime(timestamp)
1141 return email.utils.formatdate(timestamp, usegmt=True)
1144def parse_age(value: str | None = None) -> timedelta | None:
1145 """Parses a base-10 integer count of seconds into a timedelta.
1147 If parsing fails, the return value is `None`.
1149 :param value: a string consisting of an integer represented in base-10
1150 :return: a :class:`datetime.timedelta` object or `None`.
1151 """
1152 if not value:
1153 return None
1154 try:
1155 seconds = int(value)
1156 except ValueError:
1157 return None
1158 if seconds < 0:
1159 return None
1160 try:
1161 return timedelta(seconds=seconds)
1162 except OverflowError:
1163 return None
1166def dump_age(age: timedelta | int | None = None) -> str | None:
1167 """Formats the duration as a base-10 integer.
1169 :param age: should be an integer number of seconds,
1170 a :class:`datetime.timedelta` object, or,
1171 if the age is unknown, `None` (default).
1172 """
1173 if age is None:
1174 return None
1175 if isinstance(age, timedelta):
1176 age = int(age.total_seconds())
1177 else:
1178 age = int(age)
1180 if age < 0:
1181 raise ValueError("age cannot be negative")
1183 return str(age)
1186def is_resource_modified(
1187 environ: WSGIEnvironment,
1188 etag: str | None = None,
1189 data: bytes | None = None,
1190 last_modified: datetime | str | None = None,
1191 ignore_if_range: bool = True,
1192) -> bool:
1193 """Convenience method for conditional requests.
1195 :param environ: the WSGI environment of the request to be checked.
1196 :param etag: the etag for the response for comparison.
1197 :param data: or alternatively the data of the response to automatically
1198 generate an etag using :func:`generate_etag`.
1199 :param last_modified: an optional date of the last modification.
1200 :param ignore_if_range: If `False`, `If-Range` header will be taken into
1201 account.
1202 :return: `True` if the resource was modified, otherwise `False`.
1204 .. versionchanged:: 2.0
1205 SHA-1 is used to generate an etag value for the data. MD5 may
1206 not be available in some environments.
1208 .. versionchanged:: 1.0.0
1209 The check is run for methods other than ``GET`` and ``HEAD``.
1210 """
1211 return _sansio_http.is_resource_modified(
1212 http_range=environ.get("HTTP_RANGE"),
1213 http_if_range=environ.get("HTTP_IF_RANGE"),
1214 http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),
1215 http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),
1216 http_if_match=environ.get("HTTP_IF_MATCH"),
1217 etag=etag,
1218 data=data,
1219 last_modified=last_modified,
1220 ignore_if_range=ignore_if_range,
1221 )
1224def remove_entity_headers(
1225 headers: ds.Headers | list[tuple[str, str]],
1226 allowed: t.Iterable[str] = ("expires", "content-location"),
1227) -> None:
1228 """Remove all entity headers from a list or :class:`Headers` object. This
1229 operation works in-place. `Expires` and `Content-Location` headers are
1230 by default not removed. The reason for this is :rfc:`2616` section
1231 10.3.5 which specifies some entity headers that should be sent.
1233 .. versionchanged:: 0.5
1234 added `allowed` parameter.
1236 :param headers: a list or :class:`Headers` object.
1237 :param allowed: a list of headers that should still be allowed even though
1238 they are entity headers.
1239 """
1240 allowed = {x.lower() for x in allowed}
1241 headers[:] = [
1242 (key, value)
1243 for key, value in headers
1244 if not is_entity_header(key) or key.lower() in allowed
1245 ]
1248def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None:
1249 """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or
1250 :class:`Headers` object. This operation works in-place.
1252 .. versionadded:: 0.5
1254 :param headers: a list or :class:`Headers` object.
1255 """
1256 headers[:] = [
1257 (key, value) for key, value in headers if not is_hop_by_hop_header(key)
1258 ]
1261def is_entity_header(header: str) -> bool:
1262 """Check if a header is an entity header.
1264 .. versionadded:: 0.5
1266 :param header: the header to test.
1267 :return: `True` if it's an entity header, `False` otherwise.
1268 """
1269 return header.lower() in _entity_headers
1272def is_hop_by_hop_header(header: str) -> bool:
1273 """Check if a header is an HTTP/1.1 "Hop-by-Hop" header.
1275 .. versionadded:: 0.5
1277 :param header: the header to test.
1278 :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise.
1279 """
1280 return header.lower() in _hop_by_hop_headers
1283def parse_cookie(
1284 header: WSGIEnvironment | str | None,
1285 charset: str | None = None,
1286 errors: str | None = None,
1287 cls: type[ds.MultiDict] | None = None,
1288) -> ds.MultiDict[str, str]:
1289 """Parse a cookie from a string or WSGI environ.
1291 The same key can be provided multiple times, the values are stored
1292 in-order. The default :class:`MultiDict` will have the first value
1293 first, and all values can be retrieved with
1294 :meth:`MultiDict.getlist`.
1296 :param header: The cookie header as a string, or a WSGI environ dict
1297 with a ``HTTP_COOKIE`` key.
1298 :param cls: A dict-like class to store the parsed cookies in.
1299 Defaults to :class:`MultiDict`.
1301 .. versionchanged:: 2.3
1302 Passing bytes, and the ``charset`` and ``errors`` parameters, are deprecated and
1303 will be removed in Werkzeug 3.0.
1305 .. versionchanged:: 1.0
1306 Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.
1308 .. versionchanged:: 0.5
1309 Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls``
1310 parameter was added.
1311 """
1312 if isinstance(header, dict):
1313 cookie = header.get("HTTP_COOKIE")
1314 elif isinstance(header, bytes):
1315 warnings.warn(
1316 "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.",
1317 DeprecationWarning,
1318 stacklevel=2,
1319 )
1320 cookie = header.decode()
1321 else:
1322 cookie = header
1324 if cookie:
1325 cookie = cookie.encode("latin1").decode()
1327 return _sansio_http.parse_cookie(
1328 cookie=cookie, charset=charset, errors=errors, cls=cls
1329 )
1332_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
1333_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A)
1334_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"}
1335_cookie_slash_map.update(
1336 (v.to_bytes(1, "big"), b"\\%03o" % v)
1337 for v in [*range(0x20), *b",;", *range(0x7F, 256)]
1338)
1341def dump_cookie(
1342 key: str,
1343 value: str = "",
1344 max_age: timedelta | int | None = None,
1345 expires: str | datetime | int | float | None = None,
1346 path: str | None = "/",
1347 domain: str | None = None,
1348 secure: bool = False,
1349 httponly: bool = False,
1350 charset: str | None = None,
1351 sync_expires: bool = True,
1352 max_size: int = 4093,
1353 samesite: str | None = None,
1354) -> str:
1355 """Create a Set-Cookie header without the ``Set-Cookie`` prefix.
1357 The return value is usually restricted to ascii as the vast majority
1358 of values are properly escaped, but that is no guarantee. It's
1359 tunneled through latin1 as required by :pep:`3333`.
1361 The return value is not ASCII safe if the key contains unicode
1362 characters. This is technically against the specification but
1363 happens in the wild. It's strongly recommended to not use
1364 non-ASCII values for the keys.
1366 :param max_age: should be a number of seconds, or `None` (default) if
1367 the cookie should last only as long as the client's
1368 browser session. Additionally `timedelta` objects
1369 are accepted, too.
1370 :param expires: should be a `datetime` object or unix timestamp.
1371 :param path: limits the cookie to a given path, per default it will
1372 span the whole domain.
1373 :param domain: Use this if you want to set a cross-domain cookie. For
1374 example, ``domain=".example.com"`` will set a cookie
1375 that is readable by the domain ``www.example.com``,
1376 ``foo.example.com`` etc. Otherwise, a cookie will only
1377 be readable by the domain that set it.
1378 :param secure: The cookie will only be available via HTTPS
1379 :param httponly: disallow JavaScript to access the cookie. This is an
1380 extension to the cookie standard and probably not
1381 supported by all browsers.
1382 :param charset: the encoding for string values.
1383 :param sync_expires: automatically set expires if max_age is defined
1384 but expires not.
1385 :param max_size: Warn if the final header value exceeds this size. The
1386 default, 4093, should be safely `supported by most browsers
1387 <cookie_>`_. Set to 0 to disable this check.
1388 :param samesite: Limits the scope of the cookie such that it will
1389 only be attached to requests if those requests are same-site.
1391 .. _`cookie`: http://browsercookielimits.squawky.net/
1393 .. versionchanged:: 2.3.3
1394 The ``path`` parameter is ``/`` by default.
1396 .. versionchanged:: 2.3.1
1397 The value allows more characters without quoting.
1399 .. versionchanged:: 2.3
1400 ``localhost`` and other names without a dot are allowed for the domain. A
1401 leading dot is ignored.
1403 .. versionchanged:: 2.3
1404 The ``path`` parameter is ``None`` by default.
1406 .. versionchanged:: 2.3
1407 Passing bytes, and the ``charset`` parameter, are deprecated and will be removed
1408 in Werkzeug 3.0.
1410 .. versionchanged:: 1.0.0
1411 The string ``'None'`` is accepted for ``samesite``.
1412 """
1413 if charset is not None:
1414 warnings.warn(
1415 "The 'charset' parameter is deprecated and will be removed"
1416 " in Werkzeug 3.0.",
1417 DeprecationWarning,
1418 stacklevel=2,
1419 )
1420 else:
1421 charset = "utf-8"
1423 if isinstance(key, bytes):
1424 warnings.warn(
1425 "The 'key' parameter must be a string. Bytes are deprecated"
1426 " and will not be supported in Werkzeug 3.0.",
1427 DeprecationWarning,
1428 stacklevel=2,
1429 )
1430 key = key.decode()
1432 if isinstance(value, bytes):
1433 warnings.warn(
1434 "The 'value' parameter must be a string. Bytes are"
1435 " deprecated and will not be supported in Werkzeug 3.0.",
1436 DeprecationWarning,
1437 stacklevel=2,
1438 )
1439 value = value.decode()
1441 if path is not None:
1442 # safe = https://url.spec.whatwg.org/#url-path-segment-string
1443 # as well as percent for things that are already quoted
1444 # excluding semicolon since it's part of the header syntax
1445 path = quote(path, safe="%!$&'()*+,/:=@", encoding=charset)
1447 if domain:
1448 domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii")
1450 if isinstance(max_age, timedelta):
1451 max_age = int(max_age.total_seconds())
1453 if expires is not None:
1454 if not isinstance(expires, str):
1455 expires = http_date(expires)
1456 elif max_age is not None and sync_expires:
1457 expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age)
1459 if samesite is not None:
1460 samesite = samesite.title()
1462 if samesite not in {"Strict", "Lax", "None"}:
1463 raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
1465 # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
1466 # three octal digits, which matches http.cookies, although the RFC suggests base64.
1467 if not _cookie_no_quote_re.fullmatch(value):
1468 # Work with bytes here, since a UTF-8 character could be multiple bytes.
1469 value = _cookie_slash_re.sub(
1470 lambda m: _cookie_slash_map[m.group()], value.encode(charset)
1471 ).decode("ascii")
1472 value = f'"{value}"'
1474 # Send a non-ASCII key as mojibake. Everything else should already be ASCII.
1475 # TODO Remove encoding dance, it seems like clients accept UTF-8 keys
1476 buf = [f"{key.encode().decode('latin1')}={value}"]
1478 for k, v in (
1479 ("Domain", domain),
1480 ("Expires", expires),
1481 ("Max-Age", max_age),
1482 ("Secure", secure),
1483 ("HttpOnly", httponly),
1484 ("Path", path),
1485 ("SameSite", samesite),
1486 ):
1487 if v is None or v is False:
1488 continue
1490 if v is True:
1491 buf.append(k)
1492 continue
1494 buf.append(f"{k}={v}")
1496 rv = "; ".join(buf)
1498 # Warn if the final value of the cookie is larger than the limit. If the cookie is
1499 # too large, then it may be silently ignored by the browser, which can be quite hard
1500 # to debug.
1501 cookie_size = len(rv)
1503 if max_size and cookie_size > max_size:
1504 value_size = len(value)
1505 warnings.warn(
1506 f"The '{key}' cookie is too large: the value was {value_size} bytes but the"
1507 f" header required {cookie_size - value_size} extra bytes. The final size"
1508 f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may"
1509 " silently ignore cookies larger than this.",
1510 stacklevel=2,
1511 )
1513 return rv
1516def is_byte_range_valid(
1517 start: int | None, stop: int | None, length: int | None
1518) -> bool:
1519 """Checks if a given byte content range is valid for the given length.
1521 .. versionadded:: 0.7
1522 """
1523 if (start is None) != (stop is None):
1524 return False
1525 elif start is None:
1526 return length is None or length >= 0
1527 elif length is None:
1528 return 0 <= start < stop # type: ignore
1529 elif start >= stop: # type: ignore
1530 return False
1531 return 0 <= start < length
1534# circular dependencies
1535from . import datastructures as ds
1536from .sansio import http as _sansio_http