plain 0.1.0__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.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
plain/utils/README.md
ADDED
plain/utils/__init__.py
ADDED
|
File without changes
|
plain/utils/_os.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from os.path import abspath, dirname, join, normcase, sep
|
|
4
|
+
|
|
5
|
+
from plain.exceptions import SuspiciousFileOperation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safe_join(base, *paths):
|
|
9
|
+
"""
|
|
10
|
+
Join one or more path components to the base path component intelligently.
|
|
11
|
+
Return a normalized, absolute version of the final path.
|
|
12
|
+
|
|
13
|
+
Raise ValueError if the final path isn't located inside of the base path
|
|
14
|
+
component.
|
|
15
|
+
"""
|
|
16
|
+
final_path = abspath(join(base, *paths))
|
|
17
|
+
base_path = abspath(base)
|
|
18
|
+
# Ensure final_path starts with base_path (using normcase to ensure we
|
|
19
|
+
# don't false-negative on case insensitive operating systems like Windows),
|
|
20
|
+
# further, one of the following conditions must be true:
|
|
21
|
+
# a) The next character is the path separator (to prevent conditions like
|
|
22
|
+
# safe_join("/dir", "/../d"))
|
|
23
|
+
# b) The final path must be the same as the base path.
|
|
24
|
+
# c) The base path must be the most root path (meaning either "/" or "C:\\")
|
|
25
|
+
if (
|
|
26
|
+
not normcase(final_path).startswith(normcase(base_path + sep))
|
|
27
|
+
and normcase(final_path) != normcase(base_path)
|
|
28
|
+
and dirname(normcase(base_path)) != normcase(base_path)
|
|
29
|
+
):
|
|
30
|
+
raise SuspiciousFileOperation(
|
|
31
|
+
f"The joined path ({final_path}) is located outside of the base path "
|
|
32
|
+
f"component ({base_path})"
|
|
33
|
+
)
|
|
34
|
+
return final_path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def symlinks_supported():
|
|
38
|
+
"""
|
|
39
|
+
Return whether or not creating symlinks are supported in the host platform
|
|
40
|
+
and/or if they are allowed to be created (e.g. on Windows it requires admin
|
|
41
|
+
permissions).
|
|
42
|
+
"""
|
|
43
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
44
|
+
original_path = os.path.join(temp_dir, "original")
|
|
45
|
+
symlink_path = os.path.join(temp_dir, "symlink")
|
|
46
|
+
os.makedirs(original_path)
|
|
47
|
+
try:
|
|
48
|
+
os.symlink(original_path, symlink_path)
|
|
49
|
+
supported = True
|
|
50
|
+
except (OSError, NotImplementedError):
|
|
51
|
+
supported = False
|
|
52
|
+
return supported
|
plain/utils/cache.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains helper functions for controlling caching. It does so by
|
|
3
|
+
managing the "Vary" header of responses. It includes functions to patch the
|
|
4
|
+
header of response objects directly and decorators that change functions to do
|
|
5
|
+
that header-patching themselves.
|
|
6
|
+
|
|
7
|
+
For information on the Vary header, see RFC 9110 Section 12.5.5.
|
|
8
|
+
|
|
9
|
+
Essentially, the "Vary" HTTP header defines which headers a cache should take
|
|
10
|
+
into account when building its cache key. Requests with the same path but
|
|
11
|
+
different header content for headers named in "Vary" need to get different
|
|
12
|
+
cache keys to prevent delivery of wrong content.
|
|
13
|
+
|
|
14
|
+
An example: i18n middleware would need to distinguish caches by the
|
|
15
|
+
"Accept-language" header.
|
|
16
|
+
"""
|
|
17
|
+
import time
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from hashlib import md5
|
|
20
|
+
|
|
21
|
+
from plain.http import Response, ResponseNotModified
|
|
22
|
+
from plain.logs import log_response
|
|
23
|
+
from plain.runtime import settings
|
|
24
|
+
from plain.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag
|
|
25
|
+
from plain.utils.regex_helper import _lazy_re_compile
|
|
26
|
+
|
|
27
|
+
cc_delim_re = _lazy_re_compile(r"\s*,\s*")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def patch_cache_control(response, **kwargs):
|
|
31
|
+
"""
|
|
32
|
+
Patch the Cache-Control header by adding all keyword arguments to it.
|
|
33
|
+
The transformation is as follows:
|
|
34
|
+
|
|
35
|
+
* All keyword parameter names are turned to lowercase, and underscores
|
|
36
|
+
are converted to hyphens.
|
|
37
|
+
* If the value of a parameter is True (exactly True, not just a
|
|
38
|
+
true value), only the parameter name is added to the header.
|
|
39
|
+
* All other parameters are added with their value, after applying
|
|
40
|
+
str() to it.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def dictitem(s):
|
|
44
|
+
t = s.split("=", 1)
|
|
45
|
+
if len(t) > 1:
|
|
46
|
+
return (t[0].lower(), t[1])
|
|
47
|
+
else:
|
|
48
|
+
return (t[0].lower(), True)
|
|
49
|
+
|
|
50
|
+
def dictvalue(*t):
|
|
51
|
+
if t[1] is True:
|
|
52
|
+
return t[0]
|
|
53
|
+
else:
|
|
54
|
+
return f"{t[0]}={t[1]}"
|
|
55
|
+
|
|
56
|
+
cc = defaultdict(set)
|
|
57
|
+
if response.get("Cache-Control"):
|
|
58
|
+
for field in cc_delim_re.split(response.headers["Cache-Control"]):
|
|
59
|
+
directive, value = dictitem(field)
|
|
60
|
+
if directive == "no-cache":
|
|
61
|
+
# no-cache supports multiple field names.
|
|
62
|
+
cc[directive].add(value)
|
|
63
|
+
else:
|
|
64
|
+
cc[directive] = value
|
|
65
|
+
|
|
66
|
+
# If there's already a max-age header but we're being asked to set a new
|
|
67
|
+
# max-age, use the minimum of the two ages. In practice this happens when
|
|
68
|
+
# a decorator and a piece of middleware both operate on a given view.
|
|
69
|
+
if "max-age" in cc and "max_age" in kwargs:
|
|
70
|
+
kwargs["max_age"] = min(int(cc["max-age"]), kwargs["max_age"])
|
|
71
|
+
|
|
72
|
+
# Allow overriding private caching and vice versa
|
|
73
|
+
if "private" in cc and "public" in kwargs:
|
|
74
|
+
del cc["private"]
|
|
75
|
+
elif "public" in cc and "private" in kwargs:
|
|
76
|
+
del cc["public"]
|
|
77
|
+
|
|
78
|
+
for k, v in kwargs.items():
|
|
79
|
+
directive = k.replace("_", "-")
|
|
80
|
+
if directive == "no-cache":
|
|
81
|
+
# no-cache supports multiple field names.
|
|
82
|
+
cc[directive].add(v)
|
|
83
|
+
else:
|
|
84
|
+
cc[directive] = v
|
|
85
|
+
|
|
86
|
+
directives = []
|
|
87
|
+
for directive, values in cc.items():
|
|
88
|
+
if isinstance(values, set):
|
|
89
|
+
if True in values:
|
|
90
|
+
# True takes precedence.
|
|
91
|
+
values = {True}
|
|
92
|
+
directives.extend([dictvalue(directive, value) for value in values])
|
|
93
|
+
else:
|
|
94
|
+
directives.append(dictvalue(directive, values))
|
|
95
|
+
cc = ", ".join(directives)
|
|
96
|
+
response.headers["Cache-Control"] = cc
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_max_age(response):
|
|
100
|
+
"""
|
|
101
|
+
Return the max-age from the response Cache-Control header as an integer,
|
|
102
|
+
or None if it wasn't found or wasn't an integer.
|
|
103
|
+
"""
|
|
104
|
+
if not response.has_header("Cache-Control"):
|
|
105
|
+
return
|
|
106
|
+
cc = dict(
|
|
107
|
+
_to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
return int(cc["max-age"])
|
|
111
|
+
except (ValueError, TypeError, KeyError):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def set_response_etag(response):
|
|
116
|
+
if not response.streaming and response.content:
|
|
117
|
+
response.headers["ETag"] = quote_etag(
|
|
118
|
+
md5(response.content, usedforsecurity=False).hexdigest(),
|
|
119
|
+
)
|
|
120
|
+
return response
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _precondition_failed(request):
|
|
124
|
+
response = Response(status=412)
|
|
125
|
+
log_response(
|
|
126
|
+
"Precondition Failed: %s",
|
|
127
|
+
request.path,
|
|
128
|
+
response=response,
|
|
129
|
+
request=request,
|
|
130
|
+
)
|
|
131
|
+
return response
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _not_modified(request, response=None):
|
|
135
|
+
new_response = ResponseNotModified()
|
|
136
|
+
if response:
|
|
137
|
+
# Preserve the headers required by RFC 9110 Section 15.4.5, as well as
|
|
138
|
+
# Last-Modified.
|
|
139
|
+
for header in (
|
|
140
|
+
"Cache-Control",
|
|
141
|
+
"Content-Location",
|
|
142
|
+
"Date",
|
|
143
|
+
"ETag",
|
|
144
|
+
"Expires",
|
|
145
|
+
"Last-Modified",
|
|
146
|
+
"Vary",
|
|
147
|
+
):
|
|
148
|
+
if header in response:
|
|
149
|
+
new_response.headers[header] = response.headers[header]
|
|
150
|
+
|
|
151
|
+
# Preserve cookies as per the cookie specification: "If a proxy server
|
|
152
|
+
# receives a response which contains a Set-cookie header, it should
|
|
153
|
+
# propagate the Set-cookie header to the client, regardless of whether
|
|
154
|
+
# the response was 304 (Not Modified) or 200 (OK).
|
|
155
|
+
# https://curl.haxx.se/rfc/cookie_spec.html
|
|
156
|
+
new_response.cookies = response.cookies
|
|
157
|
+
return new_response
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_conditional_response(request, etag=None, last_modified=None, response=None):
|
|
161
|
+
# Only return conditional responses on successful requests.
|
|
162
|
+
if response and not (200 <= response.status_code < 300):
|
|
163
|
+
return response
|
|
164
|
+
|
|
165
|
+
# Get HTTP request headers.
|
|
166
|
+
if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", ""))
|
|
167
|
+
if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE")
|
|
168
|
+
if_unmodified_since = if_unmodified_since and parse_http_date_safe(
|
|
169
|
+
if_unmodified_since
|
|
170
|
+
)
|
|
171
|
+
if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", ""))
|
|
172
|
+
if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
|
|
173
|
+
if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since)
|
|
174
|
+
|
|
175
|
+
# Evaluation of request preconditions below follows RFC 9110 Section
|
|
176
|
+
# 13.2.2.
|
|
177
|
+
# Step 1: Test the If-Match precondition.
|
|
178
|
+
if if_match_etags and not _if_match_passes(etag, if_match_etags):
|
|
179
|
+
return _precondition_failed(request)
|
|
180
|
+
|
|
181
|
+
# Step 2: Test the If-Unmodified-Since precondition.
|
|
182
|
+
if (
|
|
183
|
+
not if_match_etags
|
|
184
|
+
and if_unmodified_since
|
|
185
|
+
and not _if_unmodified_since_passes(last_modified, if_unmodified_since)
|
|
186
|
+
):
|
|
187
|
+
return _precondition_failed(request)
|
|
188
|
+
|
|
189
|
+
# Step 3: Test the If-None-Match precondition.
|
|
190
|
+
if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags):
|
|
191
|
+
if request.method in ("GET", "HEAD"):
|
|
192
|
+
return _not_modified(request, response)
|
|
193
|
+
else:
|
|
194
|
+
return _precondition_failed(request)
|
|
195
|
+
|
|
196
|
+
# Step 4: Test the If-Modified-Since precondition.
|
|
197
|
+
if (
|
|
198
|
+
not if_none_match_etags
|
|
199
|
+
and if_modified_since
|
|
200
|
+
and not _if_modified_since_passes(last_modified, if_modified_since)
|
|
201
|
+
and request.method in ("GET", "HEAD")
|
|
202
|
+
):
|
|
203
|
+
return _not_modified(request, response)
|
|
204
|
+
|
|
205
|
+
# Step 5: Test the If-Range precondition (not supported).
|
|
206
|
+
# Step 6: Return original response since there isn't a conditional response.
|
|
207
|
+
return response
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _if_match_passes(target_etag, etags):
|
|
211
|
+
"""
|
|
212
|
+
Test the If-Match comparison as defined in RFC 9110 Section 13.1.1.
|
|
213
|
+
"""
|
|
214
|
+
if not target_etag:
|
|
215
|
+
# If there isn't an ETag, then there can't be a match.
|
|
216
|
+
return False
|
|
217
|
+
elif etags == ["*"]:
|
|
218
|
+
# The existence of an ETag means that there is "a current
|
|
219
|
+
# representation for the target resource", even if the ETag is weak,
|
|
220
|
+
# so there is a match to '*'.
|
|
221
|
+
return True
|
|
222
|
+
elif target_etag.startswith("W/"):
|
|
223
|
+
# A weak ETag can never strongly match another ETag.
|
|
224
|
+
return False
|
|
225
|
+
else:
|
|
226
|
+
# Since the ETag is strong, this will only return True if there's a
|
|
227
|
+
# strong match.
|
|
228
|
+
return target_etag in etags
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _if_unmodified_since_passes(last_modified, if_unmodified_since):
|
|
232
|
+
"""
|
|
233
|
+
Test the If-Unmodified-Since comparison as defined in RFC 9110 Section
|
|
234
|
+
13.1.4.
|
|
235
|
+
"""
|
|
236
|
+
return last_modified and last_modified <= if_unmodified_since
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _if_none_match_passes(target_etag, etags):
|
|
240
|
+
"""
|
|
241
|
+
Test the If-None-Match comparison as defined in RFC 9110 Section 13.1.2.
|
|
242
|
+
"""
|
|
243
|
+
if not target_etag:
|
|
244
|
+
# If there isn't an ETag, then there isn't a match.
|
|
245
|
+
return True
|
|
246
|
+
elif etags == ["*"]:
|
|
247
|
+
# The existence of an ETag means that there is "a current
|
|
248
|
+
# representation for the target resource", so there is a match to '*'.
|
|
249
|
+
return False
|
|
250
|
+
else:
|
|
251
|
+
# The comparison should be weak, so look for a match after stripping
|
|
252
|
+
# off any weak indicators.
|
|
253
|
+
target_etag = target_etag.strip("W/")
|
|
254
|
+
etags = (etag.strip("W/") for etag in etags)
|
|
255
|
+
return target_etag not in etags
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _if_modified_since_passes(last_modified, if_modified_since):
|
|
259
|
+
"""
|
|
260
|
+
Test the If-Modified-Since comparison as defined in RFC 9110 Section
|
|
261
|
+
13.1.3.
|
|
262
|
+
"""
|
|
263
|
+
return not last_modified or last_modified > if_modified_since
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def patch_response_headers(response, cache_timeout=None):
|
|
267
|
+
"""
|
|
268
|
+
Add HTTP caching headers to the given Response: Expires and
|
|
269
|
+
Cache-Control.
|
|
270
|
+
|
|
271
|
+
Each header is only added if it isn't already set.
|
|
272
|
+
|
|
273
|
+
cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
|
|
274
|
+
by default.
|
|
275
|
+
"""
|
|
276
|
+
if cache_timeout is None:
|
|
277
|
+
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
|
|
278
|
+
if cache_timeout < 0:
|
|
279
|
+
cache_timeout = 0 # Can't have max-age negative
|
|
280
|
+
if not response.has_header("Expires"):
|
|
281
|
+
response.headers["Expires"] = http_date(time.time() + cache_timeout)
|
|
282
|
+
patch_cache_control(response, max_age=cache_timeout)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def add_never_cache_headers(response):
|
|
286
|
+
"""
|
|
287
|
+
Add headers to a response to indicate that a page should never be cached.
|
|
288
|
+
"""
|
|
289
|
+
patch_response_headers(response, cache_timeout=-1)
|
|
290
|
+
patch_cache_control(
|
|
291
|
+
response, no_cache=True, no_store=True, must_revalidate=True, private=True
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def patch_vary_headers(response, newheaders):
|
|
296
|
+
"""
|
|
297
|
+
Add (or update) the "Vary" header in the given Response object.
|
|
298
|
+
newheaders is a list of header names that should be in "Vary". If headers
|
|
299
|
+
contains an asterisk, then "Vary" header will consist of a single asterisk
|
|
300
|
+
'*'. Otherwise, existing headers in "Vary" aren't removed.
|
|
301
|
+
"""
|
|
302
|
+
# Note that we need to keep the original order intact, because cache
|
|
303
|
+
# implementations may rely on the order of the Vary contents in, say,
|
|
304
|
+
# computing an MD5 hash.
|
|
305
|
+
if response.has_header("Vary"):
|
|
306
|
+
vary_headers = cc_delim_re.split(response.headers["Vary"])
|
|
307
|
+
else:
|
|
308
|
+
vary_headers = []
|
|
309
|
+
# Use .lower() here so we treat headers as case-insensitive.
|
|
310
|
+
existing_headers = {header.lower() for header in vary_headers}
|
|
311
|
+
additional_headers = [
|
|
312
|
+
newheader
|
|
313
|
+
for newheader in newheaders
|
|
314
|
+
if newheader.lower() not in existing_headers
|
|
315
|
+
]
|
|
316
|
+
vary_headers += additional_headers
|
|
317
|
+
if "*" in vary_headers:
|
|
318
|
+
response.headers["Vary"] = "*"
|
|
319
|
+
else:
|
|
320
|
+
response.headers["Vary"] = ", ".join(vary_headers)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _to_tuple(s):
|
|
324
|
+
t = s.split("=", 1)
|
|
325
|
+
if len(t) == 2:
|
|
326
|
+
return t[0].lower(), t[1]
|
|
327
|
+
return t[0].lower(), True
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from threading import local
|
|
2
|
+
|
|
3
|
+
from plain.runtime import settings as plain_settings
|
|
4
|
+
from plain.utils.functional import cached_property
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConnectionProxy:
|
|
8
|
+
"""Proxy for accessing a connection object's attributes."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, connections, alias):
|
|
11
|
+
self.__dict__["_connections"] = connections
|
|
12
|
+
self.__dict__["_alias"] = alias
|
|
13
|
+
|
|
14
|
+
def __getattr__(self, item):
|
|
15
|
+
return getattr(self._connections[self._alias], item)
|
|
16
|
+
|
|
17
|
+
def __setattr__(self, name, value):
|
|
18
|
+
return setattr(self._connections[self._alias], name, value)
|
|
19
|
+
|
|
20
|
+
def __delattr__(self, name):
|
|
21
|
+
return delattr(self._connections[self._alias], name)
|
|
22
|
+
|
|
23
|
+
def __contains__(self, key):
|
|
24
|
+
return key in self._connections[self._alias]
|
|
25
|
+
|
|
26
|
+
def __eq__(self, other):
|
|
27
|
+
return self._connections[self._alias] == other
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConnectionDoesNotExist(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseConnectionHandler:
|
|
35
|
+
settings_name = None
|
|
36
|
+
exception_class = ConnectionDoesNotExist
|
|
37
|
+
|
|
38
|
+
def __init__(self, settings=None):
|
|
39
|
+
self._settings = settings
|
|
40
|
+
self._connections = local()
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
43
|
+
def settings(self):
|
|
44
|
+
self._settings = self.configure_settings(self._settings)
|
|
45
|
+
return self._settings
|
|
46
|
+
|
|
47
|
+
def configure_settings(self, settings):
|
|
48
|
+
if settings is None:
|
|
49
|
+
settings = getattr(plain_settings, self.settings_name)
|
|
50
|
+
return settings
|
|
51
|
+
|
|
52
|
+
def create_connection(self, alias):
|
|
53
|
+
raise NotImplementedError("Subclasses must implement create_connection().")
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, alias):
|
|
56
|
+
try:
|
|
57
|
+
return getattr(self._connections, alias)
|
|
58
|
+
except AttributeError:
|
|
59
|
+
if alias not in self.settings:
|
|
60
|
+
raise self.exception_class(f"The connection '{alias}' doesn't exist.")
|
|
61
|
+
conn = self.create_connection(alias)
|
|
62
|
+
setattr(self._connections, alias, conn)
|
|
63
|
+
return conn
|
|
64
|
+
|
|
65
|
+
def __setitem__(self, key, value):
|
|
66
|
+
setattr(self._connections, key, value)
|
|
67
|
+
|
|
68
|
+
def __delitem__(self, key):
|
|
69
|
+
delattr(self._connections, key)
|
|
70
|
+
|
|
71
|
+
def __iter__(self):
|
|
72
|
+
return iter(self.settings)
|
|
73
|
+
|
|
74
|
+
def all(self, initialized_only=False):
|
|
75
|
+
return [
|
|
76
|
+
self[alias]
|
|
77
|
+
for alias in self
|
|
78
|
+
# If initialized_only is True, return only initialized connections.
|
|
79
|
+
if not initialized_only or hasattr(self._connections, alias)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
def close_all(self):
|
|
83
|
+
for conn in self.all(initialized_only=True):
|
|
84
|
+
conn.close()
|
plain/utils/crypto.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plain's standard crypto functions and utilities.
|
|
3
|
+
"""
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import secrets
|
|
7
|
+
|
|
8
|
+
from plain.runtime import settings
|
|
9
|
+
from plain.utils.encoding import force_bytes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidAlgorithm(ValueError):
|
|
13
|
+
"""Algorithm is not supported by hashlib."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
|
19
|
+
"""
|
|
20
|
+
Return the HMAC of 'value', using a key generated from key_salt and a
|
|
21
|
+
secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
|
|
22
|
+
but any algorithm name supported by hashlib can be passed.
|
|
23
|
+
|
|
24
|
+
A different key_salt should be passed in for every application of HMAC.
|
|
25
|
+
"""
|
|
26
|
+
if secret is None:
|
|
27
|
+
secret = settings.SECRET_KEY
|
|
28
|
+
|
|
29
|
+
key_salt = force_bytes(key_salt)
|
|
30
|
+
secret = force_bytes(secret)
|
|
31
|
+
try:
|
|
32
|
+
hasher = getattr(hashlib, algorithm)
|
|
33
|
+
except AttributeError as e:
|
|
34
|
+
raise InvalidAlgorithm(
|
|
35
|
+
"%r is not an algorithm accepted by the hashlib module." % algorithm
|
|
36
|
+
) from e
|
|
37
|
+
# We need to generate a derived key from our base key. We can do this by
|
|
38
|
+
# passing the key_salt and our base key through a pseudo-random function.
|
|
39
|
+
key = hasher(key_salt + secret).digest()
|
|
40
|
+
# If len(key_salt + secret) > block size of the hash algorithm, the above
|
|
41
|
+
# line is redundant and could be replaced by key = key_salt + secret, since
|
|
42
|
+
# the hmac module does the same thing for keys longer than the block size.
|
|
43
|
+
# However, we need to ensure that we *always* do this.
|
|
44
|
+
return hmac.new(key, msg=force_bytes(value), digestmod=hasher)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
|
|
51
|
+
"""
|
|
52
|
+
Return a securely generated random string.
|
|
53
|
+
|
|
54
|
+
The bit length of the returned value can be calculated with the formula:
|
|
55
|
+
log_2(len(allowed_chars)^length)
|
|
56
|
+
|
|
57
|
+
For example, with default `allowed_chars` (26+26+10), this gives:
|
|
58
|
+
* length: 12, bit length =~ 71 bits
|
|
59
|
+
* length: 22, bit length =~ 131 bits
|
|
60
|
+
"""
|
|
61
|
+
return "".join(secrets.choice(allowed_chars) for i in range(length))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def constant_time_compare(val1, val2):
|
|
65
|
+
"""Return True if the two strings are equal, False otherwise."""
|
|
66
|
+
return secrets.compare_digest(force_bytes(val1), force_bytes(val2))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pbkdf2(password, salt, iterations, dklen=0, digest=None):
|
|
70
|
+
"""Return the hash of password using pbkdf2."""
|
|
71
|
+
if digest is None:
|
|
72
|
+
digest = hashlib.sha256
|
|
73
|
+
dklen = dklen or None
|
|
74
|
+
password = force_bytes(password)
|
|
75
|
+
salt = force_bytes(salt)
|
|
76
|
+
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
|