plain 0.65.1__py3-none-any.whl → 0.66.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/CHANGELOG.md +14 -0
- plain/csrf/middleware.py +5 -6
- plain/exceptions.py +0 -6
- plain/http/request.py +17 -35
- plain/internal/handlers/base.py +2 -5
- plain/{http → internal/middleware}/hosts.py +59 -13
- plain/internal/middleware/https.py +1 -2
- plain/internal/middleware/slash.py +1 -1
- plain/runtime/global_settings.py +1 -1
- {plain-0.65.1.dist-info → plain-0.66.0.dist-info}/METADATA +1 -1
- {plain-0.65.1.dist-info → plain-0.66.0.dist-info}/RECORD +14 -14
- {plain-0.65.1.dist-info → plain-0.66.0.dist-info}/WHEEL +0 -0
- {plain-0.65.1.dist-info → plain-0.66.0.dist-info}/entry_points.txt +0 -0
- {plain-0.65.1.dist-info → plain-0.66.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.66.0](https://github.com/dropseed/plain/releases/plain@0.66.0) (2025-09-22)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Host validation moved to dedicated middleware and `ALLOWED_HOSTS` setting is now required ([6a4b7be](https://github.com/dropseed/plain/commit/6a4b7be220))
|
8
|
+
- Changed `request.get_port()` method to `request.port` cached property ([544f3e1](https://github.com/dropseed/plain/commit/544f3e19f8))
|
9
|
+
- Removed internal `request._get_full_path()` method ([50cdb58](https://github.com/dropseed/plain/commit/50cdb58d4e))
|
10
|
+
|
11
|
+
### Upgrade instructions
|
12
|
+
|
13
|
+
- Add `ALLOWED_HOSTS` setting to your configuration if not already present (required for host validation)
|
14
|
+
- Replace any usage of `request.get_host()` with `request.host`
|
15
|
+
- Replace any usage of `request.get_port()` with `request.port`
|
16
|
+
|
3
17
|
## [0.65.1](https://github.com/dropseed/plain/releases/plain@0.65.1) (2025-09-22)
|
4
18
|
|
5
19
|
### What's changed
|
plain/csrf/middleware.py
CHANGED
@@ -2,7 +2,6 @@ import logging
|
|
2
2
|
import re
|
3
3
|
from urllib.parse import urlparse
|
4
4
|
|
5
|
-
from plain.exceptions import DisallowedHost
|
6
5
|
from plain.logs.utils import log_response
|
7
6
|
from plain.runtime import settings
|
8
7
|
|
@@ -81,8 +80,8 @@ class CsrfViewMiddleware:
|
|
81
80
|
if origin == "null":
|
82
81
|
return False, "Cross-origin request detected - null Origin header"
|
83
82
|
|
84
|
-
|
85
|
-
|
83
|
+
if (parsed_origin := urlparse(origin)) and (host := request.host):
|
84
|
+
try:
|
86
85
|
# Scheme-agnostic host:port comparison
|
87
86
|
origin_host = parsed_origin.hostname
|
88
87
|
origin_port = parsed_origin.port or (
|
@@ -97,7 +96,7 @@ class CsrfViewMiddleware:
|
|
97
96
|
# Use a fake scheme since we only care about host parsing
|
98
97
|
parsed_host = urlparse(f"http://{host}")
|
99
98
|
request_host = parsed_host.hostname or host
|
100
|
-
request_port = request.
|
99
|
+
request_port = request.port
|
101
100
|
|
102
101
|
# Compare hostname and port (scheme-agnostic)
|
103
102
|
# Both origin_host and request_host are normalized by urlparse (IPv6 brackets stripped)
|
@@ -110,8 +109,8 @@ class CsrfViewMiddleware:
|
|
110
109
|
True,
|
111
110
|
f"Same-origin request - Origin {origin} matches Host {host}",
|
112
111
|
)
|
113
|
-
|
114
|
-
|
112
|
+
except ValueError:
|
113
|
+
pass
|
115
114
|
|
116
115
|
# Origin present but doesn't match host
|
117
116
|
return (
|
plain/exceptions.py
CHANGED
@@ -67,12 +67,6 @@ class SuspiciousFileOperation(SuspiciousOperation):
|
|
67
67
|
pass
|
68
68
|
|
69
69
|
|
70
|
-
class DisallowedHost(SuspiciousOperation):
|
71
|
-
"""HTTP_HOST header contains invalid value"""
|
72
|
-
|
73
|
-
pass
|
74
|
-
|
75
|
-
|
76
70
|
class TooManyFieldsSent(SuspiciousOperation):
|
77
71
|
"""
|
78
72
|
The number of fields in a GET or POST request exceeded
|
plain/http/request.py
CHANGED
@@ -8,7 +8,6 @@ from itertools import chain
|
|
8
8
|
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
|
9
9
|
|
10
10
|
from plain.exceptions import (
|
11
|
-
DisallowedHost,
|
12
11
|
ImproperlyConfigured,
|
13
12
|
RequestDataTooBig,
|
14
13
|
TooManyFieldsSent,
|
@@ -29,8 +28,6 @@ from plain.utils.datastructures import (
|
|
29
28
|
from plain.utils.encoding import iri_to_uri
|
30
29
|
from plain.utils.http import parse_header_parameters
|
31
30
|
|
32
|
-
from .hosts import split_domain_port, validate_host
|
33
|
-
|
34
31
|
|
35
32
|
class UnreadablePostError(OSError):
|
36
33
|
pass
|
@@ -123,10 +120,13 @@ class HttpRequest:
|
|
123
120
|
else:
|
124
121
|
self.encoding = self.content_params["charset"]
|
125
122
|
|
126
|
-
|
123
|
+
@cached_property
|
124
|
+
def host(self):
|
127
125
|
"""
|
128
|
-
Return the HTTP host using the environment or request headers.
|
129
|
-
|
126
|
+
Return the HTTP host using the environment or request headers.
|
127
|
+
|
128
|
+
Host validation is performed by HostValidationMiddleware, so this
|
129
|
+
property can safely return the host without any validation.
|
130
130
|
"""
|
131
131
|
# We try three options, in order of decreasing preference.
|
132
132
|
if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.meta):
|
@@ -136,34 +136,13 @@ class HttpRequest:
|
|
136
136
|
else:
|
137
137
|
# Reconstruct the host using the algorithm from PEP 333.
|
138
138
|
host = self.meta["SERVER_NAME"]
|
139
|
-
server_port = self.
|
139
|
+
server_port = self.port
|
140
140
|
if server_port != ("443" if self.is_https() else "80"):
|
141
141
|
host = f"{host}:{server_port}"
|
142
142
|
return host
|
143
143
|
|
144
|
-
|
145
|
-
|
146
|
-
host = self._get_raw_host()
|
147
|
-
|
148
|
-
# Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
|
149
|
-
allowed_hosts = settings.ALLOWED_HOSTS
|
150
|
-
if settings.DEBUG and not allowed_hosts:
|
151
|
-
allowed_hosts = [".localhost", "127.0.0.1", "[::1]"]
|
152
|
-
|
153
|
-
domain, port = split_domain_port(host)
|
154
|
-
if domain and validate_host(domain, allowed_hosts):
|
155
|
-
return host
|
156
|
-
else:
|
157
|
-
msg = f"Invalid HTTP_HOST header: {host!r}."
|
158
|
-
if domain:
|
159
|
-
msg += f" You may need to add {domain!r} to ALLOWED_HOSTS."
|
160
|
-
else:
|
161
|
-
msg += (
|
162
|
-
" The domain name provided is not valid according to RFC 1034/1035."
|
163
|
-
)
|
164
|
-
raise DisallowedHost(msg)
|
165
|
-
|
166
|
-
def get_port(self):
|
144
|
+
@cached_property
|
145
|
+
def port(self):
|
167
146
|
"""Return the port number for the request as a string."""
|
168
147
|
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.meta:
|
169
148
|
port = self.meta["HTTP_X_FORWARDED_PORT"]
|
@@ -172,9 +151,12 @@ class HttpRequest:
|
|
172
151
|
return str(port)
|
173
152
|
|
174
153
|
def get_full_path(self, force_append_slash=False):
|
175
|
-
|
154
|
+
"""
|
155
|
+
Return the full path for the request, including query string.
|
176
156
|
|
177
|
-
|
157
|
+
If force_append_slash is True, append a trailing slash if the path
|
158
|
+
doesn't already end with one.
|
159
|
+
"""
|
178
160
|
# RFC 3986 requires query string arguments to be in the ASCII range.
|
179
161
|
# Rather than crash if this doesn't happen, we encode defensively.
|
180
162
|
|
@@ -195,8 +177,8 @@ class HttpRequest:
|
|
195
177
|
return quote(path, safe="/:@&+$,-_.!~*'()")
|
196
178
|
|
197
179
|
return "{}{}{}".format(
|
198
|
-
escape_uri_path(path),
|
199
|
-
"/" if force_append_slash and not path.endswith("/") else "",
|
180
|
+
escape_uri_path(self.path),
|
181
|
+
"/" if force_append_slash and not self.path.endswith("/") else "",
|
200
182
|
("?" + iri_to_uri(self.meta.get("QUERY_STRING", "")))
|
201
183
|
if self.meta.get("QUERY_STRING", "")
|
202
184
|
else "",
|
@@ -220,7 +202,7 @@ class HttpRequest:
|
|
220
202
|
location = str(location)
|
221
203
|
bits = urlsplit(location)
|
222
204
|
if not (bits.scheme and bits.netloc):
|
223
|
-
current_scheme_host = f"{self.scheme}://{self.
|
205
|
+
current_scheme_host = f"{self.scheme}://{self.host}"
|
224
206
|
|
225
207
|
# Handle the simple, most common case. If the location is absolute
|
226
208
|
# and a scheme or host (netloc) isn't provided, skip an expensive
|
plain/internal/handlers/base.py
CHANGED
@@ -4,7 +4,7 @@ import types
|
|
4
4
|
from opentelemetry import baggage, trace
|
5
5
|
from opentelemetry.semconv.attributes import http_attributes, url_attributes
|
6
6
|
|
7
|
-
from plain.exceptions import
|
7
|
+
from plain.exceptions import ImproperlyConfigured
|
8
8
|
from plain.logs.utils import log_response
|
9
9
|
from plain.runtime import settings
|
10
10
|
from plain.urls import get_resolver
|
@@ -17,6 +17,7 @@ logger = logging.getLogger("plain.request")
|
|
17
17
|
|
18
18
|
# These middleware classes are always used by Plain.
|
19
19
|
BUILTIN_BEFORE_MIDDLEWARE = [
|
20
|
+
"plain.internal.middleware.hosts.HostValidationMiddleware", # Validate Host header first
|
20
21
|
"plain.internal.middleware.headers.DefaultHeadersMiddleware", # Runs after response, to set missing headers
|
21
22
|
"plain.internal.middleware.https.HttpsRedirectMiddleware", # Runs before response, to redirect to HTTPS quickly
|
22
23
|
"plain.csrf.middleware.CsrfViewMiddleware", # Runs before and after get_response...
|
@@ -78,10 +79,6 @@ class BaseHandler:
|
|
78
79
|
except KeyError:
|
79
80
|
# Missing required WSGI environment variables (e.g. in tests)
|
80
81
|
pass
|
81
|
-
except DisallowedHost:
|
82
|
-
# Invalid host header - skip URL_FULL for telemetry but let the
|
83
|
-
# exception be handled normally by middleware for proper 400 response
|
84
|
-
pass
|
85
82
|
|
86
83
|
# Add query string if present
|
87
84
|
if query_string := request.meta.get("QUERY_STRING"):
|
@@ -1,19 +1,65 @@
|
|
1
|
-
"""
|
2
|
-
Host validation utilities for ALLOWED_HOSTS functionality.
|
3
|
-
|
4
|
-
This module provides functions for validating hosts against allowed patterns,
|
5
|
-
including domain patterns, wildcards, and CIDR notation for IP ranges.
|
6
|
-
"""
|
7
|
-
|
8
1
|
import ipaddress
|
2
|
+
import logging
|
9
3
|
|
4
|
+
from plain.http import HttpRequest, ResponseBadRequest
|
5
|
+
from plain.runtime import settings
|
10
6
|
from plain.utils.regex_helper import _lazy_re_compile
|
11
7
|
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
12
10
|
host_validation_re = _lazy_re_compile(
|
13
11
|
r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(:[0-9]+)?$"
|
14
12
|
)
|
15
13
|
|
16
14
|
|
15
|
+
class HostValidationMiddleware:
|
16
|
+
"""
|
17
|
+
Middleware to validate the Host header against ALLOWED_HOSTS.
|
18
|
+
|
19
|
+
This middleware should run first to ensure all subsequent code can trust
|
20
|
+
that the host header is valid. Returns a 400 Bad Request response if the
|
21
|
+
host is not allowed.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, get_response):
|
25
|
+
self.get_response = get_response
|
26
|
+
|
27
|
+
def __call__(self, request):
|
28
|
+
if not is_host_valid(request):
|
29
|
+
host = request.host
|
30
|
+
msg = f"Invalid HTTP_HOST header: {host!r}."
|
31
|
+
|
32
|
+
domain, _ = split_domain_port(host)
|
33
|
+
if domain:
|
34
|
+
msg += f" You may need to add {domain!r} to ALLOWED_HOSTS."
|
35
|
+
else:
|
36
|
+
msg += (
|
37
|
+
" The domain name provided is not valid according to RFC 1034/1035."
|
38
|
+
)
|
39
|
+
|
40
|
+
logger.warning(
|
41
|
+
msg,
|
42
|
+
extra={"status_code": 400, "request": request},
|
43
|
+
)
|
44
|
+
|
45
|
+
return ResponseBadRequest()
|
46
|
+
|
47
|
+
return self.get_response(request)
|
48
|
+
|
49
|
+
|
50
|
+
def is_host_valid(request: HttpRequest) -> bool:
|
51
|
+
"""
|
52
|
+
Check if the host is valid according to ALLOWED_HOSTS settings.
|
53
|
+
"""
|
54
|
+
allowed_hosts = settings.ALLOWED_HOSTS
|
55
|
+
|
56
|
+
if not allowed_hosts:
|
57
|
+
return True # Allow all hosts if ALLOWED_HOSTS is empty
|
58
|
+
|
59
|
+
domain, _ = split_domain_port(request.host)
|
60
|
+
return bool(domain) and validate_host(domain, allowed_hosts)
|
61
|
+
|
62
|
+
|
17
63
|
def split_domain_port(host: str) -> tuple[str, str]:
|
18
64
|
"""
|
19
65
|
Return a (domain, port) tuple from a given host.
|
@@ -36,7 +82,7 @@ def split_domain_port(host: str) -> tuple[str, str]:
|
|
36
82
|
return domain, port
|
37
83
|
|
38
84
|
|
39
|
-
def
|
85
|
+
def is_same_domain(host: str, pattern: str) -> bool:
|
40
86
|
"""
|
41
87
|
Return ``True`` if the host is either an exact match or a match
|
42
88
|
to the wildcard pattern.
|
@@ -56,7 +102,7 @@ def _is_same_domain(host: str, pattern: str) -> bool:
|
|
56
102
|
)
|
57
103
|
|
58
104
|
|
59
|
-
def
|
105
|
+
def parse_ip_address(
|
60
106
|
host: str,
|
61
107
|
) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
62
108
|
"""
|
@@ -75,7 +121,7 @@ def _parse_ip_address(
|
|
75
121
|
return None
|
76
122
|
|
77
123
|
|
78
|
-
def
|
124
|
+
def parse_cidr_pattern(
|
79
125
|
pattern: str,
|
80
126
|
) -> ipaddress.IPv4Network | ipaddress.IPv6Network | None:
|
81
127
|
"""
|
@@ -127,7 +173,7 @@ def validate_host(host: str, allowed_hosts: list[str]) -> bool:
|
|
127
173
|
Return ``True`` for a valid host, ``False`` otherwise.
|
128
174
|
"""
|
129
175
|
# Parse the host as an IP address if possible
|
130
|
-
host_ip =
|
176
|
+
host_ip = parse_ip_address(host)
|
131
177
|
|
132
178
|
for pattern in allowed_hosts:
|
133
179
|
# Wildcard matches everything
|
@@ -135,13 +181,13 @@ def validate_host(host: str, allowed_hosts: list[str]) -> bool:
|
|
135
181
|
return True
|
136
182
|
|
137
183
|
# Check CIDR notation patterns using walrus operator
|
138
|
-
if network :=
|
184
|
+
if network := parse_cidr_pattern(pattern):
|
139
185
|
if host_ip and host_ip in network:
|
140
186
|
return True
|
141
187
|
continue
|
142
188
|
|
143
189
|
# For non-CIDR patterns, use existing domain matching logic
|
144
|
-
if
|
190
|
+
if is_same_domain(host, pattern):
|
145
191
|
return True
|
146
192
|
|
147
193
|
return False
|
@@ -21,7 +21,6 @@ class HttpsRedirectMiddleware:
|
|
21
21
|
|
22
22
|
def maybe_https_redirect(self, request):
|
23
23
|
if self.https_redirect_enabled and not request.is_https():
|
24
|
-
host = request.get_host()
|
25
24
|
return ResponseRedirect(
|
26
|
-
f"https://{host}{request.get_full_path()}", status_code=301
|
25
|
+
f"https://{request.host}{request.get_full_path()}", status_code=301
|
27
26
|
)
|
@@ -65,7 +65,7 @@ class RedirectSlashMiddleware:
|
|
65
65
|
f"You called this URL via {request.method}, but the URL doesn't end "
|
66
66
|
"in a slash and you have APPEND_SLASH set. Plain can't "
|
67
67
|
f"redirect to the slash URL while maintaining {request.method} data. "
|
68
|
-
f"Change your form to point to {request.
|
68
|
+
f"Change your form to point to {request.host + new_path} (note the trailing "
|
69
69
|
"slash), or set APPEND_SLASH=False in your Plain settings."
|
70
70
|
)
|
71
71
|
return new_path
|
plain/runtime/global_settings.py
CHANGED
@@ -25,7 +25,7 @@ URLS_ROUTER: str
|
|
25
25
|
# Hosts/domain names that are valid for this site.
|
26
26
|
# "*" matches anything, ".example.com" matches example.com and all subdomains
|
27
27
|
# "192.168.1.0/24" matches IP addresses in that CIDR range
|
28
|
-
ALLOWED_HOSTS: list[str]
|
28
|
+
ALLOWED_HOSTS: list[str]
|
29
29
|
|
30
30
|
# Default headers for all responses.
|
31
31
|
DEFAULT_RESPONSE_HEADERS = {
|
@@ -1,9 +1,9 @@
|
|
1
1
|
plain/AGENTS.md,sha256=5XMGBpJgbCNIpp60DPXB7bpAtFk8FAzqiZke95T965o,1038
|
2
|
-
plain/CHANGELOG.md,sha256=
|
2
|
+
plain/CHANGELOG.md,sha256=9wAiAPwm8o5sNxPeriOAjP74g9sNxJRF5EpTTwjogpk,16069
|
3
3
|
plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
|
4
4
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
5
5
|
plain/debug.py,sha256=XdjnXcbPGsi0J2SpHGaLthhYU5AjhBlkHdemaP4sbYY,758
|
6
|
-
plain/exceptions.py,sha256=
|
6
|
+
plain/exceptions.py,sha256=QepC5UG5IBGW3h2cimOqGtjslAzvYdLYIn6xHdyco-Y,5868
|
7
7
|
plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
|
8
8
|
plain/paginator.py,sha256=iXiOyt2r_YwNrkqCRlaU7V-M_BKaaQ8XZElUBVa6yeU,5844
|
9
9
|
plain/signing.py,sha256=i8Bf12c96u_1BZYjETiixhsLAWMAt_y4CIYZOsI6IVA,8295
|
@@ -46,7 +46,7 @@ plain/cli/agent/md.py,sha256=7r1II8ckubBFOZNGPASWaPmJdgByWFPINLqIOzRetLQ,2581
|
|
46
46
|
plain/cli/agent/prompt.py,sha256=rugYyQHV7JDNqGrx3_PPShwwqYlnEVbxw8RsczOo8tg,1253
|
47
47
|
plain/cli/agent/request.py,sha256=U4acLmpXlbFRjivrPtVv_r8DBts8OXg3m3-qotQxGL4,6547
|
48
48
|
plain/csrf/README.md,sha256=ApWpB-qlEf0LkOKm9Yr-6f_lB9XJEvGFDo_fraw8ghI,2391
|
49
|
-
plain/csrf/middleware.py,sha256=
|
49
|
+
plain/csrf/middleware.py,sha256=KF8ngFadWdS0MHXC1dTLx-K3VtD6Xs-3RDjFqpiiEjQ,5053
|
50
50
|
plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
|
51
51
|
plain/forms/README.md,sha256=7MJQxNBoKkg0rW16qF6bGpUBxZrMrWjl2DZZk6gjzAU,2258
|
52
52
|
plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
|
@@ -57,9 +57,8 @@ plain/forms/forms.py,sha256=hF7Dl8rEaiBTZhFQyfbh1Zf54BSEka8RYpBiGqkTa8I,10441
|
|
57
57
|
plain/http/README.md,sha256=hkjTJJ2_WEGm7vaIxjjNHrzD6EN5VI6pjDJPIR9l1jo,760
|
58
58
|
plain/http/__init__.py,sha256=PfmXBIq7onLO_bbOrVdj5rJeHxqJMYqrIobVKupUzUA,1003
|
59
59
|
plain/http/cookie.py,sha256=THd7nOl-2ugeBPKgOhbD87aM2oxUbNH8HWrarUn0fpM,1955
|
60
|
-
plain/http/hosts.py,sha256=GbvFzle2VgPC2ehtXW8MM_1XD9gKuv7h-8HTOX3RiJA,4567
|
61
60
|
plain/http/multipartparser.py,sha256=Z1dFJNAd8N5RHUuF67jh1jBfZOFepORsre_3ee6CgOQ,27266
|
62
|
-
plain/http/request.py,sha256=
|
61
|
+
plain/http/request.py,sha256=4VkejuFitAwlmHs45hV6g-BfZxKM-D9vqnNTTljzj28,23864
|
63
62
|
plain/http/response.py,sha256=FM3otFkKEEkAaV_pVB3JUSMhMk7x_zf5JcDWKhOKviM,23223
|
64
63
|
plain/internal/__init__.py,sha256=fVBaYLCXEQc-7riHMSlw3vMTTuF7-0Bj2I8aGzv0o0w,171
|
65
64
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
@@ -71,13 +70,14 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
|
|
71
70
|
plain/internal/files/uploadhandler.py,sha256=63_QUwAwfq3bevw79i0S7zt2EB2UBoO7MaauvezaVMY,7198
|
72
71
|
plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
|
73
72
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
74
|
-
plain/internal/handlers/base.py,sha256=
|
73
|
+
plain/internal/handlers/base.py,sha256=YAfb43PPtkVtBzAlS7UsaCPh10a9h_K7qdIUoNg6LdE,6100
|
75
74
|
plain/internal/handlers/exception.py,sha256=TbPYtgZ7ITJahUKhQWkptHK28Lb4zh_nOviNctC2EYs,4815
|
76
75
|
plain/internal/handlers/wsgi.py,sha256=dgPT29t_F9llB-c5RYU3SHxGuZNaZ83xRjOfuOmtOl8,8209
|
77
76
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
78
77
|
plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
|
79
|
-
plain/internal/middleware/
|
80
|
-
plain/internal/middleware/
|
78
|
+
plain/internal/middleware/hosts.py,sha256=eDrW8ksCVyxZqehA6enNDG_nYOc2c2-CMvwMlfS4n9g,5918
|
79
|
+
plain/internal/middleware/https.py,sha256=bjysNX6_2l4Rp4DpzF2XYjH-m6gfHNq2x-MN2hHGW78,804
|
80
|
+
plain/internal/middleware/slash.py,sha256=qBTJ-yvOM0d8gUISUC7Fx22LG6aj76uhOiNHjvTkAAs,2840
|
81
81
|
plain/logs/README.md,sha256=rzOHfngjizLgXL21g0svC1Cdya2s_gBA_E-IljtHpy8,4069
|
82
82
|
plain/logs/__init__.py,sha256=gFVMcNn5D6z0JrvUJgGsOeYj1NKNtEXhw0MvPDtkN6w,58
|
83
83
|
plain/logs/configure.py,sha256=G5kLP-92hOWE7vlWG3lhSbzOKXobavFbqjohevJF1Jg,1322
|
@@ -98,7 +98,7 @@ plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2
|
|
98
98
|
plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
|
99
99
|
plain/runtime/README.md,sha256=sTqXXJkckwqkk9O06XMMSNRokAYjrZBnB50JD36BsYI,4873
|
100
100
|
plain/runtime/__init__.py,sha256=byFYnHrpUCwkpkHtdNhxr9iUdLDCWJjy92HPj30Ilck,2478
|
101
|
-
plain/runtime/global_settings.py,sha256=
|
101
|
+
plain/runtime/global_settings.py,sha256=tTvx7Z9gtkdy297n20aVhuHz7XKmo7OFU1spuJ22dhQ,5821
|
102
102
|
plain/runtime/user_settings.py,sha256=OzMiEkE6ZQ50nxd1WIqirXPiNuMAQULklYHEzgzLWgA,11027
|
103
103
|
plain/runtime/utils.py,sha256=p5IuNTzc7Kq-9Ym7etYnt_xqHw5TioxfSkFeq1bKdgk,832
|
104
104
|
plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
|
@@ -161,8 +161,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
|
161
161
|
plain/views/objects.py,sha256=v3Vgvdoc1s0QW6JNWWrO5XXy9zF7vgwndgxX1eOSQoE,4999
|
162
162
|
plain/views/redirect.py,sha256=Xpb3cB7nZYvKgkNqcAxf9Jwm2SWcQ0u2xz4oO5M3vP8,1909
|
163
163
|
plain/views/templates.py,sha256=oAlebEyfES0rzBhfyEJzFmgLkpkbleA6Eip-8zDp-yk,1863
|
164
|
-
plain-0.
|
165
|
-
plain-0.
|
166
|
-
plain-0.
|
167
|
-
plain-0.
|
168
|
-
plain-0.
|
164
|
+
plain-0.66.0.dist-info/METADATA,sha256=Gdvvx2LeKOpOjVbrSmq1S5DLWDT9qGGOmy4v-kOb_y4,4488
|
165
|
+
plain-0.66.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
166
|
+
plain-0.66.0.dist-info/entry_points.txt,sha256=iGx7EijzXy87htbSv90RhtAcjhSTH_kvE8aeRCn1TRA,129
|
167
|
+
plain-0.66.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
168
|
+
plain-0.66.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|