plain 0.83.0__tar.gz → 0.84.0__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.
Potentially problematic release.
This version of plain might be problematic. Click here for more details.
- {plain-0.83.0 → plain-0.84.0}/PKG-INFO +1 -1
- {plain-0.83.0 → plain-0.84.0}/plain/CHANGELOG.md +28 -0
- plain-0.84.0/plain/http/README.md +142 -0
- plain-0.84.0/plain/internal/middleware/headers.py +59 -0
- {plain-0.83.0 → plain-0.84.0}/plain/runtime/global_settings.py +6 -2
- {plain-0.83.0 → plain-0.84.0}/pyproject.toml +1 -1
- plain-0.83.0/plain/http/README.md +0 -89
- plain-0.83.0/plain/internal/middleware/headers.py +0 -37
- {plain-0.83.0 → plain-0.84.0}/.gitignore +0 -0
- {plain-0.83.0 → plain-0.84.0}/LICENSE +0 -0
- {plain-0.83.0 → plain-0.84.0}/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/AGENTS.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/__main__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/compile.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/finders.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/urls.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/assets/views.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/chores/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/chores/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/chores/core.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/chores/registry.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/docs.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/md.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/request.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/build.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/changelog.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/chores.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/core.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/docs.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/formatting.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/install.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/output.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/preflight.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/print.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/registry.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/runtime.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/scaffold.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/server.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/settings.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/shell.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/startup.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/upgrade.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/urls.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/cli/utils.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/csrf/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/csrf/middleware.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/debug.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/exceptions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/boundfield.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/exceptions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/fields.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/forms/forms.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/cookie.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/middleware.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/multipartparser.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/request.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/http/response.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/base.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/locks.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/move.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/temp.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/files/utils.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/base.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/exception.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/wsgi.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/hosts.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/internal/reloader.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/json.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/configure.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/debug.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/formatters.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/logs/loggers.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/packages/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/packages/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/packages/config.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/packages/registry.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/paginator.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/checks.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/files.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/registry.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/results.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/security.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/preflight/urls.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/runtime/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/runtime/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/runtime/utils.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/LICENSE +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/app.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/arbiter.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/config.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/errors.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/glogging.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/body.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/errors.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/message.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/parser.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/unreader.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/http/wsgi.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/pidfile.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/sock.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/util.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/workers/base.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/workers/sync.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/workers/thread.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signals/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signals/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/signing.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/core.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/test/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/test/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/test/client.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/test/encoding.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/test/exceptions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/converters.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/exceptions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/patterns.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/resolvers.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/routers.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/urls/utils.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/cache.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/crypto.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/datastructures.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/dateparse.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/decorators.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/duration.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/encoding.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/functional.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/hashable.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/html.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/http.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/inspect.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/ipv6.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/itercompat.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/module_loading.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/safestring.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/text.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/timesince.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/timezone.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/utils/tree.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/validators.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/README.md +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/base.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/errors.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/exceptions.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/forms.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/objects.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/redirect.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/views/templates.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/plain/wsgi.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/.gitignore +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/app/.gitignore +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/app/settings.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/app/test/__init__.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/app/urls.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/conftest.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_cli.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_csrf.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_http_hosts.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_logs.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_runtime.py +0 -0
- {plain-0.83.0 → plain-0.84.0}/tests/test_wsgi.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.84.0](https://github.com/dropseed/plain/releases/plain@0.84.0) (2025-10-29)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- The `DEFAULT_RESPONSE_HEADERS` setting now supports format string placeholders (e.g., `{request.csp_nonce}`) for dynamic header values instead of requiring a callable function ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
8
|
+
- Views can now set headers to `None` to explicitly remove default response headers ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
9
|
+
- Added comprehensive documentation for customizing default response headers including override, remove, and extend patterns ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
|
|
10
|
+
|
|
11
|
+
### Upgrade instructions
|
|
12
|
+
|
|
13
|
+
- If you have `DEFAULT_RESPONSE_HEADERS` configured as a callable function, convert it to a dictionary with format string placeholders:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# Before:
|
|
17
|
+
def DEFAULT_RESPONSE_HEADERS(request):
|
|
18
|
+
nonce = request.csp_nonce
|
|
19
|
+
return {
|
|
20
|
+
"Content-Security-Policy": f"script-src 'self' 'nonce-{nonce}'",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# After:
|
|
24
|
+
DEFAULT_RESPONSE_HEADERS = {
|
|
25
|
+
"Content-Security-Policy": "script-src 'self' 'nonce-{request.csp_nonce}'",
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- If you were overriding default headers to empty strings (`""`) to remove them, change those to `None` instead
|
|
30
|
+
|
|
3
31
|
## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
|
|
4
32
|
|
|
5
33
|
### What's changed
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# HTTP
|
|
2
|
+
|
|
3
|
+
**HTTP request and response handling.**
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Content Security Policy (CSP)](#content-security-policy-csp)
|
|
7
|
+
- [Customizing Default Response Headers](#customizing-default-response-headers)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from plain.views import View
|
|
15
|
+
from plain.http import Response
|
|
16
|
+
|
|
17
|
+
class ExampleView(View):
|
|
18
|
+
def get(self):
|
|
19
|
+
# Accessing a request header
|
|
20
|
+
print(self.request.headers.get("Example-Header"))
|
|
21
|
+
|
|
22
|
+
# Accessing a query parameter
|
|
23
|
+
print(self.request.query_params.get("example"))
|
|
24
|
+
|
|
25
|
+
# Creating a response
|
|
26
|
+
response = Response("Hello, world!", status_code=200)
|
|
27
|
+
|
|
28
|
+
# Setting a response header
|
|
29
|
+
response.headers["Example-Header"] = "Example Value"
|
|
30
|
+
|
|
31
|
+
return response
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Content Security Policy (CSP)
|
|
35
|
+
|
|
36
|
+
Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
|
|
37
|
+
|
|
38
|
+
Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
|
|
39
|
+
|
|
40
|
+
### Configuring CSP Headers
|
|
41
|
+
|
|
42
|
+
Include CSP in `DEFAULT_RESPONSE_HEADERS` using `{request.csp_nonce}` placeholders for dynamic nonces:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# app/settings.py
|
|
46
|
+
DEFAULT_RESPONSE_HEADERS = {
|
|
47
|
+
"Content-Security-Policy": (
|
|
48
|
+
"default-src 'self'; "
|
|
49
|
+
"script-src 'self' 'nonce-{request.csp_nonce}'; "
|
|
50
|
+
"style-src 'self' 'nonce-{request.csp_nonce}'; "
|
|
51
|
+
"img-src 'self' data:; "
|
|
52
|
+
"font-src 'self'; "
|
|
53
|
+
"connect-src 'self'; "
|
|
54
|
+
"frame-ancestors 'self'; "
|
|
55
|
+
"base-uri 'self'; "
|
|
56
|
+
"form-action 'self'"
|
|
57
|
+
),
|
|
58
|
+
# Other default headers...
|
|
59
|
+
"X-Frame-Options": "DENY",
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `{request.csp_nonce}` placeholder will be replaced with a unique nonce for each request.
|
|
64
|
+
|
|
65
|
+
Use tools like [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com/) to analyze your CSP policy and identify potential security issues or misconfigurations.
|
|
66
|
+
|
|
67
|
+
### Using Nonces in Templates
|
|
68
|
+
|
|
69
|
+
Add the nonce attribute to inline scripts and styles in your templates:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<!-- Inline script with nonce -->
|
|
73
|
+
<script nonce="{{ request.csp_nonce }}">
|
|
74
|
+
console.log("This script is allowed by CSP");
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<!-- Inline style with nonce -->
|
|
78
|
+
<style nonce="{{ request.csp_nonce }}">
|
|
79
|
+
.example { color: red; }
|
|
80
|
+
</style>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
External scripts and stylesheets loaded from `'self'` don't need nonces:
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<!-- External scripts/styles work with 'self' directive -->
|
|
87
|
+
<script src="/assets/app.js"></script>
|
|
88
|
+
<link rel="stylesheet" href="/assets/app.css">
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Customizing Default Response Headers
|
|
92
|
+
|
|
93
|
+
Plain applies default response headers to all responses via `DEFAULT_RESPONSE_HEADERS` in settings. Views can customize these headers in several ways:
|
|
94
|
+
|
|
95
|
+
### Override Default Headers
|
|
96
|
+
|
|
97
|
+
Set the header to a different value in your view:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
class MyView(View):
|
|
101
|
+
def get(self):
|
|
102
|
+
response = Response("content")
|
|
103
|
+
# Override the default X-Frame-Options: DENY
|
|
104
|
+
response.headers["X-Frame-Options"] = "SAMEORIGIN"
|
|
105
|
+
return response
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Remove Default Headers
|
|
109
|
+
|
|
110
|
+
Set the header to `None` to prevent it from being applied:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class EmbeddableView(View):
|
|
114
|
+
def get(self):
|
|
115
|
+
response = Response("content")
|
|
116
|
+
# Remove X-Frame-Options entirely to allow embedding
|
|
117
|
+
response.headers["X-Frame-Options"] = None
|
|
118
|
+
return response
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Extend Default Headers
|
|
122
|
+
|
|
123
|
+
Read the default value from settings, modify it, then set it in your view:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from plain.runtime import settings
|
|
127
|
+
|
|
128
|
+
class MyView(View):
|
|
129
|
+
def get(self):
|
|
130
|
+
response = Response("content")
|
|
131
|
+
|
|
132
|
+
# Get the default CSP policy
|
|
133
|
+
if csp := settings.DEFAULT_RESPONSE_HEADERS.get("Content-Security-Policy"):
|
|
134
|
+
# Format it with the current request to resolve placeholders
|
|
135
|
+
csp = csp.format(request=self.request)
|
|
136
|
+
# Extend with additional sources
|
|
137
|
+
response.headers["Content-Security-Policy"] = (
|
|
138
|
+
f"{csp}; script-src https://analytics.example.com"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return response
|
|
142
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from plain.http import HttpMiddleware
|
|
6
|
+
from plain.runtime import settings
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from plain.http import Request, Response
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DefaultHeadersMiddleware(HttpMiddleware):
|
|
13
|
+
"""
|
|
14
|
+
Applies default response headers from settings.DEFAULT_RESPONSE_HEADERS.
|
|
15
|
+
|
|
16
|
+
This middleware runs after the view executes and applies default headers
|
|
17
|
+
to the response using setdefault(), which means:
|
|
18
|
+
- Headers already set by the view won't be overridden
|
|
19
|
+
- Headers not set by the view will use the default value
|
|
20
|
+
|
|
21
|
+
View Customization Patterns:
|
|
22
|
+
- Use default: Don't set the header (middleware applies it)
|
|
23
|
+
- Override: Set the header to a different value
|
|
24
|
+
- Remove: Set the header to None (middleware will delete it)
|
|
25
|
+
- Extend: Read from settings.DEFAULT_RESPONSE_HEADERS, modify, then set
|
|
26
|
+
|
|
27
|
+
Format Strings:
|
|
28
|
+
Header values can include {request.attribute} placeholders for dynamic
|
|
29
|
+
content. Example: 'nonce-{request.csp_nonce}' will be formatted with
|
|
30
|
+
the request's csp_nonce value. Headers without placeholders are used as-is.
|
|
31
|
+
|
|
32
|
+
None Removal:
|
|
33
|
+
Views can set a header to None to opt-out of that default header entirely.
|
|
34
|
+
The middleware will delete any header set to None, preventing the default
|
|
35
|
+
from being applied.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def process_request(self, request: Request) -> Response:
|
|
39
|
+
# Get the response from the view (and any inner middleware)
|
|
40
|
+
response = self.get_response(request)
|
|
41
|
+
|
|
42
|
+
# Apply default headers to the response
|
|
43
|
+
for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
|
|
44
|
+
if header not in response.headers:
|
|
45
|
+
# Header not set - apply default
|
|
46
|
+
if "{" in value:
|
|
47
|
+
response.headers[header] = value.format(request=request)
|
|
48
|
+
else:
|
|
49
|
+
response.headers[header] = value
|
|
50
|
+
elif response.headers[header] is None:
|
|
51
|
+
# Header explicitly set to None by view - remove it
|
|
52
|
+
del response.headers[header]
|
|
53
|
+
|
|
54
|
+
# Add the Content-Length header to non-streaming responses if not
|
|
55
|
+
# already set.
|
|
56
|
+
if not response.streaming and "Content-Length" not in response.headers:
|
|
57
|
+
response.headers["Content-Length"] = str(len(response.content))
|
|
58
|
+
|
|
59
|
+
return response
|
|
@@ -27,8 +27,12 @@ URLS_ROUTER: str
|
|
|
27
27
|
ALLOWED_HOSTS: list[str] = []
|
|
28
28
|
|
|
29
29
|
# Default headers for all responses.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# Header values can include {request.attribute} placeholders for dynamic content.
|
|
31
|
+
# Example: "script-src 'nonce-{request.csp_nonce}'" will use the request's nonce.
|
|
32
|
+
# Views can override, remove, or extend these headers - see plain/http/README.md
|
|
33
|
+
# for customization patterns.
|
|
34
|
+
DEFAULT_RESPONSE_HEADERS: dict = {
|
|
35
|
+
# "Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-{request.csp_nonce}'",
|
|
32
36
|
# https://hstspreload.org/
|
|
33
37
|
# "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
34
38
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# HTTP
|
|
2
|
-
|
|
3
|
-
**HTTP request and response handling.**
|
|
4
|
-
|
|
5
|
-
- [Overview](#overview)
|
|
6
|
-
- [Content Security Policy (CSP)](#content-security-policy-csp)
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
|
|
11
|
-
|
|
12
|
-
```python
|
|
13
|
-
from plain.views import View
|
|
14
|
-
from plain.http import Response
|
|
15
|
-
|
|
16
|
-
class ExampleView(View):
|
|
17
|
-
def get(self):
|
|
18
|
-
# Accessing a request header
|
|
19
|
-
print(self.request.headers.get("Example-Header"))
|
|
20
|
-
|
|
21
|
-
# Accessing a query parameter
|
|
22
|
-
print(self.request.query_params.get("example"))
|
|
23
|
-
|
|
24
|
-
# Creating a response
|
|
25
|
-
response = Response("Hello, world!", status_code=200)
|
|
26
|
-
|
|
27
|
-
# Setting a response header
|
|
28
|
-
response.headers["Example-Header"] = "Example Value"
|
|
29
|
-
|
|
30
|
-
return response
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Content Security Policy (CSP)
|
|
34
|
-
|
|
35
|
-
Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
|
|
36
|
-
|
|
37
|
-
Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
|
|
38
|
-
|
|
39
|
-
### Configuring CSP Headers
|
|
40
|
-
|
|
41
|
-
Set `DEFAULT_RESPONSE_HEADERS` as a callable function to generate dynamic CSP headers with nonces:
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
# app/settings.py
|
|
45
|
-
def DEFAULT_RESPONSE_HEADERS(request):
|
|
46
|
-
"""
|
|
47
|
-
Dynamic response headers with CSP nonces.
|
|
48
|
-
"""
|
|
49
|
-
nonce = request.csp_nonce
|
|
50
|
-
return {
|
|
51
|
-
"Content-Security-Policy": (
|
|
52
|
-
f"default-src 'self'; "
|
|
53
|
-
f"script-src 'self' 'nonce-{nonce}'; "
|
|
54
|
-
f"style-src 'self' 'nonce-{nonce}'; "
|
|
55
|
-
f"img-src 'self' data:; "
|
|
56
|
-
f"font-src 'self'; "
|
|
57
|
-
f"connect-src 'self'; "
|
|
58
|
-
f"frame-ancestors 'self'; "
|
|
59
|
-
f"base-uri 'self'; "
|
|
60
|
-
f"form-action 'self'"
|
|
61
|
-
),
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Use tools like [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com/) to analyze your CSP policy and identify potential security issues or misconfigurations.
|
|
66
|
-
|
|
67
|
-
### Using Nonces in Templates
|
|
68
|
-
|
|
69
|
-
Add the nonce attribute to inline scripts and styles in your templates:
|
|
70
|
-
|
|
71
|
-
```html
|
|
72
|
-
<!-- Inline script with nonce -->
|
|
73
|
-
<script nonce="{{ request.csp_nonce }}">
|
|
74
|
-
console.log("This script is allowed by CSP");
|
|
75
|
-
</script>
|
|
76
|
-
|
|
77
|
-
<!-- Inline style with nonce -->
|
|
78
|
-
<style nonce="{{ request.csp_nonce }}">
|
|
79
|
-
.example { color: red; }
|
|
80
|
-
</style>
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
External scripts and stylesheets loaded from `'self'` don't need nonces:
|
|
84
|
-
|
|
85
|
-
```html
|
|
86
|
-
<!-- External scripts/styles work with 'self' directive -->
|
|
87
|
-
<script src="/assets/app.js"></script>
|
|
88
|
-
<link rel="stylesheet" href="/assets/app.css">
|
|
89
|
-
```
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
from plain.http import HttpMiddleware
|
|
6
|
-
from plain.runtime import settings
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from plain.http import Request, Response
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class DefaultHeadersMiddleware(HttpMiddleware):
|
|
13
|
-
def process_request(self, request: Request) -> Response:
|
|
14
|
-
response = self.get_response(request)
|
|
15
|
-
|
|
16
|
-
# Support callable DEFAULT_RESPONSE_HEADERS for dynamic header generation
|
|
17
|
-
# (e.g., CSP nonces that change per request)
|
|
18
|
-
if callable(settings.DEFAULT_RESPONSE_HEADERS):
|
|
19
|
-
default_headers = settings.DEFAULT_RESPONSE_HEADERS(request)
|
|
20
|
-
else:
|
|
21
|
-
default_headers = settings.DEFAULT_RESPONSE_HEADERS
|
|
22
|
-
|
|
23
|
-
for header, value in default_headers.items():
|
|
24
|
-
# Since we don't have a good way to *remove* default response headers,
|
|
25
|
-
# use allow users to set them to an empty string to indicate they should be removed.
|
|
26
|
-
if header in response.headers and response.headers[header] == "":
|
|
27
|
-
del response.headers[header]
|
|
28
|
-
continue
|
|
29
|
-
|
|
30
|
-
response.headers.setdefault(header, value)
|
|
31
|
-
|
|
32
|
-
# Add the Content-Length header to non-streaming responses if not
|
|
33
|
-
# already set.
|
|
34
|
-
if not response.streaming and "Content-Length" not in response.headers:
|
|
35
|
-
response.headers["Content-Length"] = str(len(response.content))
|
|
36
|
-
|
|
37
|
-
return response
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|