httplint 2026.1.8__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.
- httplint/__init__.py +6 -0
- httplint/cache.py +617 -0
- httplint/cli/__init__.py +43 -0
- httplint/cli/http_parser.py +116 -0
- httplint/content_encoding.py +188 -0
- httplint/content_type.py +186 -0
- httplint/field/__init__.py +145 -0
- httplint/field/broken_field.py +49 -0
- httplint/field/cors.py +99 -0
- httplint/field/deprecated.py +100 -0
- httplint/field/description.py +18 -0
- httplint/field/finder.py +120 -0
- httplint/field/json_field.py +48 -0
- httplint/field/notes.py +172 -0
- httplint/field/parsers/__init__.py +8 -0
- httplint/field/parsers/accept.py +25 -0
- httplint/field/parsers/accept_ch.py +133 -0
- httplint/field/parsers/accept_encoding.py +43 -0
- httplint/field/parsers/accept_language.py +25 -0
- httplint/field/parsers/accept_ranges.py +65 -0
- httplint/field/parsers/access_control.py +22 -0
- httplint/field/parsers/access_control_allow_credentials.py +44 -0
- httplint/field/parsers/access_control_allow_headers.py +54 -0
- httplint/field/parsers/access_control_allow_methods.py +54 -0
- httplint/field/parsers/access_control_allow_origin.py +57 -0
- httplint/field/parsers/access_control_expose_headers.py +22 -0
- httplint/field/parsers/access_control_max_age.py +55 -0
- httplint/field/parsers/access_control_request_headers.py +95 -0
- httplint/field/parsers/access_control_request_method.py +62 -0
- httplint/field/parsers/age.py +93 -0
- httplint/field/parsers/allow.py +20 -0
- httplint/field/parsers/alt_svc.py +82 -0
- httplint/field/parsers/authentication_info.py +37 -0
- httplint/field/parsers/authorization.py +25 -0
- httplint/field/parsers/available_dictionary.py +108 -0
- httplint/field/parsers/cache_control.py +383 -0
- httplint/field/parsers/cache_group_invalidation.py +120 -0
- httplint/field/parsers/cache_groups.py +60 -0
- httplint/field/parsers/cache_status.py +157 -0
- httplint/field/parsers/cdn_cache_control.py +113 -0
- httplint/field/parsers/clear_site_data.py +56 -0
- httplint/field/parsers/connection.py +26 -0
- httplint/field/parsers/connectiox.py +19 -0
- httplint/field/parsers/content_base.py +15 -0
- httplint/field/parsers/content_disposition.py +158 -0
- httplint/field/parsers/content_encoding.py +123 -0
- httplint/field/parsers/content_language.py +59 -0
- httplint/field/parsers/content_length.py +129 -0
- httplint/field/parsers/content_location.py +22 -0
- httplint/field/parsers/content_md5.py +25 -0
- httplint/field/parsers/content_range.py +157 -0
- httplint/field/parsers/content_security_policy.py +249 -0
- httplint/field/parsers/content_security_policy_report_only.py +28 -0
- httplint/field/parsers/content_transfer_encoding.py +14 -0
- httplint/field/parsers/content_type.py +37 -0
- httplint/field/parsers/cross_origin_embedder_policy.py +142 -0
- httplint/field/parsers/cross_origin_embedder_policy_report_only.py +51 -0
- httplint/field/parsers/cross_origin_opener_policy.py +141 -0
- httplint/field/parsers/cross_origin_opener_policy_report_only.py +51 -0
- httplint/field/parsers/cross_origin_resource_policy.py +107 -0
- httplint/field/parsers/cteonnt_length.py +19 -0
- httplint/field/parsers/date.py +108 -0
- httplint/field/parsers/etag.py +43 -0
- httplint/field/parsers/expect.py +75 -0
- httplint/field/parsers/expires.py +47 -0
- httplint/field/parsers/from_field.py +25 -0
- httplint/field/parsers/host.py +26 -0
- httplint/field/parsers/if_match.py +25 -0
- httplint/field/parsers/if_modified_since.py +26 -0
- httplint/field/parsers/if_none_match.py +26 -0
- httplint/field/parsers/if_range.py +26 -0
- httplint/field/parsers/if_unmodified_since.py +26 -0
- httplint/field/parsers/keep_alive.py +50 -0
- httplint/field/parsers/last_modified.py +78 -0
- httplint/field/parsers/link.py +113 -0
- httplint/field/parsers/location.py +80 -0
- httplint/field/parsers/max_forwards.py +70 -0
- httplint/field/parsers/mime_version.py +18 -0
- httplint/field/parsers/nel.py +150 -0
- httplint/field/parsers/p3p.py +15 -0
- httplint/field/parsers/permissions_policy.py +160 -0
- httplint/field/parsers/pragma.py +42 -0
- httplint/field/parsers/proxy_authenticate.py +14 -0
- httplint/field/parsers/proxy_authentication_info.py +37 -0
- httplint/field/parsers/proxy_authorization.py +25 -0
- httplint/field/parsers/proxy_status.py +197 -0
- httplint/field/parsers/range.py +25 -0
- httplint/field/parsers/referer.py +80 -0
- httplint/field/parsers/referrer_policy.py +109 -0
- httplint/field/parsers/report_to.py +113 -0
- httplint/field/parsers/retry_after.py +16 -0
- httplint/field/parsers/server.py +54 -0
- httplint/field/parsers/server_timing.py +118 -0
- httplint/field/parsers/set_cookie.py +722 -0
- httplint/field/parsers/set_cookie2.py +14 -0
- httplint/field/parsers/soapaction.py +13 -0
- httplint/field/parsers/strict_transport_security.py +388 -0
- httplint/field/parsers/tcn.py +24 -0
- httplint/field/parsers/te.py +41 -0
- httplint/field/parsers/trailer.py +15 -0
- httplint/field/parsers/transfer_encoding.py +163 -0
- httplint/field/parsers/upgrade.py +27 -0
- httplint/field/parsers/use_as_dictionary.py +100 -0
- httplint/field/parsers/user_agent.py +24 -0
- httplint/field/parsers/vary.py +113 -0
- httplint/field/parsers/via.py +50 -0
- httplint/field/parsers/warning.py +24 -0
- httplint/field/parsers/www_authenticate.py +22 -0
- httplint/field/parsers/x_cache.py +14 -0
- httplint/field/parsers/x_cache_lookup.py +14 -0
- httplint/field/parsers/x_content_type_options.py +60 -0
- httplint/field/parsers/x_frame_options.py +123 -0
- httplint/field/parsers/x_pad.py +21 -0
- httplint/field/section.py +134 -0
- httplint/field/singleton_field.py +56 -0
- httplint/field/structured_field.py +88 -0
- httplint/field/tests.py +115 -0
- httplint/field/unnecessary.py +56 -0
- httplint/field/utils.py +227 -0
- httplint/i18n.py +65 -0
- httplint/message.py +334 -0
- httplint/note.py +112 -0
- httplint/py.typed +1 -0
- httplint/status.py +721 -0
- httplint/syntax/__init__.py +17 -0
- httplint/syntax/rfc3986.py +228 -0
- httplint/syntax/rfc5234.py +92 -0
- httplint/syntax/rfc5322.py +121 -0
- httplint/syntax/rfc5646.py +152 -0
- httplint/syntax/rfc5987.py +70 -0
- httplint/syntax/rfc5988.py +112 -0
- httplint/syntax/rfc9110.py +673 -0
- httplint/syntax/rfc9111.py +83 -0
- httplint/syntax/rfc9112.py +166 -0
- httplint/syntax/rfc9651.py +87 -0
- httplint/translations/es/LC_MESSAGES/messages.mo +0 -0
- httplint/translations/es/LC_MESSAGES/messages.po +5706 -0
- httplint/translations/fr/LC_MESSAGES/messages.mo +0 -0
- httplint/translations/fr/LC_MESSAGES/messages.po +5722 -0
- httplint/translations/ja/LC_MESSAGES/messages.mo +0 -0
- httplint/translations/ja/LC_MESSAGES/messages.po +5024 -0
- httplint/translations/messages.pot +4078 -0
- httplint/translations/zh/LC_MESSAGES/messages.mo +0 -0
- httplint/translations/zh/LC_MESSAGES/messages.po +4823 -0
- httplint/types.py +25 -0
- httplint/util.py +124 -0
- httplint-2026.1.8.dist-info/METADATA +150 -0
- httplint-2026.1.8.dist-info/RECORD +173 -0
- httplint-2026.1.8.dist-info/WHEEL +5 -0
- httplint-2026.1.8.dist-info/entry_points.txt +2 -0
- httplint-2026.1.8.dist-info/licenses/LICENSE.md +19 -0
- httplint-2026.1.8.dist-info/top_level.txt +3 -0
- test/smoke.py +19 -0
- test/test_cache.py +144 -0
- test/test_content_encoding.py +35 -0
- test/test_description.py +31 -0
- test/test_dictionary_transport.py +92 -0
- test/test_duplicate_keys.py +22 -0
- test/test_fields.py +149 -0
- test/test_i18n.py +34 -0
- test/test_message.py +101 -0
- test/test_notes.py +27 -0
- test/test_status.py +118 -0
- test/test_syntax.py +35 -0
- test/test_unnecessary.py +37 -0
- test/utils.py +25 -0
- tools/__init__.py +0 -0
- tools/i18n/__init__.py +0 -0
- tools/i18n/autotranslate.py +118 -0
- tools/i18n/check.py +100 -0
- tools/i18n/extractors.py +70 -0
- tools/i18n/utils.py +29 -0
- tools/update_readme.py +53 -0
httplint/__init__.py
ADDED
httplint/cache.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
# pylint: disable=too-many-branches,too-many-statements
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
from httplint.note import Note, categories, levels
|
|
6
|
+
from httplint.util import relative_time
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from httplint.message import HttpRequestLinter, HttpResponseLinter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### configuration
|
|
13
|
+
CACHEABLE_METHODS = ["GET", "HEAD"]
|
|
14
|
+
HEURISTIC_CACHEABLE_STATUS = [200, 203, 206, 300, 301, 410]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResponseCacheChecker:
|
|
18
|
+
def __init__(self, response: "HttpResponseLinter") -> None:
|
|
19
|
+
self._response = response
|
|
20
|
+
self._request = cast("HttpRequestLinter", response.related)
|
|
21
|
+
self.notes = response.notes
|
|
22
|
+
self.age: int = 0
|
|
23
|
+
self.store_private = True
|
|
24
|
+
self.freshness_lifetime_private: int = 0
|
|
25
|
+
self.store_shared = True
|
|
26
|
+
self.freshness_lifetime_shared: int = 0
|
|
27
|
+
|
|
28
|
+
self.age_value = response.headers.parsed.get("age", 0)
|
|
29
|
+
self.date_value = response.headers.parsed.get("date", None)
|
|
30
|
+
self.expires_value = response.headers.parsed.get("expires", None)
|
|
31
|
+
self.lm_value = response.headers.parsed.get("last-modified", None)
|
|
32
|
+
self.etag_value = response.headers.parsed.get("etag", None)
|
|
33
|
+
self.vary_value = response.headers.parsed.get("vary", set())
|
|
34
|
+
self.cc_value = response.headers.parsed.get("cache-control", [])
|
|
35
|
+
self.cc_dict = dict(self.cc_value)
|
|
36
|
+
|
|
37
|
+
if self._request:
|
|
38
|
+
self.request_time = self._request.start_time
|
|
39
|
+
else:
|
|
40
|
+
self.request_time = None
|
|
41
|
+
self.response_time = response.start_time
|
|
42
|
+
|
|
43
|
+
if not self.check_basic():
|
|
44
|
+
return
|
|
45
|
+
if not self.check_storable():
|
|
46
|
+
return
|
|
47
|
+
if not self.check_age():
|
|
48
|
+
return
|
|
49
|
+
if not self.check_freshness():
|
|
50
|
+
return
|
|
51
|
+
if not self.check_stale():
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def check_basic(self) -> bool:
|
|
55
|
+
# Is Vary: * present?
|
|
56
|
+
if "*" in self.vary_value:
|
|
57
|
+
self.store_shared = self.store_private = False
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
# is Date present?
|
|
61
|
+
text_fieldnames = [fn.lower() for (fn, fv) in self._response.headers.text]
|
|
62
|
+
if "date" not in text_fieldnames:
|
|
63
|
+
self.notes.add("", DATE_CLOCKLESS)
|
|
64
|
+
if "expires" in text_fieldnames or "last-modified" in text_fieldnames:
|
|
65
|
+
self.notes.add(
|
|
66
|
+
"field-expires field-last-modified", DATE_CLOCKLESS_BAD_HDR
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def check_storable(self) -> bool:
|
|
72
|
+
# method
|
|
73
|
+
if (
|
|
74
|
+
self._request
|
|
75
|
+
and self._request.method
|
|
76
|
+
and self._request.method not in CACHEABLE_METHODS
|
|
77
|
+
):
|
|
78
|
+
self.store_shared = self.store_private = False
|
|
79
|
+
self._request.notes.add(
|
|
80
|
+
"method", STORE_METHOD_UNCACHEABLE, method=self._request.method
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# no-store
|
|
85
|
+
if "no-store" in self.cc_dict:
|
|
86
|
+
self.store_shared = self.store_private = False
|
|
87
|
+
self.notes.add("field-cache-control", STORE_NO_STORE)
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# private
|
|
91
|
+
if "private" in self.cc_dict:
|
|
92
|
+
self.store_shared = False
|
|
93
|
+
self.notes.add("field-cache-control", STORE_PRIVATE_CC)
|
|
94
|
+
|
|
95
|
+
if "public" in self.cc_dict:
|
|
96
|
+
self.notes.add("field-cache-control", STORE_PRIVATE_PUBLIC_CONFLICT)
|
|
97
|
+
|
|
98
|
+
# authorization
|
|
99
|
+
elif self._request and "authorization" in [
|
|
100
|
+
k.lower() for k, v in self._request.headers.text
|
|
101
|
+
]:
|
|
102
|
+
if "public" in self.cc_dict:
|
|
103
|
+
self.notes.add(
|
|
104
|
+
"field-cache-control", STORE_PUBLIC_AUTH, directive="public"
|
|
105
|
+
)
|
|
106
|
+
elif "must-revalidate" in self.cc_dict:
|
|
107
|
+
self.notes.add(
|
|
108
|
+
"field-cache-control",
|
|
109
|
+
STORE_PUBLIC_AUTH,
|
|
110
|
+
directive="must-revalidate",
|
|
111
|
+
)
|
|
112
|
+
elif "s-maxage" in self.cc_dict:
|
|
113
|
+
self.notes.add(
|
|
114
|
+
"field-cache-control", STORE_PUBLIC_AUTH, directive="s-maxage"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
self.store_shared = False
|
|
118
|
+
self.notes.add("field-cache-control", STORE_PRIVATE_AUTH)
|
|
119
|
+
else:
|
|
120
|
+
if (
|
|
121
|
+
"public" in self.cc_dict
|
|
122
|
+
or "private" in self.cc_dict
|
|
123
|
+
or self.expires_value
|
|
124
|
+
or "max-age" in self.cc_dict
|
|
125
|
+
or self._response.status_code in HEURISTIC_CACHEABLE_STATUS
|
|
126
|
+
):
|
|
127
|
+
self.notes.add("field-cache-control", STORE_STORABLE)
|
|
128
|
+
elif "s-maxage" in self.cc_dict:
|
|
129
|
+
self.notes.add("field-cache-control", STORE_STORABLE_SHARED_ONLY)
|
|
130
|
+
else:
|
|
131
|
+
self.notes.add("field-cache-control", STORE_UNSTORABLE)
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def check_age(self) -> bool:
|
|
137
|
+
if self.response_time and self.date_value and self.date_value > 0:
|
|
138
|
+
apparent_age = max(0, int(self.response_time) - self.date_value)
|
|
139
|
+
else:
|
|
140
|
+
apparent_age = 0
|
|
141
|
+
|
|
142
|
+
if self.request_time and self.response_time:
|
|
143
|
+
response_delay = self.response_time - self.request_time
|
|
144
|
+
corrected_age_value = self.age_value + response_delay
|
|
145
|
+
elif self.age_value is not None:
|
|
146
|
+
corrected_age_value = self.age_value
|
|
147
|
+
else:
|
|
148
|
+
corrected_age_value = 0
|
|
149
|
+
|
|
150
|
+
corrected_initial_age = max(apparent_age, corrected_age_value)
|
|
151
|
+
self.age = corrected_initial_age
|
|
152
|
+
|
|
153
|
+
if self.age >= 1:
|
|
154
|
+
age_str = relative_time(self.age, 0, 0)
|
|
155
|
+
self.notes.add("field-age field-date", CURRENT_AGE, age=age_str)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def check_freshness(self) -> bool:
|
|
159
|
+
# check to see if there's an Expires, even if it's invalid
|
|
160
|
+
expires_hdr_present = "expires" in [
|
|
161
|
+
n.lower() for (n, v) in self._response.headers.text
|
|
162
|
+
]
|
|
163
|
+
self.has_explicit_freshness = False
|
|
164
|
+
self.has_heuristic_freshness = False
|
|
165
|
+
freshness_hdrs = ["field-date"]
|
|
166
|
+
if expires_hdr_present and self.response_time:
|
|
167
|
+
# An invalid Expires header means it's automatically stale
|
|
168
|
+
self.has_explicit_freshness = True
|
|
169
|
+
freshness_hdrs.append("field-expires")
|
|
170
|
+
expires_lifetime = self.freshness_lifetime_shared = (
|
|
171
|
+
self.expires_value or 0
|
|
172
|
+
) - (self.date_value or int(self.response_time))
|
|
173
|
+
self.freshness_lifetime_private = expires_lifetime
|
|
174
|
+
self.freshness_lifetime_shared = expires_lifetime
|
|
175
|
+
if "max-age" in self.cc_dict:
|
|
176
|
+
self.freshness_lifetime_private = self.cc_dict["max-age"]
|
|
177
|
+
self.freshness_lifetime_shared = self.cc_dict["max-age"]
|
|
178
|
+
freshness_hdrs.append("field-cache-control")
|
|
179
|
+
self.has_explicit_freshness = True
|
|
180
|
+
if "s-maxage" in self.cc_dict:
|
|
181
|
+
self.freshness_lifetime_shared = self.cc_dict["s-maxage"]
|
|
182
|
+
freshness_hdrs.append("field-cache-control")
|
|
183
|
+
self.has_explicit_freshness = True
|
|
184
|
+
|
|
185
|
+
freshness_left = self.freshness_lifetime_private - self.age
|
|
186
|
+
freshness_left_str = relative_time(abs(int(freshness_left)), 0, 0)
|
|
187
|
+
freshness_lifetime_str = relative_time(
|
|
188
|
+
int(self.freshness_lifetime_private), 0, 0
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
shared_freshness_left = self.freshness_lifetime_shared - self.age
|
|
192
|
+
shared_freshness_left_str = relative_time(abs(int(shared_freshness_left)), 0, 0)
|
|
193
|
+
shared_freshness_lifetime_str = relative_time(
|
|
194
|
+
int(self.freshness_lifetime_shared), 0, 0
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
self.is_fresh = freshness_left > 0
|
|
198
|
+
self.is_shared_fresh = shared_freshness_left > 0
|
|
199
|
+
current_age_str = relative_time(self.age, 0, 0)
|
|
200
|
+
|
|
201
|
+
# no-cache
|
|
202
|
+
if "no-cache" in self.cc_dict:
|
|
203
|
+
if self.lm_value is None and self.etag_value is None:
|
|
204
|
+
self.notes.add("field-cache-control", FRESHNESS_NO_CACHE_NO_VALIDATOR)
|
|
205
|
+
else:
|
|
206
|
+
self.notes.add("field-cache-control", FRESHNESS_NO_CACHE)
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# explicit freshness
|
|
210
|
+
if self.has_explicit_freshness:
|
|
211
|
+
if self.freshness_lifetime_shared != self.freshness_lifetime_private:
|
|
212
|
+
self.notes.add(
|
|
213
|
+
" ".join(freshness_hdrs),
|
|
214
|
+
FRESHNESS_SHARED_PRIVATE,
|
|
215
|
+
fresh_lifetime=freshness_lifetime_str,
|
|
216
|
+
fresh_left=freshness_left_str,
|
|
217
|
+
share_lifetime=shared_freshness_lifetime_str,
|
|
218
|
+
share_left=shared_freshness_left_str,
|
|
219
|
+
private_status="fresh" if self.is_fresh else "stale",
|
|
220
|
+
shared_status="fresh" if self.is_shared_fresh else "stale",
|
|
221
|
+
)
|
|
222
|
+
elif self.is_fresh or self.is_shared_fresh:
|
|
223
|
+
self.notes.add(
|
|
224
|
+
" ".join(freshness_hdrs),
|
|
225
|
+
FRESHNESS_FRESH,
|
|
226
|
+
freshness_lifetime=freshness_lifetime_str,
|
|
227
|
+
freshness_left=freshness_left_str,
|
|
228
|
+
current_age=current_age_str,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
self.notes.add(
|
|
232
|
+
" ".join(freshness_hdrs),
|
|
233
|
+
FRESHNESS_STALE_ALREADY,
|
|
234
|
+
freshness_lifetime=freshness_lifetime_str,
|
|
235
|
+
freshness_left=freshness_left_str,
|
|
236
|
+
current_age=current_age_str,
|
|
237
|
+
)
|
|
238
|
+
if "max-age" in self.cc_dict and expires_hdr_present:
|
|
239
|
+
self.notes.add("field-expires field-cache-control", CC_AND_EXPIRES)
|
|
240
|
+
|
|
241
|
+
# heuristic freshness
|
|
242
|
+
elif self._response.status_code in HEURISTIC_CACHEABLE_STATUS:
|
|
243
|
+
self.has_heuristic_freshness = True
|
|
244
|
+
self.notes.add("field-last-modified", FRESHNESS_HEURISTIC)
|
|
245
|
+
else:
|
|
246
|
+
self.notes.add("", FRESHNESS_NONE)
|
|
247
|
+
|
|
248
|
+
# check to see if public was necessary
|
|
249
|
+
if "public" in self.cc_dict and (
|
|
250
|
+
self.has_explicit_freshness or self.has_heuristic_freshness
|
|
251
|
+
):
|
|
252
|
+
self.notes.add("field-cache-control", STORE_PUBLIC_UNNECESSARY)
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
def check_stale(self) -> bool:
|
|
256
|
+
# stale-while-revalidate
|
|
257
|
+
if "stale-while-revalidate" in self.cc_dict:
|
|
258
|
+
self.notes.add("field-cache-control", STALE_WHILE_REVALIDATE)
|
|
259
|
+
|
|
260
|
+
# stale revalidation
|
|
261
|
+
if "stale-if-error" in self.cc_dict:
|
|
262
|
+
self.notes.add("field-cache-control", STALE_IF_ERROR)
|
|
263
|
+
elif "must-revalidate" in self.cc_dict:
|
|
264
|
+
if self.is_fresh:
|
|
265
|
+
self.notes.add("field-cache-control", FRESH_MUST_REVALIDATE)
|
|
266
|
+
elif self.has_explicit_freshness:
|
|
267
|
+
self.notes.add("field-cache-control", STALE_MUST_REVALIDATE)
|
|
268
|
+
elif "proxy-revalidate" in self.cc_dict or "s-maxage" in self.cc_dict:
|
|
269
|
+
if self.is_shared_fresh:
|
|
270
|
+
self.notes.add("field-cache-control", FRESH_PROXY_REVALIDATE)
|
|
271
|
+
elif self.has_explicit_freshness:
|
|
272
|
+
self.notes.add("field-cache-control", STALE_PROXY_REVALIDATE)
|
|
273
|
+
else:
|
|
274
|
+
self.notes.add("field-cache-control", STALE_SERVABLE)
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class DATE_CLOCKLESS(Note):
|
|
279
|
+
category = categories.GENERAL
|
|
280
|
+
level = levels.WARN
|
|
281
|
+
_summary = "This response doesn't have a Date header."
|
|
282
|
+
_text = """\
|
|
283
|
+
Although HTTP allows a server not to send a `Date` header in responses if it doesn't have a local
|
|
284
|
+
clock, this can make calculation of a stored response's age inexact."""
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class DATE_CLOCKLESS_BAD_HDR(Note):
|
|
288
|
+
category = categories.CACHING
|
|
289
|
+
level = levels.BAD
|
|
290
|
+
_summary = "This response has an Expires or Last-Modified header, but no Date."
|
|
291
|
+
_text = """\
|
|
292
|
+
Because both the `Expires` and `Last-Modified` headers are date-based, it's necessary to send when
|
|
293
|
+
the message was generated in `Date` for them to be useful; otherwise, clock drift, transit times
|
|
294
|
+
between nodes as well as caching could skew their application."""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class STORE_METHOD_UNCACHEABLE(Note):
|
|
298
|
+
category = categories.CACHING
|
|
299
|
+
level = levels.INFO
|
|
300
|
+
_summary = "Responses to the %(method)s method can't be stored by caches."
|
|
301
|
+
_text = """\
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class STORE_NO_STORE(Note):
|
|
306
|
+
category = categories.CACHING
|
|
307
|
+
level = levels.INFO
|
|
308
|
+
_summary = "This response can't be stored by caches."
|
|
309
|
+
_text = """\
|
|
310
|
+
The `Cache-Control: no-store` directive indicates that this response can't be stored by a cache."""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class STORE_PRIVATE_CC(Note):
|
|
314
|
+
category = categories.CACHING
|
|
315
|
+
level = levels.INFO
|
|
316
|
+
_summary = "This response allows only private caches to store it."
|
|
317
|
+
_text = """\
|
|
318
|
+
The `Cache-Control: private` directive indicates that the response can only be stored by caches
|
|
319
|
+
that are specific to a single user; for example, a browser cache. Shared caches, such as those in
|
|
320
|
+
proxies, cannot store it."""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class STORE_PRIVATE_PUBLIC_CONFLICT(Note):
|
|
324
|
+
category = categories.CACHING
|
|
325
|
+
level = levels.BAD
|
|
326
|
+
_summary = "This response contains both the `public` and `private` Cache-Control directives."
|
|
327
|
+
_text = """\
|
|
328
|
+
`Cache-Control: public` and `Cache-Control: private` conflict; they should not occur on the same message.
|
|
329
|
+
|
|
330
|
+
Conservative caches will ignore `public` and honor `private`, but this cannot be relied upon."""
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class STORE_PRIVATE_AUTH(Note):
|
|
334
|
+
category = categories.CACHING
|
|
335
|
+
level = levels.INFO
|
|
336
|
+
_summary = "This response can only be stored by private caches, because the request was \
|
|
337
|
+
authenticated."
|
|
338
|
+
_text = """\
|
|
339
|
+
Because the request was authenticated and this response doesn't contain a `Cache-Control: public`
|
|
340
|
+
directive, this response can only be stored by caches that are specific to a single user; for
|
|
341
|
+
example, a browser cache. Shared caches, such as those in proxies, cannot store it."""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class STORE_PUBLIC_AUTH(Note):
|
|
345
|
+
category = categories.CACHING
|
|
346
|
+
level = levels.INFO
|
|
347
|
+
_summary = (
|
|
348
|
+
"This response can be stored by all caches, "
|
|
349
|
+
"even though the request was authenticated."
|
|
350
|
+
)
|
|
351
|
+
_text = """\
|
|
352
|
+
Usually, responses to authenticated requests can't be stored by shared caches. However, because
|
|
353
|
+
This response contains a `Cache-Control: %(directive)s` directive, it can be stored by all caches,
|
|
354
|
+
including shared caches (like those in proxies)."""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class STORE_PUBLIC_UNNECESSARY(Note):
|
|
358
|
+
category = categories.CACHING
|
|
359
|
+
level = levels.WARN
|
|
360
|
+
_summary = "Cache-Control: public is probably not necessary."
|
|
361
|
+
_text = """\
|
|
362
|
+
The `Cache-Control: public` directive allows caches to store and use responses
|
|
363
|
+
in some circumstances when they would otherwise not be able to.
|
|
364
|
+
|
|
365
|
+
For example, it allows responses to authenticated requests to be stored by shared caches.
|
|
366
|
+
|
|
367
|
+
Other responses **do not need to contain `public`**; it does not make the
|
|
368
|
+
response "more cacheable", and only makes the response headers larger."""
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class STORE_STORABLE(Note):
|
|
372
|
+
category = categories.CACHING
|
|
373
|
+
level = levels.INFO
|
|
374
|
+
_summary = """\
|
|
375
|
+
This response allows all caches to store it."""
|
|
376
|
+
_text = """\
|
|
377
|
+
A cache can store this response; it may or may not be able to use it to satisfy a particular
|
|
378
|
+
request."""
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class STORE_STORABLE_SHARED_ONLY(Note):
|
|
382
|
+
category = categories.CACHING
|
|
383
|
+
level = levels.INFO
|
|
384
|
+
_summary = """\
|
|
385
|
+
This response allows only shared caches to store it."""
|
|
386
|
+
_text = """\
|
|
387
|
+
A shared cache can store this response; it may or may not be able to use it to satisfy a
|
|
388
|
+
particular request.
|
|
389
|
+
|
|
390
|
+
Private caches cannot store it, because there is no explicit freshness information
|
|
391
|
+
(such as Cache-Control: max-age), and the status code is not heuristically cacheable.
|
|
392
|
+
|
|
393
|
+
Note that a cache extension might override this behaviour."""
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class STORE_UNSTORABLE(Note):
|
|
397
|
+
category = categories.CACHING
|
|
398
|
+
level = levels.INFO
|
|
399
|
+
_summary = "This response cannot be stored by caches."
|
|
400
|
+
_text = """\
|
|
401
|
+
While caches can store many kinds of response as being heuristically cacheable -- that is,
|
|
402
|
+
they can store them without explicit caching directives like Cache-Control:max-age, this
|
|
403
|
+
reponse's status code is not defined as being heuristically cacheable, and other directives
|
|
404
|
+
that would allow it to be stored (such as Cache-Control: public) are not present.
|
|
405
|
+
|
|
406
|
+
Note that a cache extension might override this behaviour."""
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class FRESHNESS_NO_CACHE(Note):
|
|
410
|
+
category = categories.CACHING
|
|
411
|
+
level = levels.INFO
|
|
412
|
+
_summary = "This response cannot be served from cache without validation."
|
|
413
|
+
_text = """\
|
|
414
|
+
The `Cache-Control: no-cache` directive means that while caches can store this
|
|
415
|
+
response, they cannot use it to satisfy a request unless it has been validated (either with an
|
|
416
|
+
`If-None-Match` or `If-Modified-Since` conditional) for that request."""
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class FRESHNESS_NO_CACHE_NO_VALIDATOR(Note):
|
|
420
|
+
category = categories.CACHING
|
|
421
|
+
level = levels.WARN
|
|
422
|
+
_summary = "This response cannot be served from cache without validation, \
|
|
423
|
+
and doesn't have a validator."
|
|
424
|
+
_text = """\
|
|
425
|
+
The `Cache-Control: no-cache` directive means that while caches can store this response, they
|
|
426
|
+
cannot use it to satisfy a request unless it has been validated (either with an `If-None-Match` or
|
|
427
|
+
`If-Modified-Since` conditional) for that request.
|
|
428
|
+
|
|
429
|
+
This response doesn't have a `Last-Modified` or `ETag` header, so it effectively can't be used by a
|
|
430
|
+
cache. `Cache-Control: no-store` is more appropriate."""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class CURRENT_AGE(Note):
|
|
434
|
+
category = categories.CACHING
|
|
435
|
+
level = levels.INFO
|
|
436
|
+
_summary = "This response has already been cached for %(age)s."
|
|
437
|
+
_text = """\
|
|
438
|
+
HTTP defines an algorithm that uses the `Age` header, `Date` header, and local time observations to
|
|
439
|
+
determine the age of a response. As a result, caches might use a value other than that in the `Age`
|
|
440
|
+
header to determine how old the response is -- and therefore how much longer it can be used."""
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class FRESHNESS_FRESH(Note):
|
|
444
|
+
category = categories.CACHING
|
|
445
|
+
level = levels.GOOD
|
|
446
|
+
_summary = "This response is fresh for %(freshness_left)s."
|
|
447
|
+
_text = """\
|
|
448
|
+
A response can be considered fresh when its age (here, %(current_age)s) is less than its freshness
|
|
449
|
+
lifetime (in this case, %(freshness_lifetime)s)."""
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class FRESHNESS_SHARED_PRIVATE(Note):
|
|
453
|
+
category = categories.CACHING
|
|
454
|
+
level = levels.GOOD
|
|
455
|
+
_summary = (
|
|
456
|
+
"This response is %(private_status)s for %(fresh_left)s "
|
|
457
|
+
"(%(shared_status)s for %(share_left)s in shared caches)."
|
|
458
|
+
)
|
|
459
|
+
_text = """\
|
|
460
|
+
This response has different freshness lifetimes for private (e.g., browser) and shared (e.g., proxy)
|
|
461
|
+
caches.
|
|
462
|
+
|
|
463
|
+
Private caches can consider it fresh for %(fresh_lifetime)s (until %(fresh_left)s from
|
|
464
|
+
now).
|
|
465
|
+
|
|
466
|
+
Shared caches can consider it fresh for %(share_lifetime)s (until %(share_left)s from now)."""
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class FRESHNESS_STALE_ALREADY(Note):
|
|
470
|
+
category = categories.CACHING
|
|
471
|
+
level = levels.INFO
|
|
472
|
+
_summary = "This response is stale."
|
|
473
|
+
_text = """\
|
|
474
|
+
A HTTP response is stale when its age (here, %(current_age)s) is equal to or exceeds
|
|
475
|
+
its freshness lifetime (in this case, %(freshness_lifetime)s).
|
|
476
|
+
|
|
477
|
+
There are a few reasons why a cache might serve a stale response:
|
|
478
|
+
|
|
479
|
+
* HTTP allows stale responses to be used to satisfy requests in exceptional circumstances;
|
|
480
|
+
e.g., when they lose contact with the origin server. This is subject to a number of requirements.
|
|
481
|
+
* Response directives like `stale-if-error` and `stale-while-revalidate` explicitly allow stale
|
|
482
|
+
responses to be used in particular circumstances.
|
|
483
|
+
* Some caches are configured to ignore the response's freshness directives."""
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class FRESHNESS_HEURISTIC(Note):
|
|
487
|
+
category = categories.CACHING
|
|
488
|
+
level = levels.WARN
|
|
489
|
+
_summary = (
|
|
490
|
+
"This response allows caches to assign their own freshness lifetimes to it."
|
|
491
|
+
)
|
|
492
|
+
_text = """\
|
|
493
|
+
When a response doesn't have explicit freshness information (like a `Cache-Control: max-age`
|
|
494
|
+
directive, or `Expires` header), caches are allowed to estimate how fresh it is using a heuristic
|
|
495
|
+
(provided that the status code allows it).
|
|
496
|
+
|
|
497
|
+
Caches often do this using the `Last-Modified` header. For example, if a response was last modified a
|
|
498
|
+
week ago, a cache might consider it fresh for a day.
|
|
499
|
+
|
|
500
|
+
Consider adding a `Cache-Control: max-age` header; otherwise, it may be cached for longer or shorter
|
|
501
|
+
than you'd like."""
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class FRESHNESS_NONE(Note):
|
|
505
|
+
category = categories.CACHING
|
|
506
|
+
level = levels.INFO
|
|
507
|
+
_summary = "This response cannot be considered fresh by caches."
|
|
508
|
+
_text = """\
|
|
509
|
+
This response doesn't have explicit freshness information (like a ` Cache-Control: max-age`
|
|
510
|
+
directive, or `Expires` header), and this status code doesn't allow caches to calculate their own.
|
|
511
|
+
|
|
512
|
+
Additionally, its status code doesn't allow caches to assign their own freshness lifetimes to it.
|
|
513
|
+
|
|
514
|
+
Therefore, while caches might be allowed to store it, they generally can't use it, unless it can be
|
|
515
|
+
served stale.
|
|
516
|
+
|
|
517
|
+
As a result, many caches will not store the response at all, because it is not generally useful to do
|
|
518
|
+
so."""
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class STALE_SERVABLE(Note):
|
|
522
|
+
category = categories.CACHING
|
|
523
|
+
level = levels.INFO
|
|
524
|
+
_summary = "In exceptional circumstances, caches can serve this response stale."
|
|
525
|
+
_text = """\
|
|
526
|
+
HTTP allows stale responses to be served in some circumstances; for example, if the origin
|
|
527
|
+
server can't be contacted, a stale response can be used even if it doesn't have explicit freshness
|
|
528
|
+
information available.
|
|
529
|
+
|
|
530
|
+
This behaviour can be prevented by using the `Cache-Control: must-revalidate` response directive."""
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class STALE_IF_ERROR(Note):
|
|
534
|
+
category = categories.CACHING
|
|
535
|
+
level = levels.INFO
|
|
536
|
+
_summary = "If an error occurs, caches can serve this response stale."
|
|
537
|
+
_text = """\
|
|
538
|
+
The `stale-if-error` cache directive allows a cache to return a stale response when an error
|
|
539
|
+
(e.g., a 500 Internal Server Error, or a network timeout) is encountered while attempting to
|
|
540
|
+
revalidate it.
|
|
541
|
+
|
|
542
|
+
See [RFC 5861](https://www.rfc-editor.org/rfc/rfc5861) for more information."""
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class STALE_WHILE_REVALIDATE(Note):
|
|
546
|
+
category = categories.CACHING
|
|
547
|
+
level = levels.INFO
|
|
548
|
+
_summary = (
|
|
549
|
+
"This response can be served stale from a cache while it is being revalidated."
|
|
550
|
+
)
|
|
551
|
+
_text = """\
|
|
552
|
+
The `stale-while-revalidate` cache directive allows a cache to serve a stale response while a
|
|
553
|
+
revalidation request is happening in the background.
|
|
554
|
+
|
|
555
|
+
See [RFC 5861](https://www.rfc-editor.org/rfc/rfc5861) for more information."""
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class FRESH_MUST_REVALIDATE(Note):
|
|
559
|
+
category = categories.CACHING
|
|
560
|
+
level = levels.INFO
|
|
561
|
+
_summary = "This response cannot be served by a cache once it becomes stale."
|
|
562
|
+
_text = """\
|
|
563
|
+
The `Cache-Control: must-revalidate` directive forbids caches from using stale responses to satisfy
|
|
564
|
+
requests.
|
|
565
|
+
|
|
566
|
+
For example, caches often use stale responses when they cannot connect to the origin server; when
|
|
567
|
+
this directive is present, they will return an error rather than a stale response."""
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class STALE_MUST_REVALIDATE(Note):
|
|
571
|
+
category = categories.CACHING
|
|
572
|
+
level = levels.INFO
|
|
573
|
+
_summary = "This response cannot be served by a cache now that it is stale."
|
|
574
|
+
_text = """\
|
|
575
|
+
The `Cache-Control: must-revalidate` directive forbids caches from using stale responses to satisfy
|
|
576
|
+
requests.
|
|
577
|
+
|
|
578
|
+
For example, caches often use stale responses when they cannot connect to the origin server; when
|
|
579
|
+
this directive is present, they will return an error rather than a stale response."""
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class FRESH_PROXY_REVALIDATE(Note):
|
|
583
|
+
category = categories.CACHING
|
|
584
|
+
level = levels.INFO
|
|
585
|
+
_summary = "This response cannot be served by a shared cache once it becomes stale."
|
|
586
|
+
_text = """\
|
|
587
|
+
The presence of the `Cache-Control: proxy-revalidate` and/or `s-maxage` directives forbids shared
|
|
588
|
+
caches (e.g., proxy caches) from using stale responses to satisfy requests.
|
|
589
|
+
|
|
590
|
+
For example, caches often use stale responses when they cannot connect to the origin server; when
|
|
591
|
+
this directive is present, they will return an error rather than a stale response.
|
|
592
|
+
|
|
593
|
+
These directives do not affect private caches; for example, those in browsers."""
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class STALE_PROXY_REVALIDATE(Note):
|
|
597
|
+
category = categories.CACHING
|
|
598
|
+
level = levels.INFO
|
|
599
|
+
_summary = "This response cannot be served by a shared cache now that it is stale."
|
|
600
|
+
_text = """\
|
|
601
|
+
The presence of the `Cache-Control: proxy-revalidate` and/or `s-maxage` directives forbids shared
|
|
602
|
+
caches (e.g., proxy caches) from using stale responses to satisfy requests.
|
|
603
|
+
|
|
604
|
+
For example, caches often use stale responses when they cannot connect to the origin server; when
|
|
605
|
+
this directive is present, they will return an error rather than a stale response.
|
|
606
|
+
|
|
607
|
+
These directives do not affect private caches; for example, those in browsers."""
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class CC_AND_EXPIRES(Note):
|
|
611
|
+
category = categories.CACHING
|
|
612
|
+
level = levels.INFO
|
|
613
|
+
_summary = "Cache-Control: max-age and Expires are both present."
|
|
614
|
+
_text = """\
|
|
615
|
+
The `Cache-Control: max-age` directive and the `Expires` header are both present.
|
|
616
|
+
|
|
617
|
+
`max-age` takes precedence over `Expires`, so `Expires` is redundant and can be removed."""
|
httplint/cli/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from argparse import ArgumentParser, Namespace
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from httplint.cli.http_parser import HttpCliParser, modes
|
|
6
|
+
from httplint.i18n import set_locale
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
args = getargs()
|
|
11
|
+
with set_locale(args.locale):
|
|
12
|
+
start_time = time.time() if args.now else None
|
|
13
|
+
parser = HttpCliParser(args, start_time)
|
|
14
|
+
parser.handle_input(sys.stdin.read().encode("utf-8", "replace"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def getargs() -> Namespace:
|
|
18
|
+
parser = ArgumentParser()
|
|
19
|
+
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-i",
|
|
22
|
+
"--input",
|
|
23
|
+
choices=[i.value for i in modes],
|
|
24
|
+
default=modes.RESPONSE,
|
|
25
|
+
dest="mode",
|
|
26
|
+
help="The input mode",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"-n",
|
|
31
|
+
"--now",
|
|
32
|
+
action="store_true",
|
|
33
|
+
dest="now",
|
|
34
|
+
help="Assume that the HTTP exchange happened now",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-l",
|
|
39
|
+
"--locale",
|
|
40
|
+
dest="locale",
|
|
41
|
+
help="Locale to use for output",
|
|
42
|
+
)
|
|
43
|
+
return parser.parse_args()
|