HTTP/1.0 - ignore Expect: 100-continue

* ignore on HTTP/1.0 (would possibly confuse a client or proxy)
* refuse requests with unknown expectations

https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1
This commit is contained in:
Paul J. Dorn 2025-04-07 16:59:29 +02:00
parent f0952e5874
commit 88d503ba1c
4 changed files with 37 additions and 7 deletions

View File

@ -47,6 +47,14 @@ class InvalidRequestMethod(ParseException):
return "Invalid HTTP method: %r" % self.method
class ExpectationFailed(ParseException):
def __init__(self, expect):
self.expect = expect
def __str__(self):
return "Unable to comply with expectation: %r" % (self.expect, )
class InvalidHTTPVersion(ParseException):
def __init__(self, version):
self.version = version

View File

@ -14,6 +14,7 @@ from gunicorn.http.errors import (
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
LimitRequestLine, LimitRequestHeaders,
UnsupportedTransferCoding, ObsoleteFolding,
ExpectationFailed,
)
from gunicorn.http.errors import InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest
from gunicorn.http.errors import InvalidSchemeHeaders
@ -90,6 +91,7 @@ class Message:
self.body = None
self.scheme = "https" if cfg.is_ssl else "http"
self.must_close = False
self._expected_100_continue = False
# set headers limits
self.limit_request_fields = cfg.limit_request_fields
@ -180,6 +182,21 @@ class Message:
if header_length > self.limit_request_field_size > 0:
raise LimitRequestHeaders("limit request headers fields size")
if not from_trailer and name == "EXPECT":
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1
# "The Expect field value is case-insensitive."
if value.lower() == "100-continue":
if self.version < (1, 1):
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12
# "A server that receives a 100-continue expectation
# in an HTTP/1.0 request MUST ignore that expectation."
pass
else:
self._expected_100_continue = True
# N.B. understood but ignored expect header does not return 417
else:
raise ExpectationFailed(value)
if name in secure_scheme_headers:
secure = value == secure_scheme_headers[name]
scheme = "https" if secure else "http"

View File

@ -117,13 +117,14 @@ def create(req, sock, client, server, cfg):
host = None
script_name = os.environ.get("SCRIPT_NAME", "")
if req._expected_100_continue:
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
# rfc9112: Expect MUST be forwarded if the request is forwarded
# N.B. gunicorn just sends at most one - application might send another
# add the headers to the environ
for hdr_name, hdr_value in req.headers:
if hdr_name == "EXPECT":
# handle expect
if hdr_value.lower() == "100-continue":
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
elif hdr_name == 'HOST':
if hdr_name == 'HOST':
host = hdr_value
elif hdr_name == "SCRIPT_NAME":
script_name = hdr_value

View File

@ -19,7 +19,7 @@ from gunicorn.http.errors import (
InvalidProxyLine, InvalidRequestLine,
InvalidRequestMethod, InvalidSchemeHeaders,
LimitRequestHeaders, LimitRequestLine,
UnsupportedTransferCoding,
UnsupportedTransferCoding, ExpectationFailed,
ConfigurationProblem, ObsoleteFolding,
)
from gunicorn.http.wsgi import Response, default_environ
@ -212,7 +212,7 @@ class Worker:
LimitRequestLine, LimitRequestHeaders,
InvalidProxyLine, ForbiddenProxyRequest,
InvalidSchemeHeaders, UnsupportedTransferCoding,
ConfigurationProblem, ObsoleteFolding,
ConfigurationProblem, ObsoleteFolding, ExpectationFailed,
SSLError,
)):
@ -239,6 +239,10 @@ class Worker:
req = exc.req # for access log
elif isinstance(exc, LimitRequestLine):
mesg = "%s" % str(exc)
elif isinstance(exc, ExpectationFailed):
reason = "Expectation Failed"
mesg = str(exc)
status_int = 417
elif isinstance(exc, LimitRequestHeaders):
reason = "Request Header Fields Too Large"
mesg = "Error parsing headers: '%s'" % str(exc)