Skip to content

Commit

Permalink
extractring http functions to sansio
Browse files Browse the repository at this point in the history
  • Loading branch information
rrahkola authored and pgjones committed Jul 22, 2022
1 parent acb1b04 commit b42d1a4
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 75 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Version 2.2.0
- The debugger shows enhanced error locations in tracebacks in Python
3.11. :issue:`2407`

- Extracted is_resource_modified and parse_cookie from http.py
to sansio/http.py. :issue:`2408`

Version 2.1.2
-------------
Expand Down
94 changes: 20 additions & 74 deletions src/werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
from urllib.parse import unquote_to_bytes as _unquote
from urllib.request import parse_http_list as _parse_list_header

from ._internal import _cookie_parse_impl
from ._internal import _cookie_quote
from ._internal import _dt_as_utc
from ._internal import _make_cookie_domain
from ._internal import _to_bytes
from ._internal import _to_str
from ._internal import _wsgi_decoding_dance
from werkzeug._internal import _dt_as_utc

if t.TYPE_CHECKING:
import typing_extensions as te
Expand Down Expand Up @@ -1050,57 +1049,17 @@ def is_resource_modified(
.. versionchanged:: 1.0.0
The check is run for methods other than ``GET`` and ``HEAD``.
"""
if etag is None and data is not None:
etag = generate_etag(data)
elif data is not None:
raise TypeError("both data and etag given")

unmodified = False
if isinstance(last_modified, str):
last_modified = parse_date(last_modified)

# HTTP doesn't use microsecond, remove it to avoid false positive
# comparisons. Mark naive datetimes as UTC.
if last_modified is not None:
last_modified = _dt_as_utc(last_modified.replace(microsecond=0))

if_range = None
if not ignore_if_range and "HTTP_RANGE" in environ:
# https://tools.ietf.org/html/rfc7233#section-3.2
# A server MUST ignore an If-Range header field received in a request
# that does not contain a Range header field.
if_range = parse_if_range_header(environ.get("HTTP_IF_RANGE"))

if if_range is not None and if_range.date is not None:
modified_since: t.Optional[datetime] = if_range.date
else:
modified_since = parse_date(environ.get("HTTP_IF_MODIFIED_SINCE"))

if modified_since and last_modified and last_modified <= modified_since:
unmodified = True

if etag:
etag, _ = unquote_etag(etag)
etag = t.cast(str, etag)

if if_range is not None and if_range.etag is not None:
unmodified = parse_etags(if_range.etag).contains(etag)
else:
if_none_match = parse_etags(environ.get("HTTP_IF_NONE_MATCH"))
if if_none_match:
# https://tools.ietf.org/html/rfc7232#section-3.2
# "A recipient MUST use the weak comparison function when comparing
# entity-tags for If-None-Match"
unmodified = if_none_match.contains_weak(etag)

# https://tools.ietf.org/html/rfc7232#section-3.1
# "Origin server MUST use the strong comparison function when
# comparing entity-tags for If-Match"
if_match = parse_etags(environ.get("HTTP_IF_MATCH"))
if if_match:
unmodified = not if_match.is_strong(etag)

return not unmodified
return _sansio_http.is_resource_modified(
http_range=environ.get("HTTP_RANGE"),
http_if_range=environ.get("HTTP_IF_RANGE"),
http_if_modified_since=environ.get("HTTP_IF_MODIFIED_SINCE"),
http_if_none_match=environ.get("HTTP_IF_NONE_MATCH"),
http_if_match=environ.get("HTTP_IF_MATCH"),
etag=etag,
data=data,
last_modified=last_modified,
ignore_if_range=ignore_if_range,
)


def remove_entity_headers(
Expand Down Expand Up @@ -1193,29 +1152,15 @@ def parse_cookie(
The ``cls`` parameter was added.
"""
if isinstance(header, dict):
header = header.get("HTTP_COOKIE", "")
cookie = header.get("HTTP_COOKIE", "")
elif header is None:
header = ""

# PEP 3333 sends headers through the environ as latin1 decoded
# strings. Encode strings back to bytes for parsing.
if isinstance(header, str):
header = header.encode("latin1", "replace")

if cls is None:
cls = ds.MultiDict

def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]:
for key, val in _cookie_parse_impl(header): # type: ignore
key_str = _to_str(key, charset, errors, allow_none_charset=True)

if not key_str:
continue

val_str = _to_str(val, charset, errors, allow_none_charset=True)
yield key_str, val_str
cookie = ""
else:
cookie = header

return cls(_parse_pairs())
return _sansio_http.parse_cookie(
cookie=cookie, charset=charset, errors=errors, cls=cls
)


