aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomasz Kramkowski <tomasz@kramkow.ski>2023-03-24 20:17:39 +0000
committerTomasz Kramkowski <tomasz@kramkow.ski>2023-03-24 20:24:22 +0000
commit89021aabc755da7dca56020eaf9e15cefd0e51b6 (patch)
treec7cd69f903712f1b27e4c767f904b590fa08b412
parent1f2caffb46dcbbc75323ecb97414a62fd8b505c6 (diff)
downloadpaste-89021aabc755da7dca56020eaf9e15cefd0e51b6.tar.gz
paste-89021aabc755da7dca56020eaf9e15cefd0e51b6.tar.xz
paste-89021aabc755da7dca56020eaf9e15cefd0e51b6.zip
Implement If-None-Match handling
-rw-r--r--paste/__init__.py56
1 files changed, 55 insertions, 1 deletions
diff --git a/paste/__init__.py b/paste/__init__.py
index 0800d93..f7ee3a8 100644
--- a/paste/__init__.py
+++ b/paste/__init__.py
@@ -1,7 +1,7 @@
from base64 import b64decode, b64encode
from functools import wraps
from wsgiref.util import request_uri
-from typing import Optional, Any, Protocol
+from typing import Optional, Any, Protocol, runtime_checkable
from collections.abc import Callable, Iterable
import binascii
import traceback
@@ -11,6 +11,12 @@ from . import db
from . import store
+@runtime_checkable
+class Closable(Protocol):
+ def close(self):
+ ...
+
+
class StartResponse(Protocol):
def __call__(
self,
@@ -95,6 +101,53 @@ def validate_method(app: App, environ: Env, start_response: StartResponse) -> Re
@middleware
+def if_none_match(app: App, environ: Env, start_response: StartResponse) -> Response:
+ if "HTTP_IF_NONE_MATCH" not in environ:
+ return app(environ, start_response)
+ if_none_match = environ["HTTP_IF_NONE_MATCH"]
+ del environ["HTTP_IF_NONE_MATCH"]
+ if environ["REQUEST_METHOD"] not in {"GET", "HEAD"}:
+ return app(environ, start_response)
+ head_env = environ.copy()
+ head_env["REQUEST_METHOD"] = "HEAD"
+ etag = None
+
+ def head_start_response(
+ status: str,
+ headers: list[tuple[str, str]],
+ exc_info: Optional[tuple] = None,
+ ) -> Callable[[bytes], object]:
+ _, _ = status, exc_info
+ nonlocal etag
+ for key, value in headers:
+ if key == "ETag":
+ etag = value[1:-1]
+ return lambda _: None
+
+ resp = app(head_env, head_start_response)
+ if isinstance(resp, Closable):
+ resp.close()
+
+ if not isinstance(etag, str):
+ return app(environ, start_response)
+
+ if if_none_match == "*":
+ start_response("304 Not Modified", [("ETag", etag)])
+ return []
+
+ etags = if_none_match.split(",")
+ etags = {e.strip(" \t").removeprefix("W/") for e in etags}
+ for e in etags:
+ if e[0] != '"' or e[-1] != '"':
+ return simple_response(start_response, "400 Bad Request")
+ etags = {e[1:-1] for e in etags}
+ if isinstance(etag, str) and etag in etags:
+ start_response("304 Not Modified", [("ETag", etag)])
+ return []
+ return app(environ, start_response)
+
+
+@middleware
def options(app: App, environ: Env, start_response: StartResponse) -> Response:
if environ["REQUEST_METHOD"] != "OPTIONS":
return app(environ, start_response)
@@ -140,6 +193,7 @@ def authenticate(app: App, environ: Env, start_response: StartResponse) -> Respo
@catch_exceptions
@validate_method
@options
+@if_none_match
@open_database
@authenticate
def application(environ: Env, start_response: StartResponse) -> Response: