tigrcorn-static 0.3.18.dev1__tar.gz → 0.3.18.dev3__tar.gz

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 (20) hide show
  1. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/PKG-INFO +5 -5
  2. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/README.md +1 -1
  3. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/pyproject.toml +4 -4
  4. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/__init__.py +8 -0
  5. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/security.py +109 -0
  6. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static.egg-info/PKG-INFO +5 -5
  7. tigrcorn_static-0.3.18.dev3/src/tigrcorn_static.egg-info/requires.txt +3 -0
  8. tigrcorn_static-0.3.18.dev1/src/tigrcorn_static.egg-info/requires.txt +0 -3
  9. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/LICENSE +0 -0
  10. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/setup.cfg +0 -0
  11. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/__init__.py +0 -0
  12. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/py.typed +0 -0
  13. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/app.py +0 -0
  14. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/models.py +0 -0
  15. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/mounting.py +0 -0
  16. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/representations.py +0 -0
  17. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static/static/responses.py +0 -0
  18. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static.egg-info/SOURCES.txt +0 -0
  19. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static.egg-info/dependency_links.txt +0 -0
  20. {tigrcorn_static-0.3.18.dev1 → tigrcorn_static-0.3.18.dev3}/src/tigrcorn_static.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-static
3
- Version: 0.3.18.dev1
3
+ Version: 0.3.18.dev3
4
4
  Summary: Static file origin, file-send, validators, ranges, and cache-aware HTTP responses for the Tigrcorn ASGI server.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,9 +186,9 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.18.dev1
190
- Requires-Dist: tigrcorn-asgi==0.3.18.dev1
191
- Requires-Dist: tigrcorn-http==0.3.18.dev1
189
+ Requires-Dist: tigrcorn-core==0.3.18.dev3
190
+ Requires-Dist: tigrcorn-asgi==0.3.18.dev3
191
+ Requires-Dist: tigrcorn-http==0.3.18.dev3
192
192
  Dynamic: license-file
193
193
 
194
194
  <div align="center">
@@ -279,7 +279,7 @@ The package exposes its supported public surface through the <code>tigrcorn_stat
279
279
 
280
280
  ## Package Graph
281
281
 
282
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
282
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
283
283
 
284
284
  ## Best Practices
285
285
 
@@ -86,7 +86,7 @@ The package exposes its supported public surface through the <code>tigrcorn_stat
86
86
 
87
87
  ## Package Graph
88
88
 
89
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
89
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
90
90
 
91
91
  ## Best Practices
92
92
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-static"
7
- version = "0.3.18.dev1"
7
+ version = "0.3.18.dev3"
8
8
  description = "Static file origin, file-send, validators, ranges, and cache-aware HTTP responses for the Tigrcorn ASGI server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.15"
@@ -29,9 +29,9 @@ classifiers = [
29
29
  "Typing :: Typed",
30
30
  ]
31
31
  dependencies = [
32
- "tigrcorn-core==0.3.18.dev1",
33
- "tigrcorn-asgi==0.3.18.dev1",
34
- "tigrcorn-http==0.3.18.dev1",
32
+ "tigrcorn-core==0.3.18.dev3",
33
+ "tigrcorn-asgi==0.3.18.dev3",
34
+ "tigrcorn-http==0.3.18.dev3",
35
35
  ]
36
36
 
37
37
  [tool.setuptools]
@@ -5,21 +5,26 @@ from .mounting import mount_static_app, normalize_static_route
5
5
  from .security import (
6
6
  StaticSecurityCertificationError,
7
7
  StaticSecurityPolicy,
8
+ build_static_certification_evidence,
8
9
  certify_static_delivery_security,
9
10
  static_alt_svc_headers,
10
11
  static_cache_headers,
11
12
  static_delivery_certification_artifact,
12
13
  static_security_policy,
13
14
  validate_static_early_hints,
15
+ validate_static_content_length,
14
16
  validate_static_path,
15
17
  validate_static_range_request,
18
+ validate_static_range_amplification,
16
19
  validate_static_resolved_path,
20
+ validate_static_sidecar_pair,
17
21
  )
18
22
 
19
23
  __all__ = [
20
24
  "StaticFilesApp",
21
25
  "StaticSecurityCertificationError",
22
26
  "StaticSecurityPolicy",
27
+ "build_static_certification_evidence",
23
28
  "certify_static_delivery_security",
24
29
  "mount_static_app",
25
30
  "normalize_static_route",
@@ -28,7 +33,10 @@ __all__ = [
28
33
  "static_delivery_certification_artifact",
29
34
  "static_security_policy",
30
35
  "validate_static_early_hints",
36
+ "validate_static_content_length",
31
37
  "validate_static_path",
32
38
  "validate_static_range_request",
39
+ "validate_static_range_amplification",
33
40
  "validate_static_resolved_path",
41
+ "validate_static_sidecar_pair",
34
42
  ]
@@ -76,6 +76,34 @@ def static_delivery_certification_artifact() -> dict[str, Any]:
76
76
  }
