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.
Files changed (173) hide show
  1. httplint/__init__.py +6 -0
  2. httplint/cache.py +617 -0
  3. httplint/cli/__init__.py +43 -0
  4. httplint/cli/http_parser.py +116 -0
  5. httplint/content_encoding.py +188 -0
  6. httplint/content_type.py +186 -0
  7. httplint/field/__init__.py +145 -0
  8. httplint/field/broken_field.py +49 -0
  9. httplint/field/cors.py +99 -0
  10. httplint/field/deprecated.py +100 -0
  11. httplint/field/description.py +18 -0
  12. httplint/field/finder.py +120 -0
  13. httplint/field/json_field.py +48 -0
  14. httplint/field/notes.py +172 -0
  15. httplint/field/parsers/__init__.py +8 -0
  16. httplint/field/parsers/accept.py +25 -0
  17. httplint/field/parsers/accept_ch.py +133 -0
  18. httplint/field/parsers/accept_encoding.py +43 -0
  19. httplint/field/parsers/accept_language.py +25 -0
  20. httplint/field/parsers/accept_ranges.py +65 -0
  21. httplint/field/parsers/access_control.py +22 -0
  22. httplint/field/parsers/access_control_allow_credentials.py +44 -0
  23. httplint/field/parsers/access_control_allow_headers.py +54 -0
  24. httplint/field/parsers/access_control_allow_methods.py +54 -0
  25. httplint/field/parsers/access_control_allow_origin.py +57 -0
  26. httplint/field/parsers/access_control_expose_headers.py +22 -0
  27. httplint/field/parsers/access_control_max_age.py +55 -0
  28. httplint/field/parsers/access_control_request_headers.py +95 -0
  29. httplint/field/parsers/access_control_request_method.py +62 -0
  30. httplint/field/parsers/age.py +93 -0
  31. httplint/field/parsers/allow.py +20 -0
  32. httplint/field/parsers/alt_svc.py +82 -0
  33. httplint/field/parsers/authentication_info.py +37 -0
  34. httplint/field/parsers/authorization.py +25 -0
  35. httplint/field/parsers/available_dictionary.py +108 -0
  36. httplint/field/parsers/cache_control.py +383 -0
  37. httplint/field/parsers/cache_group_invalidation.py +120 -0
  38. httplint/field/parsers/cache_groups.py +60 -0
  39. httplint/field/parsers/cache_status.py +157 -0
  40. httplint/field/parsers/cdn_cache_control.py +113 -0
  41. httplint/field/parsers/clear_site_data.py +56 -0
  42. httplint/field/parsers/connection.py +26 -0
  43. httplint/field/parsers/connectiox.py +19 -0
  44. httplint/field/parsers/content_base.py +15 -0
  45. httplint/field/parsers/content_disposition.py +158 -0
  46. httplint/field/parsers/content_encoding.py +123 -0
  47. httplint/field/parsers/content_language.py +59 -0
  48. httplint/field/parsers/content_length.py +129 -0
  49. httplint/field/parsers/content_location.py +22 -0
  50. httplint/field/parsers/content_md5.py +25 -0
  51. httplint/field/parsers/content_range.py +157 -0
  52. httplint/field/parsers/content_security_policy.py +249 -0
  53. httplint/field/parsers/content_security_policy_report_only.py +28 -0
  54. httplint/field/parsers/content_transfer_encoding.py +14 -0
  55. httplint/field/parsers/content_type.py +37 -0
  56. httplint/field/parsers/cross_origin_embedder_policy.py +142 -0
  57. httplint/field/parsers/cross_origin_embedder_policy_report_only.py +51 -0
  58. httplint/field/parsers/cross_origin_opener_policy.py +141 -0
  59. httplint/field/parsers/cross_origin_opener_policy_report_only.py +51 -0
  60. httplint/field/parsers/cross_origin_resource_policy.py +107 -0
  61. httplint/field/parsers/cteonnt_length.py +19 -0
  62. httplint/field/parsers/date.py +108 -0
  63. httplint/field/parsers/etag.py +43 -0
  64. httplint/field/parsers/expect.py +75 -0
  65. httplint/field/parsers/expires.py +47 -0
  66. httplint/field/parsers/from_field.py +25 -0
  67. httplint/field/parsers/host.py +26 -0
  68. httplint/field/parsers/if_match.py +25 -0
  69. httplint/field/parsers/if_modified_since.py +26 -0
  70. httplint/field/parsers/if_none_match.py +26 -0
  71. httplint/field/parsers/if_range.py +26 -0
  72. httplint/field/parsers/if_unmodified_since.py +26 -0
  73. httplint/field/parsers/keep_alive.py +50 -0
  74. httplint/field/parsers/last_modified.py +78 -0
  75. httplint/field/parsers/link.py +113 -0
  76. httplint/field/parsers/location.py +80 -0
  77. httplint/field/parsers/max_forwards.py +70 -0
  78. httplint/field/parsers/mime_version.py +18 -0
  79. httplint/field/parsers/nel.py +150 -0
  80. httplint/field/parsers/p3p.py +15 -0
  81. httplint/field/parsers/permissions_policy.py +160 -0
  82. httplint/field/parsers/pragma.py +42 -0
  83. httplint/field/parsers/proxy_authenticate.py +14 -0
  84. httplint/field/parsers/proxy_authentication_info.py +37 -0
  85. httplint/field/parsers/proxy_authorization.py +25 -0
  86. httplint/field/parsers/proxy_status.py +197 -0
  87. httplint/field/parsers/range.py +25 -0
  88. httplint/field/parsers/referer.py +80 -0
  89. httplint/field/parsers/referrer_policy.py +109 -0
  90. httplint/field/parsers/report_to.py +113 -0
  91. httplint/field/parsers/retry_after.py +16 -0
  92. httplint/field/parsers/server.py +54 -0
  93. httplint/field/parsers/server_timing.py +118 -0
  94. httplint/field/parsers/set_cookie.py +722 -0
  95. httplint/field/parsers/set_cookie2.py +14 -0
  96. httplint/field/parsers/soapaction.py +13 -0
  97. httplint/field/parsers/strict_transport_security.py +388 -0
  98. httplint/field/parsers/tcn.py +24 -0
  99. httplint/field/parsers/te.py +41 -0
  100. httplint/field/parsers/trailer.py +15 -0
  101. httplint/field/parsers/transfer_encoding.py +163 -0
  102. httplint/field/parsers/upgrade.py +27 -0
  103. httplint/field/parsers/use_as_dictionary.py +100 -0
  104. httplint/field/parsers/user_agent.py +24 -0
  105. httplint/field/parsers/vary.py +113 -0
  106. httplint/field/parsers/via.py +50 -0
  107. httplint/field/parsers/warning.py +24 -0
  108. httplint/field/parsers/www_authenticate.py +22 -0
  109. httplint/field/parsers/x_cache.py +14 -0
  110. httplint/field/parsers/x_cache_lookup.py +14 -0
  111. httplint/field/parsers/x_content_type_options.py +60 -0
  112. httplint/field/parsers/x_frame_options.py +123 -0
  113. httplint/field/parsers/x_pad.py +21 -0
  114. httplint/field/section.py +134 -0
  115. httplint/field/singleton_field.py +56 -0
  116. httplint/field/structured_field.py +88 -0
  117. httplint/field/tests.py +115 -0
  118. httplint/field/unnecessary.py +56 -0
  119. httplint/field/utils.py +227 -0
  120. httplint/i18n.py +65 -0
  121. httplint/message.py +334 -0
  122. httplint/note.py +112 -0
  123. httplint/py.typed +1 -0
  124. httplint/status.py +721 -0
  125. httplint/syntax/__init__.py +17 -0
  126. httplint/syntax/rfc3986.py +228 -0
  127. httplint/syntax/rfc5234.py +92 -0
  128. httplint/syntax/rfc5322.py +121 -0
  129. httplint/syntax/rfc5646.py +152 -0
  130. httplint/syntax/rfc5987.py +70 -0
  131. httplint/syntax/rfc5988.py +112 -0
  132. httplint/syntax/rfc9110.py +673 -0
  133. httplint/syntax/rfc9111.py +83 -0
  134. httplint/syntax/rfc9112.py +166 -0
  135. httplint/syntax/rfc9651.py +87 -0
  136. httplint/translations/es/LC_MESSAGES/messages.mo +0 -0
  137. httplint/translations/es/LC_MESSAGES/messages.po +5706 -0
  138. httplint/translations/fr/LC_MESSAGES/messages.mo +0 -0
  139. httplint/translations/fr/LC_MESSAGES/messages.po +5722 -0
  140. httplint/translations/ja/LC_MESSAGES/messages.mo +0 -0
  141. httplint/translations/ja/LC_MESSAGES/messages.po +5024 -0
  142. httplint/translations/messages.pot +4078 -0
  143. httplint/translations/zh/LC_MESSAGES/messages.mo +0 -0
  144. httplint/translations/zh/LC_MESSAGES/messages.po +4823 -0
  145. httplint/types.py +25 -0
  146. httplint/util.py +124 -0
  147. httplint-2026.1.8.dist-info/METADATA +150 -0
  148. httplint-2026.1.8.dist-info/RECORD +173 -0
  149. httplint-2026.1.8.dist-info/WHEEL +5 -0
  150. httplint-2026.1.8.dist-info/entry_points.txt +2 -0
  151. httplint-2026.1.8.dist-info/licenses/LICENSE.md +19 -0
  152. httplint-2026.1.8.dist-info/top_level.txt +3 -0
  153. test/smoke.py +19 -0
  154. test/test_cache.py +144 -0
  155. test/test_content_encoding.py +35 -0
  156. test/test_description.py +31 -0
  157. test/test_dictionary_transport.py +92 -0
  158. test/test_duplicate_keys.py +22 -0
  159. test/test_fields.py +149 -0
  160. test/test_i18n.py +34 -0
  161. test/test_message.py +101 -0
  162. test/test_notes.py +27 -0
  163. test/test_status.py +118 -0
  164. test/test_syntax.py +35 -0
  165. test/test_unnecessary.py +37 -0
  166. test/utils.py +25 -0
  167. tools/__init__.py +0 -0
  168. tools/i18n/__init__.py +0 -0
  169. tools/i18n/autotranslate.py +118 -0
  170. tools/i18n/check.py +100 -0
  171. tools/i18n/extractors.py +70 -0
  172. tools/i18n/utils.py +29 -0
  173. tools/update_readme.py +53 -0
httplint/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from httplint.field.description import get_field_description
2
+ from httplint.message import HttpRequestLinter, HttpResponseLinter
3
+ from httplint.note import Notes
4
+
5
+
6
+ __version__ = "2026.01.8"
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."""
@@ -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()