tigrcorn-http 0.3.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrcorn_http/__init__.py +54 -0
- tigrcorn_http/alt_svc.py +71 -0
- tigrcorn_http/conditional.py +126 -0
- tigrcorn_http/early_hints.py +49 -0
- tigrcorn_http/entity.py +338 -0
- tigrcorn_http/etag.py +133 -0
- tigrcorn_http/py.typed +1 -0
- tigrcorn_http/range.py +293 -0
- tigrcorn_http/structured_fields.py +358 -0
- tigrcorn_http-0.3.16.dist-info/METADATA +293 -0
- tigrcorn_http-0.3.16.dist-info/RECORD +14 -0
- tigrcorn_http-0.3.16.dist-info/WHEEL +5 -0
- tigrcorn_http-0.3.16.dist-info/licenses/LICENSE +163 -0
- tigrcorn_http-0.3.16.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from tigrcorn_http.conditional import ConditionalEvaluation, apply_conditional_request, parse_http_date
|
|
2
|
+
from tigrcorn_http.entity import EntitySemanticsResult, apply_response_entity_semantics
|
|
3
|
+
from tigrcorn_http.etag import EntityTag, EntityTagList, format_etag, generate_entity_tag, parse_entity_tag, parse_entity_tag_list, strong_compare, weak_compare
|
|
4
|
+
from tigrcorn_http.range import ByteRange, RangeEvaluation, apply_byte_ranges, parse_range_header
|
|
5
|
+
from tigrcorn_http.structured_fields import (
|
|
6
|
+
ByteSequence,
|
|
7
|
+
Date,
|
|
8
|
+
InnerList,
|
|
9
|
+
Item,
|
|
10
|
+
StructuredFieldError,
|
|
11
|
+
Token,
|
|
12
|
+
parse_dictionary,
|
|
13
|
+
parse_item,
|
|
14
|
+
parse_list,
|
|
15
|
+
parse_structured_field,
|
|
16
|
+
serialize_dictionary,
|
|
17
|
+
serialize_item,
|
|
18
|
+
serialize_list,
|
|
19
|
+
serialize_structured_value,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'ByteRange',
|
|
24
|
+
'ConditionalEvaluation',
|
|
25
|
+
'EntitySemanticsResult',
|
|
26
|
+
'EntityTag',
|
|
27
|
+
'EntityTagList',
|
|
28
|
+
'RangeEvaluation',
|
|
29
|
+
'ByteSequence',
|
|
30
|
+
'Date',
|
|
31
|
+
'InnerList',
|
|
32
|
+
'Item',
|
|
33
|
+
'StructuredFieldError',
|
|
34
|
+
'Token',
|
|
35
|
+
'apply_byte_ranges',
|
|
36
|
+
'apply_conditional_request',
|
|
37
|
+
'apply_response_entity_semantics',
|
|
38
|
+
'format_etag',
|
|
39
|
+
'generate_entity_tag',
|
|
40
|
+
'parse_entity_tag',
|
|
41
|
+
'parse_entity_tag_list',
|
|
42
|
+
'parse_http_date',
|
|
43
|
+
'parse_dictionary',
|
|
44
|
+
'parse_range_header',
|
|
45
|
+
'parse_item',
|
|
46
|
+
'parse_list',
|
|
47
|
+
'parse_structured_field',
|
|
48
|
+
'serialize_dictionary',
|
|
49
|
+
'serialize_item',
|
|
50
|
+
'serialize_list',
|
|
51
|
+
'serialize_structured_value',
|
|
52
|
+
'strong_compare',
|
|
53
|
+
'weak_compare',
|
|
54
|
+
]
|
tigrcorn_http/alt_svc.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
HeaderList = list[tuple[bytes, bytes]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _to_bytes(value: bytes | str) -> bytes:
|
|
10
|
+
if isinstance(value, bytes):
|
|
11
|
+
return value
|
|
12
|
+
return str(value).encode('latin1')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _dedupe(values: Iterable[bytes]) -> list[bytes]:
|
|
16
|
+
seen: set[bytes] = set()
|
|
17
|
+
result: list[bytes] = []
|
|
18
|
+
for item in values:
|
|
19
|
+
token = bytes(item).strip()
|
|
20
|
+
if not token or token in seen:
|
|
21
|
+
continue
|
|
22
|
+
seen.add(token)
|
|
23
|
+
result.append(token)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def configured_alt_svc_values(config, *, request_http_version: str | None = None) -> list[bytes]:
|
|
28
|
+
explicit = _dedupe(_to_bytes(item) for item in getattr(config.http, 'alt_svc_headers', ()))
|
|
29
|
+
if explicit:
|
|
30
|
+
return explicit
|
|
31
|
+
if not getattr(config.http, 'alt_svc_auto', False):
|
|
32
|
+
return []
|
|
33
|
+
version = str(request_http_version or '').replace('HTTP/', '').strip().lower()
|
|
34
|
+
if version in {'3', '3.0', 'h3', 'http/3'}:
|
|
35
|
+
return []
|
|
36
|
+
values: list[bytes] = []
|
|
37
|
+
max_age = int(getattr(config.http, 'alt_svc_max_age', 86400))
|
|
38
|
+
persist = bool(getattr(config.http, 'alt_svc_persist', False))
|
|
39
|
+
for listener in getattr(config, 'listeners', ()): # pragma: no branch - tiny loop
|
|
40
|
+
if getattr(listener, 'kind', None) != 'udp':
|
|
41
|
+
continue
|
|
42
|
+
enabled = set(getattr(listener, 'enabled_protocols', ()))
|
|
43
|
+
if 'http3' not in enabled:
|
|
44
|
+
continue
|
|
45
|
+
port = getattr(listener, 'port', 0)
|
|
46
|
+
if not isinstance(port, int) or port <= 0:
|
|
47
|
+
continue
|
|
48
|
+
fragments = [f'h3=":{port}"'.encode('ascii')]
|
|
49
|
+
if max_age >= 0:
|
|
50
|
+
fragments.append(f'ma={max_age}'.encode('ascii'))
|
|
51
|
+
if persist:
|
|
52
|
+
fragments.append(b'persist=1')
|
|
53
|
+
values.append(b'; '.join(fragments))
|
|
54
|
+
return _dedupe(values)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def append_alt_svc_headers(
|
|
58
|
+
headers: Iterable[tuple[bytes, bytes]],
|
|
59
|
+
*,
|
|
60
|
+
config,
|
|
61
|
+
request_http_version: str | None = None,
|
|
62
|
+
) -> HeaderList:
|
|
63
|
+
normalized = [(bytes(name).lower(), bytes(value)) for name, value in headers]
|
|
64
|
+
if any(name == b'alt-svc' for name, _value in normalized):
|
|
65
|
+
return normalized
|
|
66
|
+
for value in configured_alt_svc_values(config, request_http_version=request_http_version):
|
|
67
|
+
normalized.append((b'alt-svc', value))
|
|
68
|
+
return normalized
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = ['append_alt_svc_headers', 'configured_alt_svc_values']
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from email.utils import parsedate_to_datetime
|
|
6
|
+
|
|
7
|
+
from tigrcorn_http.etag import EntityTag, EntityTagList, parse_entity_tag, parse_entity_tag_list, strong_compare, weak_compare
|
|
8
|
+
from tigrcorn_core.utils.headers import get_header
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
HeaderList = list[tuple[bytes, bytes]]
|
|
12
|
+
_PRECONDITION_FAILED_BODY = b'precondition failed'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class ConditionalEvaluation:
|
|
17
|
+
status: int
|
|
18
|
+
headers: HeaderList
|
|
19
|
+
body: bytes
|
|
20
|
+
not_modified: bool = False
|
|
21
|
+
precondition_failed: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_http_date(value: bytes | str | None) -> datetime | None:
|
|
25
|
+
if value is None:
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
dt = parsedate_to_datetime(value.decode('latin1') if isinstance(value, bytes) else value)
|
|
29
|
+
except (TypeError, ValueError, IndexError):
|
|
30
|
+
return None
|
|
31
|
+
if dt is None:
|
|
32
|
+
return None
|
|
33
|
+
if dt.tzinfo is None:
|
|
34
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
35
|
+
return dt.astimezone(timezone.utc).replace(microsecond=0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _current_validators(headers: HeaderList) -> tuple[EntityTag | None, bytes | None, datetime | None, bytes | None]:
|
|
39
|
+
etag_raw = get_header(headers, b'etag')
|
|
40
|
+
last_modified_raw = get_header(headers, b'last-modified')
|
|
41
|
+
return parse_entity_tag(etag_raw), etag_raw, parse_http_date(last_modified_raw), last_modified_raw
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_precondition_failed_headers(current_etag_raw: bytes | None, last_modified_raw: bytes | None) -> HeaderList:
|
|
45
|
+
headers: HeaderList = [(b'content-type', b'text/plain; charset=utf-8')]
|
|
46
|
+
if current_etag_raw is not None:
|
|
47
|
+
headers.append((b'etag', current_etag_raw))
|
|
48
|
+
if last_modified_raw is not None:
|
|
49
|
+
headers.append((b'last-modified', last_modified_raw))
|
|
50
|
+
return headers
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _matches_if_match(condition: EntityTagList | None, current: EntityTag | None) -> bool:
|
|
54
|
+
if condition is None:
|
|
55
|
+
return True
|
|
56
|
+
if condition.any_value:
|
|
57
|
+
return current is not None
|
|
58
|
+
return any(strong_compare(candidate, current) for candidate in condition.items)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _matches_if_none_match(condition: EntityTagList | None, current: EntityTag | None) -> bool:
|
|
62
|
+
if condition is None:
|
|
63
|
+
return False
|
|
64
|
+
if condition.any_value:
|
|
65
|
+
return current is not None
|
|
66
|
+
return any(weak_compare(candidate, current) for candidate in condition.items)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def apply_conditional_request(
|
|
70
|
+
*,
|
|
71
|
+
method: str,
|
|
72
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
|
|
73
|
+
response_headers: HeaderList,
|
|
74
|
+
body: bytes,
|
|
75
|
+
status: int,
|
|
76
|
+
) -> ConditionalEvaluation:
|
|
77
|
+
method_upper = method.upper()
|
|
78
|
+
headers = [(bytes(name).lower(), bytes(value)) for name, value in response_headers]
|
|
79
|
+
current_etag, current_etag_raw, last_modified, last_modified_raw = _current_validators(headers)
|
|
80
|
+
|
|
81
|
+
if_match_raw = get_header(request_headers, b'if-match')
|
|
82
|
+
if if_match_raw is not None:
|
|
83
|
+
condition = parse_entity_tag_list(if_match_raw)
|
|
84
|
+
if condition is not None and not _matches_if_match(condition, current_etag):
|
|
85
|
+
return ConditionalEvaluation(
|
|
86
|
+
status=412,
|
|
87
|
+
headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
|
|
88
|
+
body=_PRECONDITION_FAILED_BODY,
|
|
89
|
+
precondition_failed=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if_unmodified_since_raw = get_header(request_headers, b'if-unmodified-since')
|
|
93
|
+
if if_unmodified_since_raw is not None and last_modified is not None:
|
|
94
|
+
date_value = parse_http_date(if_unmodified_since_raw)
|
|
95
|
+
if date_value is not None and last_modified > date_value:
|
|
96
|
+
return ConditionalEvaluation(
|
|
97
|
+
status=412,
|
|
98
|
+
headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
|
|
99
|
+
body=_PRECONDITION_FAILED_BODY,
|
|
100
|
+
precondition_failed=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if_none_match_raw = get_header(request_headers, b'if-none-match')
|
|
104
|
+
if if_none_match_raw is not None:
|
|
105
|
+
condition = parse_entity_tag_list(if_none_match_raw)
|
|
106
|
+
if condition is not None and _matches_if_none_match(condition, current_etag):
|
|
107
|
+
if method_upper in {'GET', 'HEAD'}:
|
|
108
|
+
return ConditionalEvaluation(status=304, headers=headers, body=b'', not_modified=True)
|
|
109
|
+
return ConditionalEvaluation(
|
|
110
|
+
status=412,
|
|
111
|
+
headers=_build_precondition_failed_headers(current_etag_raw, last_modified_raw),
|
|
112
|
+
body=_PRECONDITION_FAILED_BODY,
|
|
113
|
+
precondition_failed=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if if_none_match_raw is None and method_upper in {'GET', 'HEAD'} and last_modified is not None:
|
|
117
|
+
if_modified_since_raw = get_header(request_headers, b'if-modified-since')
|
|
118
|
+
if if_modified_since_raw is not None:
|
|
119
|
+
date_value = parse_http_date(if_modified_since_raw)
|
|
120
|
+
if date_value is not None and last_modified <= date_value:
|
|
121
|
+
return ConditionalEvaluation(status=304, headers=headers, body=b'', not_modified=True)
|
|
122
|
+
|
|
123
|
+
return ConditionalEvaluation(status=status, headers=headers, body=body)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ['ConditionalEvaluation', 'apply_conditional_request', 'parse_http_date']
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from tigrcorn_core.utils.headers import strip_connection_specific_headers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
HeaderList = list[tuple[bytes, bytes]]
|
|
9
|
+
|
|
10
|
+
# Keep the public support envelope intentionally narrow for checkpointability:
|
|
11
|
+
# 103 responses are treated as preload-hint carriers and only preserve Link fields.
|
|
12
|
+
_EARLY_HINTS_ALLOWED_HEADERS = {b'link'}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize(headers: Iterable[tuple[bytes, bytes]]) -> HeaderList:
|
|
16
|
+
normalized = [(bytes(name).lower(), bytes(value)) for name, value in headers]
|
|
17
|
+
return strip_connection_specific_headers(normalized)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sanitize_informational_headers(status: int, headers: Iterable[tuple[bytes, bytes]]) -> HeaderList:
|
|
21
|
+
"""Return a safe informational-header list.
|
|
22
|
+
|
|
23
|
+
For 103 Early Hints, restrict the surface to Link preload hints and drop
|
|
24
|
+
connection-specific framing metadata. Other informational responses keep
|
|
25
|
+
ordinary end-to-end fields except framing metadata.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
normalized = _normalize(headers)
|
|
29
|
+
if status == 103:
|
|
30
|
+
result: HeaderList = []
|
|
31
|
+
seen: set[tuple[bytes, bytes]] = set()
|
|
32
|
+
for name, value in normalized:
|
|
33
|
+
if name not in _EARLY_HINTS_ALLOWED_HEADERS:
|
|
34
|
+
continue
|
|
35
|
+
if b'\r' in value or b'\n' in value:
|
|
36
|
+
continue
|
|
37
|
+
item = (name, value)
|
|
38
|
+
if item not in seen:
|
|
39
|
+
seen.add(item)
|
|
40
|
+
result.append(item)
|
|
41
|
+
return result
|
|
42
|
+
return [
|
|
43
|
+
(name, value)
|
|
44
|
+
for name, value in normalized
|
|
45
|
+
if name not in {b'content-length', b'transfer-encoding'}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ['sanitize_informational_headers']
|
tigrcorn_http/entity.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from tigrcorn_asgi.send import BodySegment
|
|
6
|
+
from tigrcorn_http.conditional import apply_conditional_request
|
|
7
|
+
from tigrcorn_http.etag import generate_entity_tag
|
|
8
|
+
from tigrcorn_http.range import apply_byte_ranges, build_file_range_segments, plan_file_byte_ranges
|
|
9
|
+
from tigrcorn_protocols.content_coding import ContentCodingSelection, apply_http_content_coding
|
|
10
|
+
from tigrcorn_protocols.http1.serializer import response_allows_body
|
|
11
|
+
from tigrcorn_core.utils.headers import append_if_missing, get_header, replace_header
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
HeaderList = list[tuple[bytes, bytes]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class EntitySemanticsResult:
|
|
19
|
+
status: int
|
|
20
|
+
headers: HeaderList
|
|
21
|
+
body: bytes
|
|
22
|
+
content_coding: ContentCodingSelection
|
|
23
|
+
range_applied: bool = False
|
|
24
|
+
not_modified: bool = False
|
|
25
|
+
precondition_failed: bool = False
|
|
26
|
+
head_response: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class FileBackedEntitySemanticsResult:
|
|
31
|
+
status: int
|
|
32
|
+
headers: HeaderList
|
|
33
|
+
body: bytes
|
|
34
|
+
body_segments: tuple[BodySegment, ...] = ()
|
|
35
|
+
use_body_segments: bool = False
|
|
36
|
+
range_applied: bool = False
|
|
37
|
+
not_modified: bool = False
|
|
38
|
+
precondition_failed: bool = False
|
|
39
|
+
head_response: bool = False
|
|
40
|
+
requires_materialization: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _normalize_headers(headers: list[tuple[bytes, bytes]]) -> HeaderList:
|
|
44
|
+
return [(bytes(name).lower(), bytes(value)) for name, value in headers]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def finalize_response_content_length(*, method: str, status: int, headers: HeaderList, body_length: int, trailers_present: bool = False) -> HeaderList:
|
|
48
|
+
normalized = [(name.lower(), value) for name, value in headers if name.lower() != b'content-length']
|
|
49
|
+
method_upper = method.upper()
|
|
50
|
+
if status in {204} or 100 <= status < 200:
|
|
51
|
+
return normalized
|
|
52
|
+
if trailers_present:
|
|
53
|
+
return normalized
|
|
54
|
+
if status == 304:
|
|
55
|
+
return normalized
|
|
56
|
+
if not response_allows_body(status):
|
|
57
|
+
return normalized
|
|
58
|
+
normalized.append((b'content-length', str(max(int(body_length), 0)).encode('ascii')))
|
|
59
|
+
if method_upper == 'HEAD':
|
|
60
|
+
return normalized
|
|
61
|
+
return normalized
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _maybe_generate_etag(headers: HeaderList, body: bytes, *, enabled: bool) -> HeaderList:
|
|
65
|
+
if not enabled:
|
|
66
|
+
return headers
|
|
67
|
+
if get_header(headers, b'etag') is not None:
|
|
68
|
+
return headers
|
|
69
|
+
headers = list(headers)
|
|
70
|
+
headers.append((b'etag', generate_entity_tag(body)))
|
|
71
|
+
return headers
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _default_selection() -> ContentCodingSelection:
|
|
75
|
+
return ContentCodingSelection(coding=None, identity_acceptable=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def should_materialize_response_body(
|
|
79
|
+
*,
|
|
80
|
+
method: str,
|
|
81
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
|
|
82
|
+
response_headers: list[tuple[bytes, bytes]],
|
|
83
|
+
status: int,
|
|
84
|
+
apply_content_coding: bool = True,
|
|
85
|
+
) -> bool:
|
|
86
|
+
method_upper = method.upper()
|
|
87
|
+
if method_upper == 'HEAD' or not response_allows_body(status) or status in {304} or 100 <= status < 200:
|
|
88
|
+
return False
|
|
89
|
+
if not apply_content_coding:
|
|
90
|
+
return False
|
|
91
|
+
if get_header(response_headers, b'content-encoding') is not None:
|
|
92
|
+
return False
|
|
93
|
+
return get_header(request_headers, b'accept-encoding') is not None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply_header_only_response_semantics(
|
|
97
|
+
*,
|
|
98
|
+
method: str,
|
|
99
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
|
|
100
|
+
response_headers: list[tuple[bytes, bytes]],
|
|
101
|
+
status: int,
|
|
102
|
+
body_length: int,
|
|
103
|
+
generated_etag: bytes | None = None,
|
|
104
|
+
trailers_present: bool = False,
|
|
105
|
+
advertise_accept_ranges: bool = False,
|
|
106
|
+
) -> EntitySemanticsResult:
|
|
107
|
+
method_upper = method.upper()
|
|
108
|
+
headers = _normalize_headers(response_headers)
|
|
109
|
+
if generated_etag is not None and get_header(headers, b'etag') is None and status not in {412, 416} and not (100 <= status < 200):
|
|
110
|
+
headers = list(headers)
|
|
111
|
+
headers.append((b'etag', generated_etag))
|
|
112
|
+
|
|
113
|
+
conditional = apply_conditional_request(
|
|
114
|
+
method=method_upper,
|
|
115
|
+
request_headers=request_headers,
|
|
116
|
+
response_headers=headers,
|
|
117
|
+
body=b'',
|
|
118
|
+
status=status,
|
|
119
|
+
)
|
|
120
|
+
status = conditional.status
|
|
121
|
+
headers = conditional.headers
|
|
122
|
+
body = conditional.body
|
|
123
|
+
|
|
124
|
+
if advertise_accept_ranges and get_header(headers, b'accept-ranges') is None and status in {200, 206} and get_header(headers, b'content-encoding') is None:
|
|
125
|
+
append_if_missing(headers, b'accept-ranges', b'bytes')
|
|
126
|
+
|
|
127
|
+
content_length = len(body) if conditional.precondition_failed else int(body_length)
|
|
128
|
+
headers = finalize_response_content_length(
|
|
129
|
+
method=method_upper,
|
|
130
|
+
status=status,
|
|
131
|
+
headers=headers,
|
|
132
|
+
body_length=content_length,
|
|
133
|
+
trailers_present=trailers_present,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if method_upper == 'HEAD':
|
|
137
|
+
return EntitySemanticsResult(
|
|
138
|
+
status=status,
|
|
139
|
+
headers=headers,
|
|
140
|
+
body=b'',
|
|
141
|
+
content_coding=_default_selection(),
|
|
142
|
+
not_modified=conditional.not_modified,
|
|
143
|
+
precondition_failed=conditional.precondition_failed,
|
|
144
|
+
head_response=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return EntitySemanticsResult(
|
|
148
|
+
status=status,
|
|
149
|
+
headers=headers,
|
|
150
|
+
body=body,
|
|
151
|
+
content_coding=_default_selection(),
|
|
152
|
+
not_modified=conditional.not_modified,
|
|
153
|
+
precondition_failed=conditional.precondition_failed,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def plan_file_backed_response_entity_semantics(
|
|
158
|
+
*,
|
|
159
|
+
method: str,
|
|
160
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
|
|
161
|
+
response_headers: list[tuple[bytes, bytes]],
|
|
162
|
+
status: int,
|
|
163
|
+
body_path: str,
|
|
164
|
+
body_length: int,
|
|
165
|
+
generated_etag: bytes | None = None,
|
|
166
|
+
apply_content_coding: bool = True,
|
|
167
|
+
trailers_present: bool = False,
|
|
168
|
+
) -> FileBackedEntitySemanticsResult:
|
|
169
|
+
method_upper = method.upper()
|
|
170
|
+
headers = _normalize_headers(response_headers)
|
|
171
|
+
if generated_etag is not None and get_header(headers, b'etag') is None and status not in {412, 416} and not (100 <= status < 200):
|
|
172
|
+
headers = list(headers)
|
|
173
|
+
headers.append((b'etag', generated_etag))
|
|
174
|
+
|
|
175
|
+
if should_materialize_response_body(
|
|
176
|
+
method=method_upper,
|
|
177
|
+
request_headers=request_headers,
|
|
178
|
+
response_headers=headers,
|
|
179
|
+
status=status,
|
|
180
|
+
apply_content_coding=apply_content_coding,
|
|
181
|
+
):
|
|
182
|
+
return FileBackedEntitySemanticsResult(
|
|
183
|
+
status=status,
|
|
184
|
+
headers=headers,
|
|
185
|
+
body=b'',
|
|
186
|
+
requires_materialization=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
conditional = apply_conditional_request(
|
|
190
|
+
method=method_upper,
|
|
191
|
+
request_headers=request_headers,
|
|
192
|
+
response_headers=headers,
|
|
193
|
+
body=b'',
|
|
194
|
+
status=status,
|
|
195
|
+
)
|
|
196
|
+
if conditional.not_modified or conditional.precondition_failed:
|
|
197
|
+
precondition_body = conditional.body if not (method_upper == 'HEAD') else b''
|
|
198
|
+
precondition_headers = finalize_response_content_length(
|
|
199
|
+
method=method_upper,
|
|
200
|
+
status=conditional.status,
|
|
201
|
+
headers=conditional.headers,
|
|
202
|
+
body_length=len(conditional.body),
|
|
203
|
+
trailers_present=False,
|
|
204
|
+
)
|
|
205
|
+
return FileBackedEntitySemanticsResult(
|
|
206
|
+
status=conditional.status,
|
|
207
|
+
headers=precondition_headers,
|
|
208
|
+
body=precondition_body,
|
|
209
|
+
not_modified=conditional.not_modified,
|
|
210
|
+
precondition_failed=conditional.precondition_failed,
|
|
211
|
+
head_response=method_upper == 'HEAD',
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
plan = plan_file_byte_ranges(
|
|
215
|
+
method=method_upper,
|
|
216
|
+
request_headers=request_headers,
|
|
217
|
+
response_headers=conditional.headers,
|
|
218
|
+
resource_length=body_length,
|
|
219
|
+
status=conditional.status,
|
|
220
|
+
)
|
|
221
|
+
headers = finalize_response_content_length(
|
|
222
|
+
method=method_upper,
|
|
223
|
+
status=plan.status,
|
|
224
|
+
headers=plan.headers,
|
|
225
|
+
body_length=plan.body_length,
|
|
226
|
+
trailers_present=trailers_present,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if method_upper == 'HEAD' or not response_allows_body(plan.status) or plan.unsatisfied:
|
|
230
|
+
return FileBackedEntitySemanticsResult(
|
|
231
|
+
status=plan.status,
|
|
232
|
+
headers=headers,
|
|
233
|
+
body=b'',
|
|
234
|
+
range_applied=plan.applied,
|
|
235
|
+
head_response=method_upper == 'HEAD',
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
body_segments = build_file_range_segments(
|
|
239
|
+
path=body_path,
|
|
240
|
+
plan=plan,
|
|
241
|
+
total_length=body_length,
|
|
242
|
+
source_content_type=get_header(conditional.headers, b'content-type'),
|
|
243
|
+
)
|
|
244
|
+
return FileBackedEntitySemanticsResult(
|
|
245
|
+
status=plan.status,
|
|
246
|
+
headers=headers,
|
|
247
|
+
body=b'',
|
|
248
|
+
body_segments=body_segments,
|
|
249
|
+
use_body_segments=True,
|
|
250
|
+
range_applied=plan.applied,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def apply_response_entity_semantics(
|
|
255
|
+
*,
|
|
256
|
+
method: str,
|
|
257
|
+
request_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...],
|
|
258
|
+
response_headers: list[tuple[bytes, bytes]],
|
|
259
|
+
body: bytes,
|
|
260
|
+
status: int,
|
|
261
|
+
content_coding_policy: str = 'allowlist',
|
|
262
|
+
supported_codings: tuple[str, ...] = ('br', 'gzip', 'deflate'),
|
|
263
|
+
apply_content_coding: bool = True,
|
|
264
|
+
generate_etag: bool = True,
|
|
265
|
+
trailers_present: bool = False,
|
|
266
|
+
) -> EntitySemanticsResult:
|
|
267
|
+
method_upper = method.upper()
|
|
268
|
+
headers = _normalize_headers(response_headers)
|
|
269
|
+
range_present = get_header(request_headers, b'range') is not None and method_upper in {'GET', 'HEAD'}
|
|
270
|
+
|
|
271
|
+
if apply_content_coding and not range_present:
|
|
272
|
+
status, headers, body, selection = apply_http_content_coding(
|
|
273
|
+
request_headers=request_headers,
|
|
274
|
+
response_headers=headers,
|
|
275
|
+
body=body,
|
|
276
|
+
status=status,
|
|
277
|
+
policy=content_coding_policy,
|
|
278
|
+
supported=supported_codings,
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
selection = _default_selection()
|
|
282
|
+
|
|
283
|
+
headers = _maybe_generate_etag(headers, body, enabled=generate_etag and status not in {412, 416} and not (100 <= status < 200))
|
|
284
|
+
|
|
285
|
+
conditional = apply_conditional_request(
|
|
286
|
+
method=method_upper,
|
|
287
|
+
request_headers=request_headers,
|
|
288
|
+
response_headers=headers,
|
|
289
|
+
body=body,
|
|
290
|
+
status=status,
|
|
291
|
+
)
|
|
292
|
+
status = conditional.status
|
|
293
|
+
headers = conditional.headers
|
|
294
|
+
body = conditional.body
|
|
295
|
+
range_applied = False
|
|
296
|
+
|
|
297
|
+
if not conditional.not_modified and not conditional.precondition_failed:
|
|
298
|
+
range_result = apply_byte_ranges(
|
|
299
|
+
method=method_upper,
|
|
300
|
+
request_headers=request_headers,
|
|
301
|
+
response_headers=headers,
|
|
302
|
+
body=body,
|
|
303
|
+
status=status,
|
|
304
|
+
)
|
|
305
|
+
status = range_result.status
|
|
306
|
+
headers = range_result.headers
|
|
307
|
+
body = range_result.body
|
|
308
|
+
range_applied = range_result.applied
|
|
309
|
+
|
|
310
|
+
if get_header(headers, b'accept-ranges') is None and status in {200, 206} and get_header(headers, b'content-encoding') is None:
|
|
311
|
+
append_if_missing(headers, b'accept-ranges', b'bytes')
|
|
312
|
+
|
|
313
|
+
headers = finalize_response_content_length(method=method_upper, status=status, headers=headers, body_length=len(body), trailers_present=trailers_present)
|
|
314
|
+
|
|
315
|
+
if method_upper == 'HEAD':
|
|
316
|
+
return EntitySemanticsResult(
|
|
317
|
+
status=status,
|
|
318
|
+
headers=headers,
|
|
319
|
+
body=b'',
|
|
320
|
+
content_coding=selection,
|
|
321
|
+
range_applied=range_applied,
|
|
322
|
+
not_modified=conditional.not_modified,
|
|
323
|
+
precondition_failed=conditional.precondition_failed,
|
|
324
|
+
head_response=True,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return EntitySemanticsResult(
|
|
328
|
+
status=status,
|
|
329
|
+
headers=headers,
|
|
330
|
+
body=body,
|
|
331
|
+
content_coding=selection,
|
|
332
|
+
range_applied=range_applied,
|
|
333
|
+
not_modified=conditional.not_modified,
|
|
334
|
+
precondition_failed=conditional.precondition_failed,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
__all__ = ['EntitySemanticsResult', 'FileBackedEntitySemanticsResult', 'apply_header_only_response_semantics', 'apply_response_entity_semantics', 'finalize_response_content_length', 'plan_file_backed_response_entity_semantics', 'should_materialize_response_body']
|