77
77
 
78
78
 
79
+ def build_static_certification_evidence(root: str | Path, *, profile: str = "default") -> dict[str, Any]:
80
+ root_path = Path(root)
81
+ negative_corpus = {
82
+ "dotdot_traversal": _raises_static_error(lambda: validate_static_path(root_path, "/../secret.txt")),
83
+ "encoded_traversal": _raises_static_error(lambda: validate_static_path(root_path, "/%2e%2e/secret.txt")),
84
+ "unicode_traversal": _raises_static_error(lambda: validate_static_path(root_path, "/\uff0e\uff0e/secret.txt")),
85
+ "symlink_escape": _raises_static_error(lambda: validate_static_resolved_path(root_path, root_path.parent / "__tigrcorn_escape_probe__")),
86
+ }
87
+ alt_svc_headers = static_alt_svc_headers(
88
+ profile=profile,
89
+ static_enabled=True,
90
+ requested_headers=[b'h3=":443"; ma=3600', b'h2=":443"'],
91
+ )
92
+ early_hints = validate_static_early_hints([(b"link", b"</assets/app.css>; rel=preload")])
93
+ return {
94
+ "checks": {
95
+ "alt_svc": _json_header_list(alt_svc_headers),
96
+ "early_hints": _json_header_list(early_hints),
97
+ "range_amplification": validate_static_range_amplification(b"bytes=0-0", resource_length=1),
98
+ },
99
+ "mount_name": root_path.name,
100
+ "negative_corpus": negative_corpus,
101
+ "policy": static_security_policy()["policy"],
102
+ "profile": profile,
103
+ "schema_version": 1,
104
+ }
105
+
106
+
79
107
  def validate_static_path(root: str | Path, request_path: str) -> dict[str, Any]:
80
108
  root_path = Path(root).resolve()
81
109
  decoded = _decode_static_path(request_path)
@@ -182,6 +210,71 @@ def validate_static_range_request(
182
210
  }
183
211
 
184
212
 
213
+ def validate_static_sidecar_pair(
214
+ origin: str | Path,
215
+ sidecar: str | Path,
216
+ *,
217
+ coding: str,
218
+ ) -> dict[str, Any]:
219
+ origin_path = Path(origin)
220
+ sidecar_path = Path(sidecar)
221
+ expected_suffix = _sidecar_suffix_map().get(coding)
222
+ if expected_suffix is None:
223
+ raise StaticSecurityCertificationError(f"unsupported static sidecar coding: {coding}")
224
+ if sidecar_path.name != origin_path.name + expected_suffix:
225
+ raise StaticSecurityCertificationError("static sidecar name does not match origin")
226
+ if not origin_path.exists() or not sidecar_path.exists():
227
+ raise StaticSecurityCertificationError("static sidecar pair is incomplete")
228
+ return {
229
+ "accepted": True,
230
+ "coding": coding,
231
+ "origin": os.fspath(origin_path),
232
+ "sidecar": os.fspath(sidecar_path),
233
+ "sidecar_size": sidecar_path.stat().st_size,
234
+ }
235
+
236
+
237
+ def validate_static_content_length(
238
+ headers: Iterable[tuple[bytes | str, bytes | str]],
239
+ *,
240
+ expected_length: int,
241
+ ) -> dict[str, Any]:
242
+ normalized = [
243
+ (
244
+ name if isinstance(name, bytes) else str(name).encode("latin1"),
245
+ value if isinstance(value, bytes) else str(value).encode("latin1"),
246
+ )
247
+ for name, value in headers
248
+ ]
249
+ value = get_header([(name.lower(), header_value) for name, header_value in normalized], b"content-length")
250
+ if value is None:
251
+ return {"accepted": True, "content_length": None}
252
+ try:
253
+ parsed = int(value.decode("ascii"))
254
+ except ValueError as exc:
255
+ raise StaticSecurityCertificationError("invalid static content-length") from exc
256
+ if parsed != expected_length:
257
+ raise StaticSecurityCertificationError("static content-length mismatch")
258
+ return {"accepted": True, "content_length": parsed}
259
+
260
+
261
+ def validate_static_range_amplification(
262
+ range_header: bytes | str,
263
+ *,
264
+ resource_length: int,
265
+ max_parts: int = 16,
266
+ ) -> dict[str, Any]:
267
+ value = range_header if isinstance(range_header, bytes) else str(range_header).encode("latin1")
268
+ if not value.lower().startswith(b"bytes="):
269
+ raise StaticSecurityCertificationError("unsupported static range unit")
270
+ parts = [part.strip() for part in value[6:].split(b",") if part.strip()]
271
+ if len(parts) > max_parts:
272
+ raise StaticSecurityCertificationError("static range amplification limit exceeded")
273
+ if resource_length < 0:
274
+ raise StaticSecurityCertificationError("invalid static resource length")
275
+ return {"accepted": True, "parts": len(parts), "resource_length": resource_length}
276
+
277
+
185
278
  def certify_static_delivery_security(evidence: Mapping[str, Any]) -> dict[str, Any]:
