Skip to content

Commit

Permalink
Update ETag functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
greshilov committed Dec 6, 2020
1 parent 1850e62 commit e6e979a
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 20 deletions.
12 changes: 10 additions & 2 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,9 +883,10 @@ def populate_with_cookies(

# https://tools.ietf.org/html/rfc7232#section-2.3
_ETAGC = r"[!#-}\x80-\xff]+"
_QUOTED_ETAG = fr'(W/)?("{_ETAGC}")'
_ETAGC_RE = re.compile(_ETAGC)
_QUOTED_ETAG = fr'(W/)?"({_ETAGC})"'
QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG)
LIST_QUOTED_ETAG_RE = re.compile(fr"({_QUOTED_ETAG})(?:\s*,\s*|$)")
LIST_QUOTED_ETAG_RE = re.compile(fr"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)")

ETAG_ANY = "*"

Expand All @@ -894,3 +895,10 @@ def populate_with_cookies(
class ETag:
value: str
is_weak: bool = False


def validate_etag_value(value: str) -> None:
if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value):
raise ValueError(
f"Value {value!r} is not a valid etag. Maybe it contains '\"'?"
)
9 changes: 7 additions & 2 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,10 +507,15 @@ def _etag_values(etag_header: str) -> Iterator[ETag]:
)
else:
for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
is_weak, quoted_value = match.group(2), match.group(3)
is_weak, value, garbage = match.group(2, 3, 4)
# Any symbol captured by 4th group means
# that the following sequence is invalid.
if garbage:
break

yield ETag(
is_weak=bool(is_weak),
value=quoted_value[1:-1],
value=value,
)

@classmethod
Expand Down
19 changes: 11 additions & 8 deletions aiohttp/web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
populate_with_cookies,
rfc822_formatted_time,
sentinel,
validate_etag_value,
)
from .http import RESPONSES, SERVER_SOFTWARE, HttpVersion10, HttpVersion11
from .payload import Payload
Expand Down Expand Up @@ -278,31 +279,33 @@ def etag(self) -> Optional[ETag]:
return None
elif quoted_value == ETAG_ANY:
return ETag(value=ETAG_ANY)
match = QUOTED_ETAG_RE.match(quoted_value)
match = QUOTED_ETAG_RE.fullmatch(quoted_value)
if not match:
return None
is_weak, value = match.group(1), match.group(2)
is_weak, value = match.group(1, 2)
return ETag(
is_weak=bool(is_weak),
value=value[1:-1],
value=value,
)

@etag.setter
def etag(self, value: Optional[Union[ETag, str]]) -> None:
if (isinstance(value, str) and value == ETAG_ANY) or (
if value is None:
self._headers.pop(hdrs.ETAG, None)
elif (isinstance(value, str) and value == ETAG_ANY) or (
isinstance(value, ETag) and value.value == ETAG_ANY
):
self._headers[hdrs.ETAG] = ETAG_ANY
elif isinstance(value, str):
validate_etag_value(value)
self._headers[hdrs.ETAG] = f'"{value}"'
elif isinstance(value, ETag) and isinstance(value.value, str):
hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value}"'
validate_etag_value(value.value)
hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"'
self._headers[hdrs.ETAG] = hdr_value
elif value is None:
self._headers.pop(hdrs.ETAG, None)
else:
raise ValueError(
f"Unsupported etag type: {type(value)!r}. "
f"Unsupported etag type: {type(value)}. "
f"etag must be str, ETag or None"
)

Expand Down
11 changes: 9 additions & 2 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,17 +834,24 @@ async def invalid_handler_1(request):
"header_val,expected",
[
pytest.param(
'"67ab43", W/"54ed21", "7892dd"',
'"67ab43", W/"54ed21", "7892,dd"',
(
ETag(is_weak=False, value="67ab43"),
ETag(is_weak=True, value="54ed21"),
ETag(is_weak=False, value="7892dd"),
ETag(is_weak=False, value="7892,dd"),
),
),
pytest.param(
'"bfc1ef-5b2c2730249c88ca92d82d"',
(ETag(is_weak=False, value="bfc1ef-5b2c2730249c88ca92d82d"),),
),
pytest.param(
'"valid-tag", "also-valid-tag",somegarbage"last-tag"',
(
ETag(is_weak=False, value="valid-tag"),
ETag(is_weak=False, value="also-valid-tag"),
),
),
pytest.param(
"*",
(ETag(is_weak=False, value="*"),),
Expand Down
27 changes: 21 additions & 6 deletions tests/test_web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,18 @@ def test_etag_string() -> None:
assert resp.headers[hdrs.ETAG] == f'"{value}"'


def test_etag_class() -> None:
@pytest.mark.parametrize(
"etag,expected_header",
(
(ETag(value="0123-weak-kotik", is_weak=True), 'W/"0123-weak-kotik"'),
(ETag(value="0123-strong-kotik", is_weak=False), '"0123-strong-kotik"'),
),
)
def test_etag_class(etag, expected_header) -> None:
resp = StreamResponse()
etag = ETag(value="0123-weak-kotik", is_weak=True)
resp.etag = etag
assert resp.etag == etag
assert resp.headers[hdrs.ETAG] == f'W/"{etag.value}"'
assert resp.headers[hdrs.ETAG] == expected_header


def test_etag_any() -> None:
Expand All @@ -286,11 +292,20 @@ def test_etag_any() -> None:
assert resp.headers[hdrs.ETAG] == "*"


@pytest.mark.parametrize("wrong_value", (123, ETag(value=123, is_weak=True)))
def test_etag_wrong_class(wrong_value) -> None:
@pytest.mark.parametrize(
"invalid", ('"invalid"', ETag(value='"invalid"', is_weak=True))
)
def test_etag_invalid_value(invalid) -> None:
resp = StreamResponse()
with pytest.raises(ValueError):
resp.etag = wrong_value
resp.etag = invalid


@pytest.mark.parametrize("invalid", (123, ETag(value=123, is_weak=True)))
def test_etag_invalid_value_class(invalid) -> None:
resp = StreamResponse()
with pytest.raises(ValueError):
resp.etag = invalid


def test_etag_reset() -> None:
Expand Down

0 comments on commit e6e979a

Please sign in to comment.