def dump_cookie(
Expand Down Expand Up @@ -1372,3 +1317,4 @@ def is_byte_range_valid(

# circular dependencies
from . import datastructures as ds
from .sansio import http as _sansio_http
156 changes: 156 additions & 0 deletions src/werkzeug/sansio/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import re
import typing as t
from datetime import datetime

from .._internal import _cookie_parse_impl
from .._internal import _dt_as_utc
from .._internal import _to_str
from ..http import generate_etag
from ..http import parse_date
from ..http import parse_etags
from ..http import parse_if_range_header
from ..http import unquote_etag

_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')


def is_resource_modified(
http_range: t.Optional[str] = None,
http_if_range: t.Optional[str] = None,
http_if_modified_since: t.Optional[str] = None,
http_if_none_match: t.Optional[str] = None,
http_if_match: t.Optional[str] = None,
etag: t.Optional[str] = None,
data: t.Optional[bytes] = None,
last_modified: t.Optional[t.Union[datetime, str]] = None,
ignore_if_range: bool = True,
) -> bool:
"""Convenience method for conditional requests.
:param http_range: Range HTTP header
:param http_if_range: If-Range HTTP header
:param http_if_modified_since: If-Modified-Since HTTP header
:param http_if_none_match: If-None-Match HTTP header
:param http_if_match: If-Match HTTP header
:param etag: the etag for the response for comparison.
:param data: or alternatively the data of the response to automatically
generate an etag using :func:`generate_etag`.
:param last_modified: an optional date of the last modification.
:param ignore_if_range: If `False`, `If-Range` header will be taken into
account.
:return: `True` if the resource was modified, otherwise `False`.
.. versionchanged:: 2.2
Made arguments explicit to support ASGI.
.. versionchanged:: 2.0
SHA-1 is used to generate an etag value for the data. MD5 may
not be available in some environments.
.. versionchanged:: 1.0.0
The check is run for methods other than ``GET`` and ``HEAD``.
"""
if etag is None and data is not None:
etag = generate_etag(data)
elif data is not None:
raise TypeError("both data and etag given")

unmodified = False
if isinstance(last_modified, str):
last_modified = parse_date(last_modified)

# HTTP doesn't use microsecond, remove it to avoid false positive
# comparisons. Mark naive datetimes as UTC.
if last_modified is not None:
last_modified = _dt_as_utc(last_modified.replace(microsecond=0))

if_range = None
if not ignore_if_range and http_range is not None:
# https://tools.ietf.org/html/rfc7233#section-3.2
# A server MUST ignore an If-Range header field received in a request
# that does not contain a Range header field.
if_range = parse_if_range_header(http_if_range)

if if_range is not None and if_range.date is not None:
modified_since: t.Optional[datetime] = if_range.date
else:
modified_since = parse_date(http_if_modified_since)

if modified_since and last_modified and last_modified <= modified_since:
unmodified = True

if etag:
etag, _ = unquote_etag(etag)
etag = t.cast(str, etag)

if if_range is not None and if_range.etag is not None:
unmodified = parse_etags(if_range.etag).contains(etag)
else:
if_none_match = parse_etags(http_if_none_match)
if if_none_match:
# https://tools.ietf.org/html/rfc7232#section-3.2
# "A recipient MUST use the weak comparison function when comparing
# entity-tags for If-None-Match"
unmodified = if_none_match.contains_weak(etag)

# https://tools.ietf.org/html/rfc7232#section-3.1
# "Origin server MUST use the strong comparison function when
# comparing entity-tags for If-Match"
if_match = parse_etags(http_if_match)
if if_match:
unmodified = not if_match.is_strong(etag)

return not unmodified


def parse_cookie(
cookie: t.Union[bytes, str, None] = "",
charset: str = "utf-8",
errors: str = "replace",
cls: t.Optional[t.Type["ds.MultiDict"]] = None,
) -> "ds.MultiDict[str, str]":
"""Parse a cookie from a string.
The same key can be provided multiple times, the values are stored
in-order. The default :class:`MultiDict` will have the first value
first, and all values can be retrieved with
:meth:`MultiDict.getlist`.
:param cookie: The cookie header as a string.
:param charset: The charset for the cookie values.
:param errors: The error behavior for the charset decoding.
:param cls: A dict-like class to store the parsed cookies in.
Defaults to :class:`MultiDict`.
.. versionchanged:: 2.2
Uses explicit cookie string argument
.. versionchanged:: 1.0.0
Returns a :class:`MultiDict` instead of a
``TypeConversionDict``.
.. versionchanged:: 0.5
Returns a :class:`TypeConversionDict` instead of a regular dict.
The ``cls`` parameter was added.
"""
# PEP 3333 sends headers through the environ as latin1 decoded
# strings. Encode strings back to bytes for parsing.
if isinstance(cookie, str):
cookie = cookie.encode("latin1", "replace")

if cls is None:
cls = ds.MultiDict

def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]:
for key, val in _cookie_parse_impl(cookie): # type: ignore
key_str = _to_str(key, charset, errors, allow_none_charset=True)

if not key_str:
continue

val_str = _to_str(val, charset, errors, allow_none_charset=True)
yield key_str, val_str

return cls(_parse_pairs())


# circular dependencies
from .. import datastructures as ds
2 changes: 1 addition & 1 deletion src/werkzeug/sansio/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from ..http import parse_accept_header
from ..http import parse_authorization_header
from ..http import parse_cache_control_header
from ..http import parse_cookie
from ..http import parse_date
from ..http import parse_etags
from ..http import parse_if_range_header
Expand All @@ -31,6 +30,7 @@
from ..user_agent import UserAgent
from ..utils import cached_property
from ..utils import header_property
from .http import parse_cookie
from .utils import get_current_url
from .utils import get_host

Expand Down

0 comments on commit b42d1a4

Please sign in to comment.