Commit 988dbd7b authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #10711: Remove HTTP 0.9 support from http.client. The `strict`

parameter to HTTPConnection and friends is deprecated.
parent 7cb3051d
......@@ -23,16 +23,13 @@ HTTPS protocols. It is normally not used directly --- the module
The module provides the following classes:
.. class:: HTTPConnection(host, port=None, strict=None[, timeout[, source_address]])
.. class:: HTTPConnection(host, port=None[, strict[, timeout[, source_address]]])
An :class:`HTTPConnection` instance represents one transaction with an HTTP
server. It should be instantiated passing it a host and optional port
number. If no port number is passed, the port is extracted from the host
string if it has the form ``host:port``, else the default HTTP port (80) is
used. When True, the optional parameter *strict* (which defaults to a false
value) causes ``BadStatusLine`` to
be raised if the status line can't be parsed as a valid HTTP/1.0 or 1.1
status line. If the optional *timeout* parameter is given, blocking
used. If the optional *timeout* parameter is given, blocking
operations (like connection attempts) will timeout after that many seconds
(if it is not given, the global default timeout setting is used).
The optional *source_address* parameter may be a typle of a (host, port)
......@@ -49,8 +46,12 @@ The module provides the following classes:
.. versionchanged:: 3.2
*source_address* was added.
.. versionchanged:: 3.2
The *strict* parameter is deprecated. HTTP 0.9-style "Simple Responses"
are not supported anymore.
.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None, strict=None[, timeout[, source_address]], *, context=None, check_hostname=None)
.. class:: HTTPSConnection(host, port=None, key_file=None, cert_file=None[, strict[, timeout[, source_address]]], *, context=None, check_hostname=None)
A subclass of :class:`HTTPConnection` that uses SSL for communication with
secure servers. Default port is ``443``. If *context* is specified, it
......@@ -80,12 +81,20 @@ The module provides the following classes:
This class now supports HTTPS virtual hosts if possible (that is,
if :data:`ssl.HAS_SNI` is true).
.. versionchanged:: 3.2
The *strict* parameter is deprecated. HTTP 0.9-style "Simple Responses"
are not supported anymore.
.. class:: HTTPResponse(sock, debuglevel=0, strict=0, method=None, url=None)
.. class:: HTTPResponse(sock, debuglevel=0[, strict], method=None, url=None)
Class whose instances are returned upon successful connection. Not
instantiated directly by user.
.. versionchanged:: 3.2
The *strict* parameter is deprecated. HTTP 0.9-style "Simple Responses"
are not supported anymore.
The following exceptions are raised as appropriate:
......
......@@ -252,13 +252,10 @@ def parse_headers(fp, _class=HTTPMessage):
hstring = b''.join(headers).decode('iso-8859-1')
return email.parser.Parser(_class=_class).parsestr(hstring)
class HTTPResponse(io.RawIOBase):
# strict: If true, raise BadStatusLine if the status line can't be
# parsed as a valid HTTP/1.0 or 1.1 status line. By default it is
# false because it prevents clients from talking to HTTP/0.9
# servers. Note that a response with a sufficiently corrupted
# status line will look like an HTTP/0.9 response.
_strict_sentinel = object()
class HTTPResponse(io.RawIOBase):
# See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details.
......@@ -267,7 +264,7 @@ class HTTPResponse(io.RawIOBase):
# text following RFC 2047. The basic status line parsing only
# accepts iso-8859-1.
def __init__(self, sock, debuglevel=0, strict=0, method=None, url=None):
def __init__(self, sock, debuglevel=0, strict=_strict_sentinel, method=None, url=None):
# If the response includes a content-length header, we need to
# make sure that the client doesn't read more than the
# specified number of bytes. If it does, it will block until
......@@ -277,7 +274,10 @@ class HTTPResponse(io.RawIOBase):
# clients unless they know what they are doing.
self.fp = sock.makefile("rb")
self.debuglevel = debuglevel
self.strict = strict
if strict is not _strict_sentinel:
warnings.warn("the 'strict' argument isn't supported anymore; "
"http.client now always assumes HTTP/1.x compliant servers.",
DeprecationWarning, 2)
self._method = method
# The HTTPResponse object is returned via urllib. The clients
......@@ -299,7 +299,6 @@ class HTTPResponse(io.RawIOBase):
self.will_close = _UNKNOWN # conn will close at end of response
def _read_status(self):
# Initialize with Simple-Response defaults.
line = str(self.fp.readline(), "iso-8859-1")
if self.debuglevel > 0:
print("reply:", repr(line))
......@@ -308,25 +307,17 @@ class HTTPResponse(io.RawIOBase):
# sending a valid response.
raise BadStatusLine(line)
try:
[version, status, reason] = line.split(None, 2)
version, status, reason = line.split(None, 2)
except ValueError:
try:
[version, status] = line.split(None, 1)
version, status = line.split(None, 1)
reason = ""
except ValueError:
# empty version will cause next test to fail and status
# will be treated as 0.9 response.
# empty version will cause next test to fail.
version = ""
if not version.startswith("HTTP/"):
if self.strict:
self.close()
raise BadStatusLine(line)
else:
# Assume it's a Simple-Response from an 0.9 server.
# We have to convert the first line back to raw bytes
# because self.fp.readline() needs to return bytes.
self.fp = LineAndFileWrapper(bytes(line, "ascii"), self.fp)
return "HTTP/0.9", 200, ""
self.close()
raise BadStatusLine(line)
# The status code is a three-digit number
try:
......@@ -357,22 +348,14 @@ class HTTPResponse(io.RawIOBase):
self.code = self.status = status
self.reason = reason.strip()
if version == "HTTP/1.0":
if version in ("HTTP/1.0", "HTTP/0.9"):
# Some servers might still return "0.9", treat it as 1.0 anyway
self.version = 10
elif version.startswith("HTTP/1."):
self.version = 11 # use HTTP/1.1 code for HTTP/1.x where x>=1
elif version == "HTTP/0.9":
self.version = 9
else:
raise UnknownProtocol(version)
if self.version == 9:
self.length = None
self.chunked = False
self.will_close = True
self.headers = self.msg = email.message_from_string('')
return
self.headers = self.msg = parse_headers(self.fp)
if self.debuglevel > 0:
......@@ -639,10 +622,13 @@ class HTTPConnection:
default_port = HTTP_PORT
auto_open = 1
debuglevel = 0
strict = 0
def __init__(self, host, port=None, strict=None,
def __init__(self, host, port=None, strict=_strict_sentinel,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
if strict is not _strict_sentinel:
warnings.warn("the 'strict' argument isn't supported anymore; "
"http.client now always assumes HTTP/1.x compliant servers.",
DeprecationWarning, 2)
self.timeout = timeout
self.source_address = source_address
self.sock = None
......@@ -654,8 +640,6 @@ class HTTPConnection:
self._tunnel_port = None
self._set_hostport(host, port)
if strict is not None:
self.strict = strict
def set_tunnel(self, host, port=None, headers=None):
""" Sets up the host and the port for the HTTP CONNECT Tunnelling.
......@@ -700,8 +684,7 @@ class HTTPConnection:
header_bytes = header_str.encode("ascii")
self.send(header_bytes)
response = self.response_class(self.sock, strict = self.strict,
method = self._method)
response = self.response_class(self.sock, method = self._method)
(version, code, message) = response._read_status()
if code != 200:
......@@ -1025,11 +1008,9 @@ class HTTPConnection:
if self.debuglevel > 0:
response = self.response_class(self.sock, self.debuglevel,
strict=self.strict,
method=self._method)
else:
response = self.response_class(self.sock, strict=self.strict,
method=self._method)
response = self.response_class(self.sock, method=self._method)
response.begin()
assert response.will_close != _UNKNOWN
......@@ -1057,7 +1038,7 @@ else:
# XXX Should key_file and cert_file be deprecated in favour of context?
def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
strict=_strict_sentinel, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None, *, context=None, check_hostname=None):
super(HTTPSConnection, self).__init__(host, port, strict, timeout,
source_address)
......@@ -1158,71 +1139,3 @@ class BadStatusLine(HTTPException):
# for backwards compatibility
error = HTTPException
class LineAndFileWrapper:
"""A limited file-like object for HTTP/0.9 responses."""
# The status-line parsing code calls readline(), which normally
# get the HTTP status line. For a 0.9 response, however, this is
# actually the first line of the body! Clients need to get a
# readable file object that contains that line.
def __init__(self, line, file):
self._line = line
self._file = file
self._line_consumed = 0
self._line_offset = 0
self._line_left = len(line)
def __getattr__(self, attr):
return getattr(self._file, attr)
def _done(self):
# called when the last byte is read from the line. After the
# call, all read methods are delegated to the underlying file
# object.
self._line_consumed = 1
self.read = self._file.read
self.readline = self._file.readline
self.readlines = self._file.readlines
def read(self, amt=None):
if self._line_consumed:
return self._file.read(amt)
assert self._line_left
if amt is None or amt > self._line_left:
s = self._line[self._line_offset:]
self._done()
if amt is None:
return s + self._file.read()
else:
return s + self._file.read(amt - len(s))
else:
assert amt <= self._line_left
i = self._line_offset
j = i + amt
s = self._line[i:j]
self._line_offset = j
self._line_left -= amt
if self._line_left == 0:
self._done()
return s
def readline(self):
if self._line_consumed:
return self._file.readline()
assert self._line_left
s = self._line[self._line_offset:]
self._done()
return s
def readlines(self, size=None):
if self._line_consumed:
return self._file.readlines(size)
assert self._line_left
L = [self._line[self._line_offset:]]
self._done()
if size is None:
return L + self._file.readlines()
else:
return L + self._file.readlines(size)
......@@ -139,8 +139,10 @@ class urlopen_HttpTests(unittest.TestCase):
def fakehttp(self, fakedata):
class FakeSocket(io.BytesIO):
io_refs = 1
def sendall(self, str): pass
def makefile(self, *args, **kwds):
self.io_refs += 1
return self
def read(self, amt=None):
if self.closed: return b""
......@@ -148,6 +150,10 @@ class urlopen_HttpTests(unittest.TestCase):
def readline(self, length=None):
if self.closed: return b""
return io.BytesIO.readline(self, length)
def close(self):
self.io_refs -= 1
if self.io_refs == 0:
io.BytesIO.close(self)
class FakeHTTPConnection(http.client.HTTPConnection):
def connect(self):
self.sock = FakeSocket(fakedata)
......@@ -157,8 +163,8 @@ class urlopen_HttpTests(unittest.TestCase):
def unfakehttp(self):
http.client.HTTPConnection = self._connection_class
def test_read(self):
self.fakehttp(b"Hello!")
def check_read(self, ver):
self.fakehttp(b"HTTP/" + ver + b" 200 OK\r\n\r\nHello!")
try:
fp = urlopen("http://python.org/")
self.assertEqual(fp.readline(), b"Hello!")
......@@ -168,6 +174,17 @@ class urlopen_HttpTests(unittest.TestCase):
finally:
self.unfakehttp()
def test_read_0_9(self):
# "0.9" response accepted (but not "simple responses" without
# a status line)
self.check_read(b"0.9")
def test_read_1_0(self):
self.check_read(b"1.0")
def test_read_1_1(self):
self.check_read(b"1.1")
def test_read_bogus(self):
# urlopen() should raise IOError for many error codes.
self.fakehttp(b'''HTTP/1.1 401 Authentication Required
......@@ -191,7 +208,7 @@ Content-Type: text/html; charset=iso-8859-1
self.unfakehttp()
def test_userpass_inurl(self):
self.fakehttp(b"Hello!")
self.fakehttp(b"HTTP/1.0 200 OK\r\n\r\nHello!")
try:
fp = urlopen("http://user:pass@python.org/")
self.assertEqual(fp.readline(), b"Hello!")
......
......@@ -19,6 +19,10 @@ Core and Builtins
Library
-------
- Issue #10711: Remove HTTP 0.9 support from http.client. The ``strict``
parameter to HTTPConnection and friends is deprecated.
- Issue #9721: Fix the behavior of urljoin when the relative url starts with a
';' character. Patch by Wes Chow.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment