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/logs/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logging
|
|
2
|
+
|
|
3
|
+
Default logging settings and key-value logger.
|
|
4
|
+
|
|
5
|
+
In Python, logging can be a surprisingly complex topic.
|
|
6
|
+
|
|
7
|
+
So Plain aims for easy-to-use defaults that "just work".
|
|
8
|
+
|
|
9
|
+
## `app_logger`
|
|
10
|
+
|
|
11
|
+
The default `app_logger` doesn't do much!
|
|
12
|
+
|
|
13
|
+
But it is paired with the default [settings](#) to actually show the logs like you would expect,
|
|
14
|
+
without any additional configuration.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from plain.logs import app_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def example_function():
|
|
21
|
+
app_logger.info("Hey!")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## `app_logger.kv`
|
plain/logs/__init__.py
ADDED
plain/logs/configure.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import logging.config
|
|
3
|
+
from os import environ
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_logging(logging_settings):
|
|
7
|
+
# Load the defaults
|
|
8
|
+
default_logging = {
|
|
9
|
+
"version": 1,
|
|
10
|
+
"disable_existing_loggers": False,
|
|
11
|
+
"formatters": {
|
|
12
|
+
"simple": {
|
|
13
|
+
"format": "[%(levelname)s] %(message)s",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"handlers": {
|
|
17
|
+
"console": {
|
|
18
|
+
"level": "INFO",
|
|
19
|
+
"class": "logging.StreamHandler",
|
|
20
|
+
"formatter": "simple",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
"loggers": {
|
|
24
|
+
"plain": {
|
|
25
|
+
"handlers": ["console"],
|
|
26
|
+
"level": environ.get("PLAIN_LOG_LEVEL", "INFO"),
|
|
27
|
+
},
|
|
28
|
+
"app": {
|
|
29
|
+
"handlers": ["console"],
|
|
30
|
+
"level": environ.get("APP_LOG_LEVEL", "INFO"),
|
|
31
|
+
"propagate": False,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
logging.config.dictConfig(default_logging)
|
|
36
|
+
|
|
37
|
+
# Then customize it from settings
|
|
38
|
+
if logging_settings:
|
|
39
|
+
logging.config.dictConfig(logging_settings)
|
plain/logs/loggers.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
app_logger = logging.getLogger("app")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KVLogger:
|
|
7
|
+
def __init__(self, logger):
|
|
8
|
+
self.logger = logger
|
|
9
|
+
self.context = {} # A dict that will be output in every log message
|
|
10
|
+
|
|
11
|
+
def log(self, level, message, **kwargs):
|
|
12
|
+
msg_kwargs = {
|
|
13
|
+
**kwargs,
|
|
14
|
+
**self.context, # Put these last so they're at the end of the line
|
|
15
|
+
}
|
|
16
|
+
self.logger.log(level, f"{message} {self._format_kwargs(msg_kwargs)}")
|
|
17
|
+
|
|
18
|
+
def _format_kwargs(self, kwargs):
|
|
19
|
+
outputs = []
|
|
20
|
+
|
|
21
|
+
for k, v in kwargs.items():
|
|
22
|
+
self._validate_key(k)
|
|
23
|
+
formatted_value = self._format_value(v)
|
|
24
|
+
outputs.append(f"{k}={formatted_value}")
|
|
25
|
+
|
|
26
|
+
return " ".join(outputs)
|
|
27
|
+
|
|
28
|
+
def _validate_key(self, key):
|
|
29
|
+
if " " in key:
|
|
30
|
+
raise ValueError("Keys cannot have spaces")
|
|
31
|
+
|
|
32
|
+
if "=" in key:
|
|
33
|
+
raise ValueError("Keys cannot have equals signs")
|
|
34
|
+
|
|
35
|
+
if '"' in key or "'" in key:
|
|
36
|
+
raise ValueError("Keys cannot have quotes")
|
|
37
|
+
|
|
38
|
+
def _format_value(self, value):
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
s = value
|
|
41
|
+
else:
|
|
42
|
+
s = str(value)
|
|
43
|
+
|
|
44
|
+
if '"' in s:
|
|
45
|
+
# Escape quotes and surround it
|
|
46
|
+
s = s.replace('"', '\\"')
|
|
47
|
+
s = f'"{s}"'
|
|
48
|
+
elif s == "":
|
|
49
|
+
# Quote empty strings instead of printing nothing
|
|
50
|
+
s = '""'
|
|
51
|
+
elif any(char in s for char in [" ", "/", "'", ":", "=", "."]):
|
|
52
|
+
# Surround these with quotes for parsers
|
|
53
|
+
s = f'"{s}"'
|
|
54
|
+
|
|
55
|
+
return s
|
|
56
|
+
|
|
57
|
+
def info(self, message, **kwargs):
|
|
58
|
+
self.log(logging.INFO, message, **kwargs)
|
|
59
|
+
|
|
60
|
+
def debug(self, message, **kwargs):
|
|
61
|
+
self.log(logging.DEBUG, message, **kwargs)
|
|
62
|
+
|
|
63
|
+
def warning(self, message, **kwargs):
|
|
64
|
+
self.log(logging.WARNING, message, **kwargs)
|
|
65
|
+
|
|
66
|
+
def error(self, message, **kwargs):
|
|
67
|
+
self.log(logging.ERROR, message, **kwargs)
|
|
68
|
+
|
|
69
|
+
def critical(self, message, **kwargs):
|
|
70
|
+
self.log(logging.CRITICAL, message, **kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Make this accessible from the app_logger
|
|
74
|
+
app_logger.kv = KVLogger(app_logger)
|
plain/logs/utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
request_logger = logging.getLogger("plain.request")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def log_response(
|
|
7
|
+
message,
|
|
8
|
+
*args,
|
|
9
|
+
response=None,
|
|
10
|
+
request=None,
|
|
11
|
+
logger=request_logger,
|
|
12
|
+
level=None,
|
|
13
|
+
exception=None,
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Log errors based on Response status.
|
|
17
|
+
|
|
18
|
+
Log 5xx responses as errors and 4xx responses as warnings (unless a level
|
|
19
|
+
is given as a keyword argument). The Response status_code and the
|
|
20
|
+
request are passed to the logger's extra parameter.
|
|
21
|
+
"""
|
|
22
|
+
# Check if the response has already been logged. Multiple requests to log
|
|
23
|
+
# the same response can be received in some cases, e.g., when the
|
|
24
|
+
# response is the result of an exception and is logged when the exception
|
|
25
|
+
# is caught, to record the exception.
|
|
26
|
+
if getattr(response, "_has_been_logged", False):
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if level is None:
|
|
30
|
+
if response.status_code >= 500:
|
|
31
|
+
level = "error"
|
|
32
|
+
elif response.status_code >= 400:
|
|
33
|
+
level = "warning"
|
|
34
|
+
else:
|
|
35
|
+
level = "info"
|
|
36
|
+
|
|
37
|
+
getattr(logger, level)(
|
|
38
|
+
message,
|
|
39
|
+
*args,
|
|
40
|
+
extra={
|
|
41
|
+
"status_code": response.status_code,
|
|
42
|
+
"request": request,
|
|
43
|
+
},
|
|
44
|
+
exc_info=exception,
|
|
45
|
+
)
|
|
46
|
+
response._has_been_logged = True
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clickjacking Protection Middleware.
|
|
3
|
+
|
|
4
|
+
This module provides a middleware that implements protection against a
|
|
5
|
+
malicious site loading resources from your site in a hidden frame.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from plain.runtime import settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class XFrameOptionsMiddleware:
|
|
12
|
+
"""
|
|
13
|
+
Set the X-Frame-Options HTTP header in HTTP responses.
|
|
14
|
+
|
|
15
|
+
Do not set the header if it's already set or if the response contains
|
|
16
|
+
a xframe_options_exempt value set to True.
|
|
17
|
+
|
|
18
|
+
By default, set the X-Frame-Options header to 'DENY', meaning the response
|
|
19
|
+
cannot be displayed in a frame, regardless of the site attempting to do so.
|
|
20
|
+
To enable the response to be loaded on a frame within the same site, set
|
|
21
|
+
X_FRAME_OPTIONS in your project's Plain settings to 'SAMEORIGIN'.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, get_response):
|
|
25
|
+
self.get_response = get_response
|
|
26
|
+
|
|
27
|
+
def __call__(self, request):
|
|
28
|
+
response = self.get_response(request)
|
|
29
|
+
|
|
30
|
+
# Don't set it if it's already in the response
|
|
31
|
+
if response.get("X-Frame-Options") is not None:
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
# Don't set it if they used @xframe_options_exempt
|
|
35
|
+
if getattr(response, "xframe_options_exempt", False):
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
response.headers["X-Frame-Options"] = self.get_xframe_options_value(
|
|
39
|
+
request,
|
|
40
|
+
response,
|
|
41
|
+
)
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
def get_xframe_options_value(self, request, response):
|
|
45
|
+
"""
|
|
46
|
+
Get the value to set for the X_FRAME_OPTIONS header. Use the value from
|
|
47
|
+
the X_FRAME_OPTIONS setting, or 'DENY' if not set.
|
|
48
|
+
|
|
49
|
+
This method can be overridden if needed, allowing it to vary based on
|
|
50
|
+
the request or response.
|
|
51
|
+
"""
|
|
52
|
+
return getattr(settings, "X_FRAME_OPTIONS", "DENY").upper()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from plain.http import ResponsePermanentRedirect
|
|
2
|
+
from plain.runtime import settings
|
|
3
|
+
from plain.urls import is_valid_path
|
|
4
|
+
from plain.utils.http import escape_leading_slashes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommonMiddleware:
|
|
8
|
+
"""
|
|
9
|
+
"Common" middleware for taking care of some basic operations:
|
|
10
|
+
|
|
11
|
+
- URL rewriting: Based on the APPEND_SLASH setting,
|
|
12
|
+
append missing slashes.
|
|
13
|
+
|
|
14
|
+
- If APPEND_SLASH is set and the initial URL doesn't end with a
|
|
15
|
+
slash, and it is not found in urlpatterns, form a new URL by
|
|
16
|
+
appending a slash at the end. If this new URL is found in
|
|
17
|
+
urlpatterns, return an HTTP redirect to this new URL; otherwise
|
|
18
|
+
process the initial URL as usual.
|
|
19
|
+
|
|
20
|
+
This behavior can be customized by subclassing CommonMiddleware and
|
|
21
|
+
overriding the response_redirect_class attribute.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
response_redirect_class = ResponsePermanentRedirect
|
|
25
|
+
|
|
26
|
+
def __init__(self, get_response):
|
|
27
|
+
self.get_response = get_response
|
|
28
|
+
|
|
29
|
+
def __call__(self, request):
|
|
30
|
+
"""
|
|
31
|
+
Rewrite the URL based on settings.APPEND_SLASH
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
response = self.get_response(request)
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
When the status code of the response is 404, it may redirect to a path
|
|
38
|
+
with an appended slash if should_redirect_with_slash() returns True.
|
|
39
|
+
"""
|
|
40
|
+
# If the given URL is "Not Found", then check if we should redirect to
|
|
41
|
+
# a path with a slash appended.
|
|
42
|
+
if response.status_code == 404 and self.should_redirect_with_slash(request):
|
|
43
|
+
return self.response_redirect_class(self.get_full_path_with_slash(request))
|
|
44
|
+
|
|
45
|
+
# Add the Content-Length header to non-streaming responses if not
|
|
46
|
+
# already set.
|
|
47
|
+
if not response.streaming and not response.has_header("Content-Length"):
|
|
48
|
+
response.headers["Content-Length"] = str(len(response.content))
|
|
49
|
+
|
|
50
|
+
return response
|
|
51
|
+
|
|
52
|
+
def should_redirect_with_slash(self, request):
|
|
53
|
+
"""
|
|
54
|
+
Return True if settings.APPEND_SLASH is True and appending a slash to
|
|
55
|
+
the request path turns an invalid path into a valid one.
|
|
56
|
+
"""
|
|
57
|
+
if settings.APPEND_SLASH and not request.path_info.endswith("/"):
|
|
58
|
+
urlconf = getattr(request, "urlconf", None)
|
|
59
|
+
if not is_valid_path(request.path_info, urlconf):
|
|
60
|
+
match = is_valid_path("%s/" % request.path_info, urlconf)
|
|
61
|
+
if match:
|
|
62
|
+
view = match.func
|
|
63
|
+
return getattr(view, "should_append_slash", True)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def get_full_path_with_slash(self, request):
|
|
67
|
+
"""
|
|
68
|
+
Return the full path of the request with a trailing slash appended.
|
|
69
|
+
|
|
70
|
+
Raise a RuntimeError if settings.DEBUG is True and request.method is
|
|
71
|
+
POST, PUT, or PATCH.
|
|
72
|
+
"""
|
|
73
|
+
new_path = request.get_full_path(force_append_slash=True)
|
|
74
|
+
# Prevent construction of scheme relative urls.
|
|
75
|
+
new_path = escape_leading_slashes(new_path)
|
|
76
|
+
if settings.DEBUG and request.method in ("POST", "PUT", "PATCH"):
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
"You called this URL via {method}, but the URL doesn't end "
|
|
79
|
+
"in a slash and you have APPEND_SLASH set. Plain can't "
|
|
80
|
+
"redirect to the slash URL while maintaining {method} data. "
|
|
81
|
+
"Change your form to point to {url} (note the trailing "
|
|
82
|
+
"slash), or set APPEND_SLASH=False in your Plain settings.".format(
|
|
83
|
+
method=request.method,
|
|
84
|
+
url=request.get_host() + new_path,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return new_path
|
plain/middleware/gzip.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from plain.utils.cache import patch_vary_headers
|
|
2
|
+
from plain.utils.regex_helper import _lazy_re_compile
|
|
3
|
+
from plain.utils.text import compress_sequence, compress_string
|
|
4
|
+
|
|
5
|
+
re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GZipMiddleware:
|
|
9
|
+
"""
|
|
10
|
+
Compress content if the browser allows gzip compression.
|
|
11
|
+
Set the Vary header accordingly, so that caches will base their storage
|
|
12
|
+
on the Accept-Encoding header.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
max_random_bytes = 100
|
|
16
|
+
|
|
17
|
+
def __init__(self, get_response):
|
|
18
|
+
self.get_response = get_response
|
|
19
|
+
|
|
20
|
+
def __call__(self, request):
|
|
21
|
+
response = self.get_response(request)
|
|
22
|
+
|
|
23
|
+
# It's not worth attempting to compress really short responses.
|
|
24
|
+
if not response.streaming and len(response.content) < 200:
|
|
25
|
+
return response
|
|
26
|
+
|
|
27
|
+
# Avoid gzipping if we've already got a content-encoding.
|
|
28
|
+
if response.has_header("Content-Encoding"):
|
|
29
|
+
return response
|
|
30
|
+
|
|
31
|
+
patch_vary_headers(response, ("Accept-Encoding",))
|
|
32
|
+
|
|
33
|
+
ae = request.META.get("HTTP_ACCEPT_ENCODING", "")
|
|
34
|
+
if not re_accepts_gzip.search(ae):
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
if response.streaming:
|
|
38
|
+
response.streaming_content = compress_sequence(
|
|
39
|
+
response.streaming_content,
|
|
40
|
+
max_random_bytes=self.max_random_bytes,
|
|
41
|
+
)
|
|
42
|
+
# Delete the `Content-Length` header for streaming content, because
|
|
43
|
+
# we won't know the compressed size until we stream it.
|
|
44
|
+
del response.headers["Content-Length"]
|
|
45
|
+
else:
|
|
46
|
+
# Return the compressed content only if it's actually shorter.
|
|
47
|
+
compressed_content = compress_string(
|
|
48
|
+
response.content,
|
|
49
|
+
max_random_bytes=self.max_random_bytes,
|
|
50
|
+
)
|
|
51
|
+
if len(compressed_content) >= len(response.content):
|
|
52
|
+
return response
|
|
53
|
+
response.content = compressed_content
|
|
54
|
+
response.headers["Content-Length"] = str(len(response.content))
|
|
55
|
+
|
|
56
|
+
# If there is a strong ETag, make it weak to fulfill the requirements
|
|
57
|
+
# of RFC 9110 Section 8.8.1 while also allowing conditional request
|
|
58
|
+
# matches on ETags.
|
|
59
|
+
etag = response.get("ETag")
|
|
60
|
+
if etag and etag.startswith('"'):
|
|
61
|
+
response.headers["ETag"] = "W/" + etag
|
|
62
|
+
response.headers["Content-Encoding"] = "gzip"
|
|
63
|
+
|
|
64
|
+
return response
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from plain.http import ResponsePermanentRedirect
|
|
4
|
+
from plain.runtime import settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SecurityMiddleware:
|
|
8
|
+
def __init__(self, get_response):
|
|
9
|
+
self.get_response = get_response
|
|
10
|
+
self.sts_seconds = settings.SECURE_HSTS_SECONDS
|
|
11
|
+
self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
|
|
12
|
+
self.sts_preload = settings.SECURE_HSTS_PRELOAD
|
|
13
|
+
self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
|
|
14
|
+
self.redirect = settings.SECURE_SSL_REDIRECT
|
|
15
|
+
self.redirect_host = settings.SECURE_SSL_HOST
|
|
16
|
+
self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
|
|
17
|
+
self.referrer_policy = settings.SECURE_REFERRER_POLICY
|
|
18
|
+
self.cross_origin_opener_policy = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
|
|
19
|
+
|
|
20
|
+
def __call__(self, request):
|
|
21
|
+
path = request.path.lstrip("/")
|
|
22
|
+
if (
|
|
23
|
+
self.redirect
|
|
24
|
+
and not request.is_secure()
|
|
25
|
+
and not any(pattern.search(path) for pattern in self.redirect_exempt)
|
|
26
|
+
):
|
|
27
|
+
host = self.redirect_host or request.get_host()
|
|
28
|
+
return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
|
|
29
|
+
|
|
30
|
+
response = self.get_response(request)
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
self.sts_seconds
|
|
34
|
+
and request.is_secure()
|
|
35
|
+
and "Strict-Transport-Security" not in response
|
|
36
|
+
):
|
|
37
|
+
sts_header = "max-age=%s" % self.sts_seconds
|
|
38
|
+
if self.sts_include_subdomains:
|
|
39
|
+
sts_header += "; includeSubDomains"
|
|
40
|
+
if self.sts_preload:
|
|
41
|
+
sts_header += "; preload"
|
|
42
|
+
response.headers["Strict-Transport-Security"] = sts_header
|
|
43
|
+
|
|
44
|
+
if self.content_type_nosniff:
|
|
45
|
+
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
46
|
+
|
|
47
|
+
if self.referrer_policy:
|
|
48
|
+
# Support a comma-separated string or iterable of values to allow
|
|
49
|
+
# fallback.
|
|
50
|
+
response.headers.setdefault(
|
|
51
|
+
"Referrer-Policy",
|
|
52
|
+
",".join(
|
|
53
|
+
[v.strip() for v in self.referrer_policy.split(",")]
|
|
54
|
+
if isinstance(self.referrer_policy, str)
|
|
55
|
+
else self.referrer_policy
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if self.cross_origin_opener_policy:
|
|
60
|
+
response.setdefault(
|
|
61
|
+
"Cross-Origin-Opener-Policy",
|
|
62
|
+
self.cross_origin_opener_policy,
|
|
63
|
+
)
|
|
64
|
+
return response
|
plain/packages/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Packages
|
|
2
|
+
|
|
3
|
+
Create app-packages and install third-party packages from PyPI.
|
|
4
|
+
|
|
5
|
+
Like Django, Plain is heavily dependent on the concept of "packages".
|
|
6
|
+
|
|
7
|
+
In your `settings.py`, you define which packages are enabled with the `INSTALLED_PACKAGES` setting.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# app/settings.py
|
|
11
|
+
INSTALLED_PACKAGES = [
|
|
12
|
+
|
|
13
|
+
]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
These can refer to third-party packages from PyPI (after you've installed them with `pip`),
|
|
17
|
+
or they can refer to packages that you've written yourself.
|
|
18
|
+
|
|
19
|
+
- some packages don't need to be installed as packages, but most do (and should specify)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Your own packages
|
|
23
|
+
|
|
24
|
+
- naming examples
|
|
25
|
+
- startapp
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## App settings
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# <app>/default_settings.py
|
|
32
|
+
EXAMPLE_SETTING: str = "example"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# <app>/models.py
|
|
37
|
+
from plain.runtime import settings
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
print(settings.EXAMPLE_SETTING)
|
|
41
|
+
```
|