186
279
  corpus = evidence.get("negative_corpus") or {}
187
280
  required = ("dotdot_traversal", "encoded_traversal", "unicode_traversal", "symlink_escape")
@@ -226,3 +319,19 @@ def _validate_static_path_parts(path: str) -> StaticPathDecision:
226
319
  def _without_header(headers: HeaderList, name: bytes) -> HeaderList:
227
320
  lowered = name.lower()
228
321
  return [(header_name, value) for header_name, value in headers if header_name.lower() != lowered]
322
+
323
+
324
+ def _raises_static_error(callback) -> bool:
325
+ try:
326
+ callback()
327
+ except StaticSecurityCertificationError:
328
+ return True
329
+ return False
330
+
331
+
332
+ def _sidecar_suffix_map() -> dict[str, str]:
333
+ return {"br": ".br", "gzip": ".gz"}
334
+
335
+
336
+ def _json_header_list(headers: Iterable[tuple[bytes, bytes]]) -> list[list[str]]:
337
+ return [[name.decode("latin1"), value.decode("latin1")] for name, value in headers]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-static
3
- Version: 0.3.18.dev1
3
+ Version: 0.3.18.dev3
4
4
  Summary: Static file origin, file-send, validators, ranges, and cache-aware HTTP responses for the Tigrcorn ASGI server.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -186,9 +186,9 @@ Classifier: Typing :: Typed
186
186
  Requires-Python: <3.15,>=3.10
187
187
  Description-Content-Type: text/markdown
188
188
  License-File: LICENSE
189
- Requires-Dist: tigrcorn-core==0.3.18.dev1
190
- Requires-Dist: tigrcorn-asgi==0.3.18.dev1
191
- Requires-Dist: tigrcorn-http==0.3.18.dev1
189
+ Requires-Dist: tigrcorn-core==0.3.18.dev3
190
+ Requires-Dist: tigrcorn-asgi==0.3.18.dev3
191
+ Requires-Dist: tigrcorn-http==0.3.18.dev3
192
192
  Dynamic: license-file
193
193
 
194
194
  <div align="center">
@@ -279,7 +279,7 @@ The package exposes its supported public surface through the <code>tigrcorn_stat
279
279
 
280
280
  ## Package Graph
281
281
 
282
- [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
282
+ [tigrcorn-core](https://pypi.org/project/tigrcorn-core/) | [tigrcorn-quic-cc](https://pypi.org/project/tigrcorn-quic-cc/) | [tigrcorn-quic-cc-reno](https://pypi.org/project/tigrcorn-quic-cc-reno/) | [tigrcorn-config](https://pypi.org/project/tigrcorn-config/) | [tigrcorn-http](https://pypi.org/project/tigrcorn-http/) | [tigrcorn-asgi](https://pypi.org/project/tigrcorn-asgi/) | [tigrcorn-contract](https://pypi.org/project/tigrcorn-contract/) | [tigrcorn-transports](https://pypi.org/project/tigrcorn-transports/) | [tigrcorn-security](https://pypi.org/project/tigrcorn-security/) | [tigrcorn-protocols](https://pypi.org/project/tigrcorn-protocols/) | [tigrcorn-static](https://pypi.org/project/tigrcorn-static/) | [tigrcorn-observability](https://pypi.org/project/tigrcorn-observability/) | [tigrcorn-runtime](https://pypi.org/project/tigrcorn-runtime/) | [tigrcorn-compat](https://pypi.org/project/tigrcorn-compat/) | [tigrcorn-certification](https://pypi.org/project/tigrcorn-certification/)
283
283
 
284
284
  ## Best Practices
285
285
 
@@ -0,0 +1,3 @@
1
+ tigrcorn-core==0.3.18.dev3
2
+ tigrcorn-asgi==0.3.18.dev3
3
+ tigrcorn-http==0.3.18.dev3
@@ -1,3 +0,0 @@
1
- tigrcorn-core==0.3.18.dev1
2
- tigrcorn-asgi==0.3.18.dev1
3
- tigrcorn-http==0.3.18.dev1