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 ...@@ -23,16 +23,13 @@ HTTPS protocols. It is normally not used directly --- the module
The module provides the following classes: 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 An :class:`HTTPConnection` instance represents one transaction with an HTTP
server. It should be instantiated passing it a host and optional port 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 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 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 used. If the optional *timeout* parameter is given, blocking
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
operations (like connection attempts) will timeout after that many seconds operations (like connection attempts) will timeout after that many seconds
(if it is not given, the global default timeout setting is used). (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) The optional *source_address* parameter may be a typle of a (host, port)
...@@ -49,8 +46,12 @@ The module provides the following classes: ...@@ -49,8 +46,12 @@ The module provides the following classes:
.. versionchanged:: 3.2 .. versionchanged:: 3.2
*source_address* was added. *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 A subclass of :class:`HTTPConnection` that uses SSL for communication with
secure servers. Default port is ``443``. If *context* is specified, it secure servers. Default port is ``443``. If *context* is specified, it
...@@ -80,12 +81,20 @@ The module provides the following classes: ...@@ -80,12 +81,20 @@ The module provides the following classes:
This class now supports HTTPS virtual hosts if possible (that is, This class now supports HTTPS virtual hosts if possible (that is,
if :data:`ssl.HAS_SNI` is true). 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 Class whose instances are returned upon successful connection. Not
instantiated directly by user. 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: The following exceptions are raised as appropriate:
......
...@@ -252,13 +252,10 @@ def parse_headers(fp, _class=HTTPMessage): ...@@ -252,13 +252,10 @@ def parse_headers(fp, _class=HTTPMessage):
hstring = b''.join(headers).decode('iso-8859-1') hstring = b''.join(headers).decode('iso-8859-1')
return email.parser.Parser(_class=_class).parsestr(hstring) return email.parser.Parser(_class=_class).parsestr(hstring)
class HTTPResponse(io.RawIOBase):
# strict: If true, raise BadStatusLine if the status line can't be _strict_sentinel = object()
# 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 class HTTPResponse(io.RawIOBase):
# servers. Note that a response with a sufficiently corrupted
# status line will look like an HTTP/0.9 response.
# See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details. # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details.
...@@ -267,7 +264,7 @@ class HTTPResponse(io.RawIOBase): ...@@ -267,7 +264,7 @@ class HTTPResponse(io.RawIOBase):
# text following RFC 2047. The basic status line parsing only # text following RFC 2047. The basic status line parsing only
# accepts iso-8859-1. # 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 # If the response includes a content-length header, we need to
# make sure that the client doesn't read more than the # make sure that the client doesn't read more than the
# specified number of bytes. If it does, it will block until # specified number of bytes. If it does, it will block until
...@@ -277,7 +274,10 @@ class HTTPResponse(io.RawIOBase): ...@@ -277,7 +274,10 @@ class HTTPResponse(io.RawIOBase):
# clients unless they know what they are doing. # clients unless they know what they are doing.
self.fp = sock.makefile("rb") self.fp = sock.makefile("rb")
self.debuglevel = debuglevel 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 self._method = method
# The HTTPResponse object is returned via urllib. The clients # The HTTPResponse object is returned via urllib. The clients
...@@ -299,7 +299,6 @@ class HTTPResponse(io.RawIOBase): ...@@ -299,7 +299,6 @@ class HTTPResponse(io.RawIOBase):
self.will_close = _UNKNOWN # conn will close at end of response self.will_close = _UNKNOWN # conn will close at end of response
def _read_status(self): def _read_status(self):
# Initialize with Simple-Response defaults.
line = str(self.fp.readline(), "iso-8859-1") line = str(self.fp.readline(), "iso-8859-1")
if self.debuglevel > 0: if self.debuglevel > 0:
print("reply:", repr(line)) print("reply:", repr(line))
...@@ -308,25 +307,17 @@ class HTTPResponse(io.RawIOBase): ...@@ -308,25 +307,17 @@ class HTTPResponse(io.RawIOBase):
# sending a valid response. # sending a valid response.
raise BadStatusLine(line) raise BadStatusLine(line)
try: try:
[version, status, reason] = line.split(None, 2) version, status, reason = line.split(None, 2)
except ValueError: except ValueError:
try: try:
[version, status] = line.split(None, 1) version, status = line.split(None, 1)
reason = "" reason = ""
except ValueError: except ValueError:
# empty version will cause next test to fail and status # empty version will cause next test to fail.
# will be treated as 0.9 response.
version = "" version = ""
if not version.startswith("HTTP/"): if not version.startswith("HTTP/"):
if self.strict: self.close()
self.close() raise BadStatusLine(line)
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, ""
# The status code is a three-digit number # The status code is a three-digit number
try: try:
...@@ -357,22 +348,14 @@ class HTTPResponse(io.RawIOBase): ...@@ -357,22 +348,14 @@ class HTTPResponse(io.RawIOBase):
self.code = self.status = status self.code = self.status = status
self.reason = reason.strip() 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 self.version = 10
elif version.startswith("HTTP/1."): elif version.startswith("HTTP/1."):
self.version = 11 # use HTTP/1.1 code for HTTP/1.x where x>=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: else:
raise UnknownProtocol(version) 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) self.headers = self.msg = parse_headers(self.fp)
if self.debuglevel > 0: if self.debuglevel > 0:
...@@ -639,10 +622,13 @@ class HTTPConnection: ...@@ -639,10 +622,13 @@ class HTTPConnection:
default_port = HTTP_PORT default_port = HTTP_PORT
auto_open = 1 auto_open = 1
debuglevel = 0 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): 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.timeout = timeout
self.source_address = source_address self.source_address = source_address
self.sock = None self.sock = None
...@@ -654,8 +640,6 @@ class HTTPConnection: ...@@ -654,8 +640,6 @@ class HTTPConnection:
self._tunnel_port = None self._tunnel_port = None
self._set_hostport(host, port) self._set_hostport(host, port)
if strict is not None:
self.strict = strict
def set_tunnel(self, host, port=None, headers=None): def set_tunnel(self, host, port=None, headers=None):
""" Sets up the host and the port for the HTTP CONNECT Tunnelling. """ Sets up the host and the port for the HTTP CONNECT Tunnelling.
...@@ -700,8 +684,7 @@ class HTTPConnection: ...@@ -700,8 +684,7 @@ class HTTPConnection:
header_bytes = header_str.encode("ascii") header_bytes = header_str.encode("ascii")
self.send(header_bytes) self.send(header_bytes)
response = self.response_class(self.sock, strict = self.strict, response = self.response_class(self.sock, method = self._method)
method = self._method)
(version, code, message) = response._read_status() (version, code, message) = response._read_status()
if code != 200: if code != 200:
...@@ -1025,11 +1008,9 @@ class HTTPConnection: ...@@ -1025,11 +1008,9 @@ class HTTPConnection:
if self.debuglevel > 0: if self.debuglevel > 0:
response = self.response_class(self.sock, self.debuglevel, response = self.response_class(self.sock, self.debuglevel,
strict=self.strict,
method=self._method) method=self._method)
else: else:
response = self.response_class(self.sock, strict=self.strict, response = self.response_class(self.sock, method=self._method)
method=self._method)
response.begin() response.begin()
assert response.will_close != _UNKNOWN assert response.will_close != _UNKNOWN
...@@ -1057,7 +1038,7 @@ else: ...@@ -1057,7 +1038,7 @@ else:
# XXX Should key_file and cert_file be deprecated in favour of context? # 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, 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): source_address=None, *, context=None, check_hostname=None):
super(HTTPSConnection, self).__init__(host, port, strict, timeout, super(HTTPSConnection, self).__init__(host, port, strict, timeout,
source_address) source_address)
...@@ -1158,71 +1139,3 @@ class BadStatusLine(HTTPException): ...@@ -1158,71 +1139,3 @@ class BadStatusLine(HTTPException):
# for backwards compatibility # for backwards compatibility
error = HTTPException 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): ...@@ -139,8 +139,10 @@ class urlopen_HttpTests(unittest.TestCase):
def fakehttp(self, fakedata): def fakehttp(self, fakedata):
class FakeSocket(io.BytesIO): class FakeSocket(io.BytesIO):
io_refs = 1
def sendall(self, str): pass def sendall(self, str): pass
def makefile(self, *args, **kwds): def makefile(self, *args, **kwds):
self.io_refs += 1
return self return self
def read(self, amt=None): def read(self, amt=None):
if self.closed: return b"" if self.closed: return b""
...@@ -148,6 +150,10 @@ class urlopen_HttpTests(unittest.TestCase): ...@@ -148,6 +150,10 @@ class urlopen_HttpTests(unittest.TestCase):
def readline(self, length=None): def readline(self, length=None):
if self.closed: return b"" if self.closed: return b""
return io.BytesIO.readline(self, length) 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): class FakeHTTPConnection(http.client.HTTPConnection):
def connect(self): def connect(self):
self.sock = FakeSocket(fakedata) self.sock = FakeSocket(fakedata)
...@@ -157,8 +163,8 @@ class urlopen_HttpTests(unittest.TestCase): ...@@ -157,8 +163,8 @@ class urlopen_HttpTests(unittest.TestCase):
def unfakehttp(self): def unfakehttp(self):
http.client.HTTPConnection = self._connection_class http.client.HTTPConnection = self._connection_class
def test_read(self): def check_read(self, ver):
self.fakehttp(b"Hello!") self.fakehttp(b"HTTP/" + ver + b" 200 OK\r\n\r\nHello!")
try: try:
fp = urlopen("http://python.org/") fp = urlopen("http://python.org/")
self.assertEqual(fp.readline(), b"Hello!") self.assertEqual(fp.readline(), b"Hello!")
...@@ -168,6 +174,17 @@ class urlopen_HttpTests(unittest.TestCase): ...@@ -168,6 +174,17 @@ class urlopen_HttpTests(unittest.TestCase):
finally: finally:
self.unfakehttp() 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): def test_read_bogus(self):
# urlopen() should raise IOError for many error codes. # urlopen() should raise IOError for many error codes.
self.fakehttp(b'''HTTP/1.1 401 Authentication Required self.fakehttp(b'''HTTP/1.1 401 Authentication Required
...@@ -191,7 +208,7 @@ Content-Type: text/html; charset=iso-8859-1 ...@@ -191,7 +208,7 @@ Content-Type: text/html; charset=iso-8859-1
self.unfakehttp() self.unfakehttp()
def test_userpass_inurl(self): def test_userpass_inurl(self):
self.fakehttp(b"Hello!") self.fakehttp(b"HTTP/1.0 200 OK\r\n\r\nHello!")
try: try:
fp = urlopen("http://user:pass@python.org/") fp = urlopen("http://user:pass@python.org/")
self.assertEqual(fp.readline(), b"Hello!") self.assertEqual(fp.readline(), b"Hello!")
......
...@@ -19,6 +19,10 @@ Core and Builtins ...@@ -19,6 +19,10 @@ Core and Builtins
Library 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 - Issue #9721: Fix the behavior of urljoin when the relative url starts with a
';' character. Patch by Wes Chow. ';' 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