plain 0.83.0__py3-none-any.whl → 0.84.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.
Potentially problematic release.
This version of plain might be problematic. Click here for more details.
- plain/CHANGELOG.md +28 -0
- plain/http/README.md +72 -19
- plain/internal/middleware/headers.py +36 -14
- plain/runtime/global_settings.py +6 -2
- {plain-0.83.0.dist-info → plain-0.84.0.dist-info}/METADATA +1 -1
- {plain-0.83.0.dist-info → plain-0.84.0.dist-info}/RECORD +9 -9
- {plain-0.83.0.dist-info → plain-0.84.0.dist-info}/WHEEL +0 -0
- {plain-0.83.0.dist-info → plain-0.84.0.dist-info}/entry_points.txt +0 -0
- {plain-0.83.0.dist-info → plain-0.84.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
|
@@ -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
|
plain/http/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
6
|
- [Content Security Policy (CSP)](#content-security-policy-csp)
|
|
7
|
+
- [Customizing Default Response Headers](#customizing-default-response-headers)
|
|
7
8
|
|
|
8
9
|
## Overview
|
|
9
10
|
|
|
@@ -38,30 +39,29 @@ Each request generates a unique cryptographically secure nonce available via [`r
|
|
|
38
39
|
|
|
39
40
|
### Configuring CSP Headers
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
Include CSP in `DEFAULT_RESPONSE_HEADERS` using `{request.csp_nonce}` placeholders for dynamic nonces:
|
|
42
43
|
|
|
43
44
|
```python
|
|
44
45
|
# app/settings.py
|
|
45
|
-
|
|
46
|
-
""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
f"form-action 'self'"
|
|
61
|
-
),
|
|
62
|
-
}
|
|
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
|
+
}
|
|
63
61
|
```
|
|
64
62
|
|
|
63
|
+
The `{request.csp_nonce}` placeholder will be replaced with a unique nonce for each request.
|
|
64
|
+
|
|
65
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
66
|
|
|
67
67
|
### Using Nonces in Templates
|
|
@@ -87,3 +87,56 @@ External scripts and stylesheets loaded from `'self'` don't need nonces:
|
|
|
87
87
|
<script src="/assets/app.js"></script>
|
|
88
88
|
<link rel="stylesheet" href="/assets/app.css">
|
|
89
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
|
+
```
|
|
@@ -10,24 +10,46 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
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
|
+
|
|
13
38
|
def process_request(self, request: Request) -> Response:
|
|
39
|
+
# Get the response from the view (and any inner middleware)
|
|
14
40
|
response = self.get_response(request)
|
|
15
41
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if header in response.headers and response.headers[header] == "":
|
|
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
|
|
27
52
|
del response.headers[header]
|
|
28
|
-
continue
|
|
29
|
-
|
|
30
|
-
response.headers.setdefault(header, value)
|
|
31
53
|
|
|
32
54
|
# Add the Content-Length header to non-streaming responses if not
|
|
33
55
|
# already set.
|
plain/runtime/global_settings.py
CHANGED
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
|
|
2
|
-
plain/CHANGELOG.md,sha256=
|
|
2
|
+
plain/CHANGELOG.md,sha256=wZ16yVxfhEgl-6T5SGrjiEqMtie_jZqTPd6bc7smcAI,35041
|
|
3
3
|
plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
|
|
4
4
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
|
5
5
|
plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
|
|
@@ -56,7 +56,7 @@ plain/forms/boundfield.py,sha256=PEquPRn1BoVd4ZkIin8tjkBzRDMv-41wO8qHV0S1shg,196
|
|
|
56
56
|
plain/forms/exceptions.py,sha256=MuMUF-33Qsc_HWk5zI3rcWzdvXzQbEOKAZvJKkbrL58,320
|
|
57
57
|
plain/forms/fields.py,sha256=GQSTI6-eaHivIXj3TQSw2LpyWMbN0NZ-SHjUO_oxqeA,37132
|
|
58
58
|
plain/forms/forms.py,sha256=GnJWzjIXOPByfrdiqjjo4xdKdkBw1gBD6Hq4mg73gHQ,11259
|
|
59
|
-
plain/http/README.md,sha256=
|
|
59
|
+
plain/http/README.md,sha256=HP0m45CKhtA5Jmc65VhMZCYEH9ATK7BfBvkHwUASnwA,4221
|
|
60
60
|
plain/http/__init__.py,sha256=gUTIGh-GbSIlh3SP-Db-XAOX4P_j2hh2445w8Cm-zKQ,1032
|
|
61
61
|
plain/http/cookie.py,sha256=x13G3LIr0jxnPK1NQRptmi0DrAq9PsivQnQTm4LKaW0,2191
|
|
62
62
|
plain/http/middleware.py,sha256=TPs585IIFjgp-5uUAJtIoigH6uwTS3FJqwFSsQdayd4,960
|
|
@@ -78,7 +78,7 @@ plain/internal/handlers/base.py,sha256=ZlpOYqd45X0wPYlmxHwXR5kyCkWBB6ZJeSGZka5VA
|
|
|
78
78
|
plain/internal/handlers/exception.py,sha256=P7oTqVtKoIHBOxCml1BSgNBV_rQWyCHlO5hS87NVjXo,4872
|
|
79
79
|
plain/internal/handlers/wsgi.py,sha256=d2Hcs4fzTSir6OvtpVromTw0RmmFAqTL4_EaBjoHtNU,8919
|
|
80
80
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
|
-
plain/internal/middleware/headers.py,sha256=
|
|
81
|
+
plain/internal/middleware/headers.py,sha256=fX8iMOeecqSyI7y-IIQ_w4OUFRJpgMvUr_xtUzI7IUQ,2361
|
|
82
82
|
plain/internal/middleware/hosts.py,sha256=UXMts7cKA9ocOK-J8DAcZPbKYQtfVyLSLH2QQX3mKjM,5895
|
|
83
83
|
plain/internal/middleware/https.py,sha256=8n990O0Ej5cRrBlfAnLW2nZ2mx0wn0002C4HuRhVpsM,1117
|
|
84
84
|
plain/internal/middleware/slash.py,sha256=V_rTb9v8uMUR_N2TLETBwlJFItoL4WD9JnRyFaFs878,3073
|
|
@@ -102,7 +102,7 @@ plain/preflight/security.py,sha256=nMbflO2LN49oKAAaa-tYvuweNB0RWv1z3w9niU037n0,3
|
|
|
102
102
|
plain/preflight/urls.py,sha256=Asw_vq-70NRqr15yuBAYL0JCZ04liumORYT3I3KmF_k,437
|
|
103
103
|
plain/runtime/README.md,sha256=ZZ3NPTjtxwyfw1V827YNwkWc8MiH5rWiy27HrN13qZ8,4819
|
|
104
104
|
plain/runtime/__init__.py,sha256=dvF5ipRckVf6LQgY4kdxE_dTlCdncuawQf3o5EqLq9k,2524
|
|
105
|
-
plain/runtime/global_settings.py,sha256=
|
|
105
|
+
plain/runtime/global_settings.py,sha256=BVkLDWA2i4kbtjQabFdJ4cYhVrt-mU8UqZdBRz7cbwU,5741
|
|
106
106
|
plain/runtime/user_settings.py,sha256=Tbd0J6bxp18tKmFsQcdlxeFhUQU68PYtsswzQ2IcfNc,11467
|
|
107
107
|
plain/runtime/utils.py,sha256=sHOv9SWCalBtg32GtZofimM2XaQtf_jAyyf6RQuOlGc,851
|
|
108
108
|
plain/server/LICENSE,sha256=Xt_dw4qYwQI9qSi2u8yMZeb4HuMRp5tESRKhtvvJBgA,1707
|
|
@@ -188,8 +188,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
|
|
188
188
|
plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
|
|
189
189
|
plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
|
|
190
190
|
plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
|
|
191
|
-
plain-0.
|
|
192
|
-
plain-0.
|
|
193
|
-
plain-0.
|
|
194
|
-
plain-0.
|
|
195
|
-
plain-0.
|
|
191
|
+
plain-0.84.0.dist-info/METADATA,sha256=UQ9z1_Z0yZvZ2Gj5xnSE75cfoPSH4Ia05Rc3NKaVFng,4516
|
|
192
|
+
plain-0.84.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
193
|
+
plain-0.84.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
|
|
194
|
+
plain-0.84.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
|
195
|
+
plain-0.84.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|