Skip to content

Commit a196790

Browse files
committed
Inline parse_request from cpython
Applied deltas: - Fix http.client references - Inline HTTPStatus codes - Address request line splitting (https://bugs.python.org/issue33973) - Special-case py2 header-parsing - Address multiple leading slashes in request path (python/cpython#99220) Closes-Bug: #1999278 Change-Id: Iae28097668213aa0734837ff21aef83251167d19 (cherry picked from commit 884f553)
1 parent 75aae4c commit a196790

File tree

2 files changed

+212
-78
lines changed

2 files changed

+212
-78
lines changed

swift/common/http_protocol.py

+108-34
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
from eventlet import wsgi, websocket
1717
import six
1818

19-
from swift.common.swob import wsgi_quote, wsgi_unquote, \
20-
wsgi_quote_plus, wsgi_unquote_plus, wsgi_to_bytes, bytes_to_wsgi
19+
20+
if six.PY2:
21+
from eventlet.green import httplib as http_client
22+
else:
23+
from eventlet.green.http import client as http_client
2124

2225

2326
class SwiftHttpProtocol(wsgi.HttpProtocol):
@@ -62,44 +65,115 @@ def get_default_type(self):
6265
return ''
6366

6467
def parse_request(self):
65-
# Need to track the bytes-on-the-wire for S3 signatures -- eventlet
66-
# would do it for us, but since we rewrite the path on py3, we need to
67-
# fix it ourselves later.
68-
self.__raw_path_info = None
68+
"""Parse a request (inlined from cpython@7e293984).
69+
70+
The request should be stored in self.raw_requestline; the results
71+
are in self.command, self.path, self.request_version and
72+
self.headers.
73+
74+
Return True for success, False for failure; on failure, any relevant
75+
error response has already been sent back.
6976
77+
"""
78+
self.command = None # set in case of error on the first line
79+
self.request_version = version = self.default_request_version
80+
self.close_connection = True
81+
requestline = self.raw_requestline
7082
if not six.PY2:
71-
# request lines *should* be ascii per the RFC, but historically
72-
# we've allowed (and even have func tests that use) arbitrary
73-
# bytes. This breaks on py3 (see https://bugs.python.org/issue33973
74-
# ) but the work-around is simple: munge the request line to be
75-
# properly quoted.
76-
if self.raw_requestline.count(b' ') >= 2:
77-
parts = self.raw_requestline.split(b' ', 2)
78-
path, q, query = parts[1].partition(b'?')
79-
self.__raw_path_info = path
80-
# unquote first, so we don't over-quote something
81-
# that was *correctly* quoted
82-
path = wsgi_to_bytes(wsgi_quote(wsgi_unquote(
83-
bytes_to_wsgi(path))))
84-
query = b'&'.join(
85-
sep.join([
86-
wsgi_to_bytes(wsgi_quote_plus(wsgi_unquote_plus(
87-
bytes_to_wsgi(key)))),
88-
wsgi_to_bytes(wsgi_quote_plus(wsgi_unquote_plus(
89-
bytes_to_wsgi(val))))
90-
])
91-
for part in query.split(b'&')
92-
for key, sep, val in (part.partition(b'='), ))
93-
parts[1] = path + q + query
94-
self.raw_requestline = b' '.join(parts)
95-
# else, mangled protocol, most likely; let base class deal with it
96-
return wsgi.HttpProtocol.parse_request(self)
83+
requestline = requestline.decode('iso-8859-1')
84+
requestline = requestline.rstrip('\r\n')
85+
self.requestline = requestline
86+
# Split off \x20 explicitly (see https://bugs.python.org/issue33973)
87+
words = requestline.split(' ')
88+
if len(words) == 0:
89+
return False
90+
91+
if len(words) >= 3: # Enough to determine protocol version
92+
version = words[-1]
93+
try:
94+
if not version.startswith('HTTP/'):
95+
raise ValueError
96+
base_version_number = version.split('/', 1)[1]
97+
version_number = base_version_number.split(".")
98+
# RFC 2145 section 3.1 says there can be only one "." and
99+
# - major and minor numbers MUST be treated as
100+
# separate integers;
101+
# - HTTP/2.4 is a lower version than HTTP/2.13, which in
102+
# turn is lower than HTTP/12.3;
103+
# - Leading zeros MUST be ignored by recipients.
104+
if len(version_number) != 2:
105+
raise ValueError
106+
version_number = int(version_number[0]), int(version_number[1])
107+
except (ValueError, IndexError):
108+
self.send_error(
109+
400,
110+
"Bad request version (%r)" % version)
111+
return False
112+
if version_number >= (1, 1) and \
113+
self.protocol_version >= "HTTP/1.1":
114+
self.close_connection = False
115+
if version_number >= (2, 0):
116+
self.send_error(
117+
505,
118+
"Invalid HTTP version (%s)" % base_version_number)
119+
return False
120+
self.request_version = version
121+
122+
if not 2 <= len(words) <= 3:
123+
self.send_error(
124+
400,
125+
"Bad request syntax (%r)" % requestline)
126+
return False
127+
command, path = words[:2]
128+
if len(words) == 2:
129+
self.close_connection = True
130+
if command != 'GET':
131+
self.send_error(
132+
400,
133+
"Bad HTTP/0.9 request type (%r)" % command)
134+
return False
135+
self.command, self.path = command, path
136+
137+
# Examine the headers and look for a Connection directive.
138+
if six.PY2:
139+
self.headers = self.MessageClass(self.rfile, 0)
140+
else:
141+
try:
142+
self.headers = http_client.parse_headers(
143+
self.rfile,
144+
_class=self.MessageClass)
145+
except http_client.LineTooLong as err:
146+
self.send_error(
147+
431,
148+
"Line too long",
149+
str(err))
150+
return False
151+
except http_client.HTTPException as err:
152+
self.send_error(
153+
431,
154+
"Too many headers",
155+
str(err)
156+
)
157+
return False
158+
159+
conntype = self.headers.get('Connection', "")
160+
if conntype.lower() == 'close':
161+
self.close_connection = True
162+
elif (conntype.lower() == 'keep-alive' and
163+
self.protocol_version >= "HTTP/1.1"):
164+
self.close_connection = False
165+
# Examine the headers and look for an Expect directive
166+
expect = self.headers.get('Expect', "")
167+
if (expect.lower() == "100-continue" and
168+
self.protocol_version >= "HTTP/1.1" and
169+
self.request_version >= "HTTP/1.1"):
170+
if not self.handle_expect_100():
171+
return False
172+
return True
97173

98174
if not six.PY2:
99175
def get_environ(self, *args, **kwargs):
100176
environ = wsgi.HttpProtocol.get_environ(self, *args, **kwargs)
101-
environ['RAW_PATH_INFO'] = bytes_to_wsgi(
102-
self.__raw_path_info)
103177
header_payload = self.headers.get_payload()
104178
if isinstance(header_payload, list) and len(header_payload) == 1:
105179
header_payload = header_payload[0].get_payload()

0 commit comments

Comments
 (0)