Skip to content

Commit 05eea64

Browse files
committed
Remove server-level scheme detection, unify on HTTPS_PROXY_HEADER
The server had a gunicorn-era mechanism that checked X-Forwarded-Proto/SSL/Protocol headers against a trusted IP allowlist (SERVER_FORWARDED_ALLOW_IPS) during HTTP parsing. This overlapped with the application-layer HTTPS_PROXY_HEADER setting, causing confusion when only one was configured. Now that WSGI is gone and the server builds Request objects directly, scheme detection belongs entirely in the application layer.
1 parent 2894abf commit 05eea64

File tree

7 files changed

+19
-86
lines changed

7 files changed

+19
-86
lines changed

plain/plain/runtime/global_settings.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,6 @@
192192
]
193193
SERVER_GRACEFUL_TIMEOUT: int = 30
194194
SERVER_SENDFILE: bool = True
195-
SERVER_FORWARDED_ALLOW_IPS: str = (
196-
"127.0.0.1,::1" # see proposals/plain-server-forwarded-allow-ips.md
197-
)
198195

199196
# MARK: Preflight Checks
200197

plain/plain/server/README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ SERVER_ACCESS_LOG = True
8282
SERVER_ACCESS_LOG_FIELDS = ["method", "path", "query", "status", "duration_ms", "size", "ip", "user_agent", "referer"]
8383
SERVER_GRACEFUL_TIMEOUT = 30
8484
SERVER_SENDFILE = True
85-
SERVER_FORWARDED_ALLOW_IPS = "127.0.0.1,::1"
8685
```
8786

8887
Settings can also be set via environment variables with the `PLAIN_` prefix (e.g., `PLAIN_SERVER_WORKERS=4`).
@@ -129,6 +128,14 @@ return response
129128

130129
Plain uses this internally to suppress asset 304 responses (controlled by the `ASSETS_LOG_304` setting).
131130

131+
### How access logging works
132+
133+
Access logging has three layers, each at the right level of abstraction:
134+
135+
1. **`SERVER_ACCESS_LOG`** (server setting) — master switch that enables or disables access logging entirely.
136+
2. **`response.log_access`** (per-response) — individual responses can opt out by setting `log_access = False`.
137+
3. **`ASSETS_LOG_304`** (assets setting) — controls whether 304 Not Modified responses for assets are logged. When `False` (default), asset 304s set `log_access = False` on the response.
138+
132139
## Signals
133140

134141
The server responds to UNIX signals for process management.
@@ -151,14 +158,21 @@ plain server --certfile cert.pem --keyfile key.pem
151158

152159
#### How do I run behind a reverse proxy?
153160

154-
Configure your proxy to pass the appropriate headers, then set `SERVER_FORWARDED_ALLOW_IPS` to include your proxy's IP address.
161+
Configure your proxy to pass the appropriate headers, then use these settings to tell Plain how to interpret them:
155162

156163
```python
157164
# settings.py
158-
SERVER_FORWARDED_ALLOW_IPS = "10.0.0.1,10.0.0.2"
165+
166+
# Tell Plain which header indicates HTTPS (format: "Header-Name: value")
167+
HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"
168+
169+
# Trust X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-For headers
170+
HTTP_X_FORWARDED_HOST = True
171+
HTTP_X_FORWARDED_PORT = True
172+
HTTP_X_FORWARDED_FOR = True
159173
```
160174

161-
The server recognizes `X-Forwarded-Proto`, `X-Forwarded-Protocol`, and `X-Forwarded-SSL` headers from trusted proxies.
175+
See the [HTTP settings docs](../../http/README.md) for details on proxy header configuration.
162176

163177
#### How do I handle worker timeouts?
164178

plain/plain/server/http/errors.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,3 @@ def __init__(self, reason: str):
156156

157157
def __str__(self) -> str:
158158
return f"Invalid Host header: {self.reason}"
159-
160-
161-
class InvalidSchemeHeaders(ParseException):
162-
def __str__(self) -> str:
163-
return "Contradictory scheme headers"

plain/plain/server/http/message.py

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
InvalidHTTPVersion,
2020
InvalidRequestLine,
2121
InvalidRequestMethod,
22-
InvalidSchemeHeaders,
2322
LimitRequestHeaders,
2423
LimitRequestLine,
2524
NoMoreData,
@@ -44,24 +43,6 @@
4443
VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
4544
RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]")
4645

47-
# Headers that indicate HTTPS when set by a trusted proxy.
48-
SECURE_SCHEME_HEADERS: dict[str, str] = {
49-
"X-FORWARDED-PROTOCOL": "ssl",
50-
"X-FORWARDED-PROTO": "https",
51-
"X-FORWARDED-SSL": "on",
52-
}
53-
54-
# Header names that proxies can use to override request attributes.
55-
FORWARDER_HEADERS: list[str] = ["SCRIPT_NAME", "PATH_INFO"]
56-
57-
58-
def _get_forwarded_allow_ips() -> list[str]:
59-
"""Trusted proxy IPs allowed to set secure headers."""
60-
from plain.runtime import settings
61-
62-
val = settings.SERVER_FORWARDED_ALLOW_IPS
63-
return [v.strip() for v in val.split(",") if v]
64-
6546

6647
class Message:
6748
def __init__(self, is_ssl: bool, unreader: Any, peer_addr: tuple[str, int] | Any):
@@ -109,24 +90,6 @@ def parse_headers(
10990
# Split lines on \r\n
11091
lines = [bytes_to_str(line) for line in data.split(b"\r\n")]
11192

112-
# handle scheme headers
113-
scheme_header = False
114-
secure_scheme_headers: dict[str, str] = {}
115-
forwarder_headers: list[str] = []
116-
if from_trailer:
117-
# nonsense. either a request is https from the beginning
118-
# .. or we are just behind a proxy who does not remove conflicting trailers
119-
pass
120-
else:
121-
forwarded_allow_ips = _get_forwarded_allow_ips()
122-
if (
123-
"*" in forwarded_allow_ips
124-
or not isinstance(self.peer_addr, tuple)
125-
or self.peer_addr[0] in forwarded_allow_ips
126-
):
127-
secure_scheme_headers = SECURE_SCHEME_HEADERS
128-
forwarder_headers = FORWARDER_HEADERS
129-
13093
# Parse headers into key/value pairs paying attention
13194
# to continuation lines.
13295
while lines:
@@ -162,23 +125,11 @@ def parse_headers(
162125
if header_length > self.limit_request_field_size > 0:
163126
raise LimitRequestHeaders("limit request headers fields size")
164127

165-
if name in secure_scheme_headers:
166-
secure = value == secure_scheme_headers[name]
167-
scheme = "https" if secure else "http"
168-
if scheme_header:
169-
if scheme != self.scheme:
170-
raise InvalidSchemeHeaders()
171-
else:
172-
scheme_header = True
173-
self.scheme = scheme
174-
175128
# Drop underscore-containing header names — they're ambiguous
176129
# since they could collide with hyphenated names after
177130
# normalization (e.g. X_Forwarded_For vs X-Forwarded-For).
178-
# Forwarder headers are exempted.
179131
if "_" in name:
180-
if name not in forwarder_headers and "*" not in forwarder_headers:
181-
continue
132+
continue
182133

183134
headers.append((name, value))
184135

plain/plain/server/http/response.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ def create_request(
6969
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
7070
elif hdr_name == "HOST":
7171
host = hdr_value
72-
elif hdr_name == "SCRIPT_NAME":
73-
script_name = hdr_value
7472

7573
# Handle duplicate headers by joining with comma
7674
if hdr_name in headers:

plain/plain/server/workers/base.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
InvalidHTTPVersion,
3232
InvalidRequestLine,
3333
InvalidRequestMethod,
34-
InvalidSchemeHeaders,
3534
LimitRequestHeaders,
3635
LimitRequestLine,
3736
ObsoleteFolding,
@@ -226,7 +225,6 @@ def handle_error(
226225
| InvalidHostHeader
227226
| LimitRequestLine
228227
| LimitRequestHeaders
229-
| InvalidSchemeHeaders
230228
| UnsupportedTransferCoding
231229
| ConfigurationProblem
232230
| ObsoleteFolding
@@ -261,8 +259,6 @@ def handle_error(
261259
reason = "Request Header Fields Too Large"
262260
mesg = f"Error parsing headers: '{str(exc)}'"
263261
status_int = 431
264-
elif isinstance(exc, InvalidSchemeHeaders):
265-
mesg = f"{str(exc)}"
266262
elif isinstance(exc, SSLError):
267263
reason = "Forbidden"
268264
mesg = f"'{str(exc)}'"

proposals/plain-server-forwarded-allow-ips.md

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)