mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Updated the urllib + http libraries + test libraries (#6672)
* Updated urllib + urllib tests * Updated http + test * Annotated failing tests * Fixed issues in httpservers, robotparser and urllib2net * Fixed windows only success * Annotated flaky test in test_logging
This commit is contained in:
1 parent
ef871d227e
commit
4cb3b9d8f0
20 files changed
+1931
-785
No files matched your search
Vendored
+63
-11
@@ -1,14 +1,15 @@
|
||||
from enum import IntEnum
|
||||
from enum import StrEnum, IntEnum, _simple_enum
|
||||
|
||||
__all__ = ['HTTPStatus']
|
||||
__all__ = ['HTTPStatus', 'HTTPMethod']
|
||||
|
||||
|
||||
class HTTPStatus(IntEnum):
|
||||
@_simple_enum(IntEnum)
|
||||
class HTTPStatus:
|
||||
"""HTTP status codes and reason phrases
|
||||
|
||||
Status codes from the following RFCs are all observed:
|
||||
|
||||
* RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
|
||||
* RFC 9110: HTTP Semantics, obsoletes 7231, which obsoleted 2616
|
||||
* RFC 6585: Additional HTTP Status Codes
|
||||
* RFC 3229: Delta encoding in HTTP
|
||||
* RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
|
||||
@@ -25,11 +26,30 @@ class HTTPStatus(IntEnum):
|
||||
def __new__(cls, value, phrase, description=''):
|
||||
obj = int.__new__(cls, value)
|
||||
obj._value_ = value
|
||||
|
||||
obj.phrase = phrase
|
||||
obj.description = description
|
||||
return obj
|
||||
|
||||
@property
|
||||
def is_informational(self):
|
||||
return 100 <= self <= 199
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
return 200 <= self <= 299
|
||||
|
||||
@property
|
||||
def is_redirection(self):
|
||||
return 300 <= self <= 399
|
||||
|
||||
@property
|
||||
def is_client_error(self):
|
||||
return 400 <= self <= 499
|
||||
|
||||
@property
|
||||
def is_server_error(self):
|
||||
return 500 <= self <= 599
|
||||
|
||||
# informational
|
||||
CONTINUE = 100, 'Continue', 'Request received, please continue'
|
||||
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
|
||||
@@ -94,22 +114,25 @@ class HTTPStatus(IntEnum):
|
||||
'Client must specify Content-Length')
|
||||
PRECONDITION_FAILED = (412, 'Precondition Failed',
|
||||
'Precondition in headers is false')
|
||||
REQUEST_ENTITY_TOO_LARGE = (413, 'Request Entity Too Large',
|
||||
'Entity is too large')
|
||||
REQUEST_URI_TOO_LONG = (414, 'Request-URI Too Long',
|
||||
CONTENT_TOO_LARGE = (413, 'Content Too Large',
|
||||
'Content is too large')
|
||||
REQUEST_ENTITY_TOO_LARGE = CONTENT_TOO_LARGE
|
||||
URI_TOO_LONG = (414, 'URI Too Long',
|
||||
'URI is too long')
|
||||
REQUEST_URI_TOO_LONG = URI_TOO_LONG
|
||||
UNSUPPORTED_MEDIA_TYPE = (415, 'Unsupported Media Type',
|
||||
'Entity body in unsupported format')
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = (416,
|
||||
'Requested Range Not Satisfiable',
|
||||
RANGE_NOT_SATISFIABLE = (416, 'Range Not Satisfiable',
|
||||
'Cannot satisfy request range')
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = RANGE_NOT_SATISFIABLE
|
||||
EXPECTATION_FAILED = (417, 'Expectation Failed',
|
||||
'Expect condition could not be satisfied')
|
||||
IM_A_TEAPOT = (418, 'I\'m a Teapot',
|
||||
'Server refuses to brew coffee because it is a teapot.')
|
||||
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
|
||||
'Server is not able to produce a response')
|
||||
UNPROCESSABLE_ENTITY = 422, 'Unprocessable Entity'
|
||||
UNPROCESSABLE_CONTENT = 422, 'Unprocessable Content'
|
||||
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
|
||||
LOCKED = 423, 'Locked'
|
||||
FAILED_DEPENDENCY = 424, 'Failed Dependency'
|
||||
TOO_EARLY = 425, 'Too Early'
|
||||
@@ -148,3 +171,32 @@ class HTTPStatus(IntEnum):
|
||||
NETWORK_AUTHENTICATION_REQUIRED = (511,
|
||||
'Network Authentication Required',
|
||||
'The client needs to authenticate to gain network access')
|
||||
|
||||
|
||||
@_simple_enum(StrEnum)
|
||||
class HTTPMethod:
|
||||
"""HTTP methods and descriptions
|
||||
|
||||
Methods from the following RFCs are all observed:
|
||||
|
||||
* RFC 9110: HTTP Semantics, obsoletes 7231, which obsoleted 2616
|
||||
* RFC 5789: PATCH Method for HTTP
|
||||
"""
|
||||
def __new__(cls, value, description):
|
||||
obj = str.__new__(cls, value)
|
||||
obj._value_ = value
|
||||
obj.description = description
|
||||
return obj
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s.%s>" % (self.__class__.__name__, self._name_)
|
||||
|
||||
CONNECT = 'CONNECT', 'Establish a connection to the server.'
|
||||
DELETE = 'DELETE', 'Remove the target.'
|
||||
GET = 'GET', 'Retrieve the target.'
|
||||
HEAD = 'HEAD', 'Same as GET, but only retrieve the status line and header section.'
|
||||
OPTIONS = 'OPTIONS', 'Describe the communication options for the target.'
|
||||
PATCH = 'PATCH', 'Apply partial modifications to a target.'
|
||||
POST = 'POST', 'Perform target-specific processing with the request payload.'
|
||||
PUT = 'PUT', 'Replace the target with the request payload.'
|
||||
TRACE = 'TRACE', 'Perform a message loop-back test along the path to the target.'
|
||||
Vendored
+133
-88
@@ -111,6 +111,11 @@ responses = {v: v.phrase for v in http.HTTPStatus.__members__.values()}
|
||||
_MAXLINE = 65536
|
||||
_MAXHEADERS = 100
|
||||
|
||||
# Data larger than this will be read in chunks, to prevent extreme
|
||||
# overallocation.
|
||||
_MIN_READ_BUF_SIZE = 1 << 20
|
||||
|
||||
|
||||
# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
|
||||
#
|
||||
# VCHAR = %x21-7E
|
||||
@@ -172,6 +177,13 @@ def _encode(data, name='data'):
|
||||
"if you want to send it encoded in UTF-8." %
|
||||
(name.title(), data[err.start:err.end], name)) from None
|
||||
|
||||
def _strip_ipv6_iface(enc_name: bytes) -> bytes:
|
||||
"""Remove interface scope from IPv6 address."""
|
||||
enc_name, percent, _ = enc_name.partition(b"%")
|
||||
if percent:
|
||||
assert enc_name.startswith(b'['), enc_name
|
||||
enc_name += b']'
|
||||
return enc_name
|
||||
|
||||
class HTTPMessage(email.message.Message):
|
||||
# XXX The only usage of this method is in
|
||||
@@ -221,8 +233,9 @@ def _read_headers(fp):
|
||||
break
|
||||
return headers
|
||||
|
||||
def parse_headers(fp, _class=HTTPMessage):
|
||||
"""Parses only RFC2822 headers from a file pointer.
|
||||
def _parse_header_lines(header_lines, _class=HTTPMessage):
|
||||
"""
|
||||
Parses only RFC 5322 headers from header lines.
|
||||
|
||||
email Parser wants to see strings rather than bytes.
|
||||
But a TextIOWrapper around self.rfile would buffer too many bytes
|
||||
@@ -231,10 +244,15 @@ def parse_headers(fp, _class=HTTPMessage):
|
||||
to parse.
|
||||
|
||||
"""
|
||||
headers = _read_headers(fp)
|
||||
hstring = b''.join(headers).decode('iso-8859-1')
|
||||
hstring = b''.join(header_lines).decode('iso-8859-1')
|
||||
return email.parser.Parser(_class=_class).parsestr(hstring)
|
||||
|
||||
def parse_headers(fp, _class=HTTPMessage):
|
||||
"""Parses only RFC 5322 headers from a file pointer."""
|
||||
|
||||
headers = _read_headers(fp)
|
||||
return _parse_header_lines(headers, _class)
|
||||
|
||||
|
||||
class HTTPResponse(io.BufferedIOBase):
|
||||
|
||||
@@ -448,6 +466,7 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
return self.fp is None
|
||||
|
||||
def read(self, amt=None):
|
||||
"""Read and return the response body, or up to the next amt bytes."""
|
||||
if self.fp is None:
|
||||
return b""
|
||||
|
||||
@@ -458,7 +477,7 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
if self.chunked:
|
||||
return self._read_chunked(amt)
|
||||
|
||||
if amt is not None:
|
||||
if amt is not None and amt >= 0:
|
||||
if self.length is not None and amt > self.length:
|
||||
# clip the read to the "end of response"
|
||||
amt = self.length
|
||||
@@ -576,13 +595,11 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
|
||||
def _read_chunked(self, amt=None):
|
||||
assert self.chunked != _UNKNOWN
|
||||
if amt is not None and amt < 0:
|
||||
amt = None
|
||||
value = []
|
||||
try:
|
||||
while True:
|
||||
chunk_left = self._get_chunk_left()
|
||||
if chunk_left is None:
|
||||
break
|
||||
|
||||
while (chunk_left := self._get_chunk_left()) is not None:
|
||||
if amt is not None and amt <= chunk_left:
|
||||
value.append(self._safe_read(amt))
|
||||
self.chunk_left = chunk_left - amt
|
||||
@@ -593,8 +610,8 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
amt -= chunk_left
|
||||
self.chunk_left = 0
|
||||
return b''.join(value)
|
||||
except IncompleteRead:
|
||||
raise IncompleteRead(b''.join(value))
|
||||
except IncompleteRead as exc:
|
||||
raise IncompleteRead(b''.join(value)) from exc
|
||||
|
||||
def _readinto_chunked(self, b):
|
||||
assert self.chunked != _UNKNOWN
|
||||
@@ -627,10 +644,25 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
reading. If the bytes are truly not available (due to EOF), then the
|
||||
IncompleteRead exception can be used to detect the problem.
|
||||
"""
|
||||
data = self.fp.read(amt)
|
||||
if len(data) < amt:
|
||||
raise IncompleteRead(data, amt-len(data))
|
||||
return data
|
||||
cursize = min(amt, _MIN_READ_BUF_SIZE)
|
||||
data = self.fp.read(cursize)
|
||||
if len(data) >= amt:
|
||||
return data
|
||||
if len(data) < cursize:
|
||||
raise IncompleteRead(data, amt - len(data))
|
||||
|
||||
data = io.BytesIO(data)
|
||||
data.seek(0, 2)
|
||||
while True:
|
||||
# This is a geometric increase in read size (never more than
|
||||
# doubling out the current length of data per loop iteration).
|
||||
delta = min(cursize, amt - cursize)
|
||||
data.write(self.fp.read(delta))
|
||||
if data.tell() >= amt:
|
||||
return data.getvalue()
|
||||
cursize += delta
|
||||
if data.tell() < cursize:
|
||||
raise IncompleteRead(data.getvalue(), amt - data.tell())
|
||||
|
||||
def _safe_readinto(self, b):
|
||||
"""Same as _safe_read, but for reading into a buffer."""
|
||||
@@ -655,6 +687,8 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
self._close_conn()
|
||||
elif self.length is not None:
|
||||
self.length -= len(result)
|
||||
if not self.length:
|
||||
self._close_conn()
|
||||
return result
|
||||
|
||||
def peek(self, n=-1):
|
||||
@@ -679,6 +713,8 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
self._close_conn()
|
||||
elif self.length is not None:
|
||||
self.length -= len(result)
|
||||
if not self.length:
|
||||
self._close_conn()
|
||||
return result
|
||||
|
||||
def _read1_chunked(self, n):
|
||||
@@ -786,6 +822,20 @@ class HTTPResponse(io.BufferedIOBase):
|
||||
'''
|
||||
return self.status
|
||||
|
||||
|
||||
def _create_https_context(http_version):
|
||||
# Function also used by urllib.request to be able to set the check_hostname
|
||||
# attribute on a context object.
|
||||
context = ssl._create_default_https_context()
|
||||
# send ALPN extension to indicate HTTP/1.1 protocol
|
||||
if http_version == 11:
|
||||
context.set_alpn_protocols(['http/1.1'])
|
||||
# enable PHA for TLS 1.3 connections if available
|
||||
if context.post_handshake_auth is not None:
|
||||
context.post_handshake_auth = True
|
||||
return context
|
||||
|
||||
|
||||
class HTTPConnection:
|
||||
|
||||
_http_vsn = 11
|
||||
@@ -847,6 +897,7 @@ class HTTPConnection:
|
||||
self._tunnel_host = None
|
||||
self._tunnel_port = None
|
||||
self._tunnel_headers = {}
|
||||
self._raw_proxy_headers = None
|
||||
|
||||
(self.host, self.port) = self._get_hostport(host, port)
|
||||
|
||||
@@ -859,9 +910,9 @@ class HTTPConnection:
|
||||
def set_tunnel(self, host, port=None, headers=None):
|
||||
"""Set up host and port for HTTP CONNECT tunnelling.
|
||||
|
||||
In a connection that uses HTTP CONNECT tunneling, the host passed to the
|
||||
constructor is used as a proxy server that relays all communication to
|
||||
the endpoint passed to `set_tunnel`. This done by sending an HTTP
|
||||
In a connection that uses HTTP CONNECT tunnelling, the host passed to
|
||||
the constructor is used as a proxy server that relays all communication
|
||||
to the endpoint passed to `set_tunnel`. This done by sending an HTTP
|
||||
CONNECT request to the proxy server when the connection is established.
|
||||
|
||||
This method must be called before the HTTP connection has been
|
||||
@@ -869,6 +920,13 @@ class HTTPConnection:
|
||||
|
||||
The headers argument should be a mapping of extra HTTP headers to send
|
||||
with the CONNECT request.
|
||||
|
||||
As HTTP/1.1 is used for HTTP CONNECT tunnelling request, as per the RFC
|
||||
(https://tools.ietf.org/html/rfc7231#section-4.3.6), a HTTP Host:
|
||||
header must be provided, matching the authority-form of the request
|
||||
target provided as the destination for the CONNECT request. If a
|
||||
HTTP Host: header is not provided via the headers argument, one
|
||||
is generated and transmitted automatically.
|
||||
"""
|
||||
|
||||
if self.sock:
|
||||
@@ -876,10 +934,15 @@ class HTTPConnection:
|
||||
|
||||
self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
|
||||
if headers:
|
||||
self._tunnel_headers = headers
|
||||
self._tunnel_headers = headers.copy()
|
||||
else:
|
||||
self._tunnel_headers.clear()
|
||||
|
||||
if not any(header.lower() == "host" for header in self._tunnel_headers):
|
||||
encoded_host = self._tunnel_host.encode("idna").decode("ascii")
|
||||
self._tunnel_headers["Host"] = "%s:%d" % (
|
||||
encoded_host, self._tunnel_port)
|
||||
|
||||
def _get_hostport(self, host, port):
|
||||
if port is None:
|
||||
i = host.rfind(':')
|
||||
@@ -895,17 +958,24 @@ class HTTPConnection:
|
||||
host = host[:i]
|
||||
else:
|
||||
port = self.default_port
|
||||
if host and host[0] == '[' and host[-1] == ']':
|
||||
host = host[1:-1]
|
||||
if host and host[0] == '[' and host[-1] == ']':
|
||||
host = host[1:-1]
|
||||
|
||||
return (host, port)
|
||||
|
||||
def set_debuglevel(self, level):
|
||||
self.debuglevel = level
|
||||
|
||||
def _wrap_ipv6(self, ip):
|
||||
if b':' in ip and ip[0] != b'['[0]:
|
||||
return b"[" + ip + b"]"
|
||||
return ip
|
||||
|
||||
def _tunnel(self):
|
||||
connect = b"CONNECT %s:%d HTTP/1.0\r\n" % (
|
||||
self._tunnel_host.encode("ascii"), self._tunnel_port)
|
||||
connect = b"CONNECT %s:%d %s\r\n" % (
|
||||
self._wrap_ipv6(self._tunnel_host.encode("idna")),
|
||||
self._tunnel_port,
|
||||
self._http_vsn_str.encode("ascii"))
|
||||
headers = [connect]
|
||||
for header, value in self._tunnel_headers.items():
|
||||
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
|
||||
@@ -917,23 +987,35 @@ class HTTPConnection:
|
||||
del headers
|
||||
|
||||
response = self.response_class(self.sock, method=self._method)
|
||||
(version, code, message) = response._read_status()
|
||||
try:
|
||||
(version, code, message) = response._read_status()
|
||||
|
||||
if code != http.HTTPStatus.OK:
|
||||
self.close()
|
||||
raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
|
||||
while True:
|
||||
line = response.fp.readline(_MAXLINE + 1)
|
||||
if len(line) > _MAXLINE:
|
||||
raise LineTooLong("header line")
|
||||
if not line:
|
||||
# for sites which EOF without sending a trailer
|
||||
break
|
||||
if line in (b'\r\n', b'\n', b''):
|
||||
break
|
||||
self._raw_proxy_headers = _read_headers(response.fp)
|
||||
|
||||
if self.debuglevel > 0:
|
||||
print('header:', line.decode())
|
||||
for header in self._raw_proxy_headers:
|
||||
print('header:', header.decode())
|
||||
|
||||
if code != http.HTTPStatus.OK:
|
||||
self.close()
|
||||
raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
|
||||
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
def get_proxy_response_headers(self):
|
||||
"""
|
||||
Returns a dictionary with the headers of the response
|
||||
received from the proxy server to the CONNECT request
|
||||
sent to set the tunnel.
|
||||
|
||||
If the CONNECT request was not sent, the method returns None.
|
||||
"""
|
||||
return (
|
||||
_parse_header_lines(self._raw_proxy_headers)
|
||||
if self._raw_proxy_headers is not None
|
||||
else None
|
||||
)
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the host and port specified in __init__."""
|
||||
@@ -942,7 +1024,7 @@ class HTTPConnection:
|
||||
(self.host,self.port), self.timeout, self.source_address)
|
||||
# Might fail in OSs that don't implement TCP_NODELAY
|
||||
try:
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOPROTOOPT:
|
||||
raise
|
||||
@@ -980,14 +1062,11 @@ class HTTPConnection:
|
||||
print("send:", repr(data))
|
||||
if hasattr(data, "read") :
|
||||
if self.debuglevel > 0:
|
||||
print("sendIng a read()able")
|
||||
print("sending a readable")
|
||||
encode = self._is_textIO(data)
|
||||
if encode and self.debuglevel > 0:
|
||||
print("encoding file using iso-8859-1")
|
||||
while 1:
|
||||
datablock = data.read(self.blocksize)
|
||||
if not datablock:
|
||||
break
|
||||
while datablock := data.read(self.blocksize):
|
||||
if encode:
|
||||
datablock = datablock.encode("iso-8859-1")
|
||||
sys.audit("http.client.send", self, datablock)
|
||||
@@ -1013,14 +1092,11 @@ class HTTPConnection:
|
||||
|
||||
def _read_readable(self, readable):
|
||||
if self.debuglevel > 0:
|
||||
print("sendIng a read()able")
|
||||
print("reading a readable")
|
||||
encode = self._is_textIO(readable)
|
||||
if encode and self.debuglevel > 0:
|
||||
print("encoding file using iso-8859-1")
|
||||
while True:
|
||||
datablock = readable.read(self.blocksize)
|
||||
if not datablock:
|
||||
break
|
||||
while datablock := readable.read(self.blocksize):
|
||||
if encode:
|
||||
datablock = datablock.encode("iso-8859-1")
|
||||
yield datablock
|
||||
@@ -1157,7 +1233,7 @@ class HTTPConnection:
|
||||
netloc_enc = netloc.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
netloc_enc = netloc.encode("idna")
|
||||
self.putheader('Host', netloc_enc)
|
||||
self.putheader('Host', _strip_ipv6_iface(netloc_enc))
|
||||
else:
|
||||
if self._tunnel_host:
|
||||
host = self._tunnel_host
|
||||
@@ -1173,9 +1249,9 @@ class HTTPConnection:
|
||||
|
||||
# As per RFC 273, IPv6 address should be wrapped with []
|
||||
# when used as Host header
|
||||
|
||||
if host.find(':') >= 0:
|
||||
host_enc = b'[' + host_enc + b']'
|
||||
host_enc = self._wrap_ipv6(host_enc)
|
||||
if ":" in host:
|
||||
host_enc = _strip_ipv6_iface(host_enc)
|
||||
|
||||
if port == self.default_port:
|
||||
self.putheader('Host', host_enc)
|
||||
@@ -1400,46 +1476,15 @@ else:
|
||||
|
||||
default_port = HTTPS_PORT
|
||||
|
||||
# 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,
|
||||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
source_address=None, *, context=None,
|
||||
check_hostname=None, blocksize=8192):
|
||||
def __init__(self, host, port=None,
|
||||
*, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
source_address=None, context=None, blocksize=8192):
|
||||
super(HTTPSConnection, self).__init__(host, port, timeout,
|
||||
source_address,
|
||||
blocksize=blocksize)
|
||||
if (key_file is not None or cert_file is not None or
|
||||
check_hostname is not None):
|
||||
import warnings
|
||||
warnings.warn("key_file, cert_file and check_hostname are "
|
||||
"deprecated, use a custom context instead.",
|
||||
DeprecationWarning, 2)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
if context is None:
|
||||
context = ssl._create_default_https_context()
|
||||
# send ALPN extension to indicate HTTP/1.1 protocol
|
||||
if self._http_vsn == 11:
|
||||
context.set_alpn_protocols(['http/1.1'])
|
||||
# enable PHA for TLS 1.3 connections if available
|
||||
if context.post_handshake_auth is not None:
|
||||
context.post_handshake_auth = True
|
||||
will_verify = context.verify_mode != ssl.CERT_NONE
|
||||
if check_hostname is None:
|
||||
check_hostname = context.check_hostname
|
||||
if check_hostname and not will_verify:
|
||||
raise ValueError("check_hostname needs a SSL context with "
|
||||
"either CERT_OPTIONAL or CERT_REQUIRED")
|
||||
if key_file or cert_file:
|
||||
context.load_cert_chain(cert_file, key_file)
|
||||
# cert and key file means the user wants to authenticate.
|
||||
# enable TLS 1.3 PHA implicitly even for custom contexts.
|
||||
if context.post_handshake_auth is not None:
|
||||
context.post_handshake_auth = True
|
||||
context = _create_https_context(self._http_vsn)
|
||||
self._context = context
|
||||
if check_hostname is not None:
|
||||
self._context.check_hostname = check_hostname
|
||||
|
||||
def connect(self):
|
||||
"Connect to a host on a given (SSL) port."
|
||||
|
||||
Vendored
+29
-35
@@ -34,10 +34,7 @@ import datetime
|
||||
import re
|
||||
import time
|
||||
import urllib.parse, urllib.request
|
||||
try:
|
||||
import threading as _threading
|
||||
except ImportError:
|
||||
import dummy_threading as _threading
|
||||
import threading as _threading
|
||||
import http.client # only for the default HTTP port
|
||||
from calendar import timegm
|
||||
|
||||
@@ -92,8 +89,7 @@ def _timegm(tt):
|
||||
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||||
MONTHS_LOWER = []
|
||||
for month in MONTHS: MONTHS_LOWER.append(month.lower())
|
||||
MONTHS_LOWER = [month.lower() for month in MONTHS]
|
||||
|
||||
def time2isoz(t=None):
|
||||
"""Return a string representing time in seconds since epoch, t.
|
||||
@@ -108,9 +104,9 @@ def time2isoz(t=None):
|
||||
|
||||
"""
|
||||
if t is None:
|
||||
dt = datetime.datetime.utcnow()
|
||||
dt = datetime.datetime.now(tz=datetime.UTC)
|
||||
else:
|
||||
dt = datetime.datetime.utcfromtimestamp(t)
|
||||
dt = datetime.datetime.fromtimestamp(t, tz=datetime.UTC)
|
||||
return "%04d-%02d-%02d %02d:%02d:%02dZ" % (
|
||||
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
|
||||
|
||||
@@ -126,9 +122,9 @@ def time2netscape(t=None):
|
||||
|
||||
"""
|
||||
if t is None:
|
||||
dt = datetime.datetime.utcnow()
|
||||
dt = datetime.datetime.now(tz=datetime.UTC)
|
||||
else:
|
||||
dt = datetime.datetime.utcfromtimestamp(t)
|
||||
dt = datetime.datetime.fromtimestamp(t, tz=datetime.UTC)
|
||||
return "%s, %02d-%s-%04d %02d:%02d:%02d GMT" % (
|
||||
DAYS[dt.weekday()], dt.day, MONTHS[dt.month-1],
|
||||
dt.year, dt.hour, dt.minute, dt.second)
|
||||
@@ -434,6 +430,7 @@ def split_header_words(header_values):
|
||||
if pairs: result.append(pairs)
|
||||
return result
|
||||
|
||||
HEADER_JOIN_TOKEN_RE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
|
||||
HEADER_JOIN_ESCAPE_RE = re.compile(r"([\"\\])")
|
||||
def join_header_words(lists):
|
||||
"""Do the inverse (almost) of the conversion done by split_header_words.
|
||||
@@ -441,10 +438,10 @@ def join_header_words(lists):
|
||||
Takes a list of lists of (key, value) pairs and produces a single header
|
||||
value. Attribute values are quoted if needed.
|
||||
|
||||
>>> join_header_words([[("text/plain", None), ("charset", "iso-8859-1")]])
|
||||
'text/plain; charset="iso-8859-1"'
|
||||
>>> join_header_words([[("text/plain", None)], [("charset", "iso-8859-1")]])
|
||||
'text/plain, charset="iso-8859-1"'
|
||||
>>> join_header_words([[("text/plain", None), ("charset", "iso-8859/1")]])
|
||||
'text/plain; charset="iso-8859/1"'
|
||||
>>> join_header_words([[("text/plain", None)], [("charset", "iso-8859/1")]])
|
||||
'text/plain, charset="iso-8859/1"'
|
||||
|
||||
"""
|
||||
headers = []
|
||||
@@ -452,7 +449,7 @@ def join_header_words(lists):
|
||||
attr = []
|
||||
for k, v in pairs:
|
||||
if v is not None:
|
||||
if not re.search(r"^\w+$", v):
|
||||
if not HEADER_JOIN_TOKEN_RE.fullmatch(v):
|
||||
v = HEADER_JOIN_ESCAPE_RE.sub(r"\\\1", v) # escape " and \
|
||||
v = '"%s"' % v
|
||||
k = "%s=%s" % (k, v)
|
||||
@@ -644,7 +641,7 @@ def eff_request_host(request):
|
||||
|
||||
"""
|
||||
erhn = req_host = request_host(request)
|
||||
if req_host.find(".") == -1 and not IPV4_RE.search(req_host):
|
||||
if "." not in req_host:
|
||||
erhn = req_host + ".local"
|
||||
return req_host, erhn
|
||||
|
||||
@@ -1047,12 +1044,13 @@ class DefaultCookiePolicy(CookiePolicy):
|
||||
else:
|
||||
undotted_domain = domain
|
||||
embedded_dots = (undotted_domain.find(".") >= 0)
|
||||
if not embedded_dots and domain != ".local":
|
||||
if not embedded_dots and not erhn.endswith(".local"):
|
||||
_debug(" non-local domain %s contains no embedded dot",
|
||||
domain)
|
||||
return False
|
||||
if cookie.version == 0:
|
||||
if (not erhn.endswith(domain) and
|
||||
if (not (erhn.endswith(domain) or
|
||||
erhn.endswith(f"{undotted_domain}.local")) and
|
||||
(not erhn.startswith(".") and
|
||||
not ("."+erhn).endswith(domain))):
|
||||
_debug(" effective request-host %s (even with added "
|
||||
@@ -1227,14 +1225,9 @@ class DefaultCookiePolicy(CookiePolicy):
|
||||
_debug(" %s does not path-match %s", req_path, path)
|
||||
return False
|
||||
|
||||
def vals_sorted_by_key(adict):
|
||||
keys = sorted(adict.keys())
|
||||
return map(adict.get, keys)
|
||||
|
||||
def deepvalues(mapping):
|
||||
"""Iterates over nested mapping, depth-first, in sorted order by key."""
|
||||
values = vals_sorted_by_key(mapping)
|
||||
for obj in values:
|
||||
"""Iterates over nested mapping, depth-first"""
|
||||
for obj in list(mapping.values()):
|
||||
mapping = False
|
||||
try:
|
||||
obj.items
|
||||
@@ -1898,7 +1891,10 @@ class LWPCookieJar(FileCookieJar):
|
||||
if self.filename is not None: filename = self.filename
|
||||
else: raise ValueError(MISSING_FILENAME_TEXT)
|
||||
|
||||
with open(filename, "w") as f:
|
||||
with os.fdopen(
|
||||
os.open(filename, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600),
|
||||
'w',
|
||||
) as f:
|
||||
# There really isn't an LWP Cookies 2.0 format, but this indicates
|
||||
# that there is extra information in here (domain_dot and
|
||||
# port_spec) while still being compatible with libwww-perl, I hope.
|
||||
@@ -1923,9 +1919,7 @@ class LWPCookieJar(FileCookieJar):
|
||||
"comment", "commenturl")
|
||||
|
||||
try:
|
||||
while 1:
|
||||
line = f.readline()
|
||||
if line == "": break
|
||||
while (line := f.readline()) != "":
|
||||
if not line.startswith(header):
|
||||
continue
|
||||
line = line[len(header):].strip()
|
||||
@@ -1993,7 +1987,7 @@ class MozillaCookieJar(FileCookieJar):
|
||||
|
||||
This class differs from CookieJar only in the format it uses to save and
|
||||
load cookies to and from a file. This class uses the Mozilla/Netscape
|
||||
`cookies.txt' format. lynx uses this file format, too.
|
||||
`cookies.txt' format. curl and lynx use this file format, too.
|
||||
|
||||
Don't expect cookies saved while the browser is running to be noticed by
|
||||
the browser (in fact, Mozilla on unix will overwrite your saved cookies if
|
||||
@@ -2025,12 +2019,9 @@ class MozillaCookieJar(FileCookieJar):
|
||||
filename)
|
||||
|
||||
try:
|
||||
while 1:
|
||||
line = f.readline()
|
||||
while (line := f.readline()) != "":
|
||||
rest = {}
|
||||
|
||||
if line == "": break
|
||||
|
||||
# httponly is a cookie flag as defined in rfc6265
|
||||
# when encoded in a netscape cookie file,
|
||||
# the line is prepended with "#HttpOnly_"
|
||||
@@ -2094,7 +2085,10 @@ class MozillaCookieJar(FileCookieJar):
|
||||
if self.filename is not None: filename = self.filename
|
||||
else: raise ValueError(MISSING_FILENAME_TEXT)
|
||||
|
||||
with open(filename, "w") as f:
|
||||
with os.fdopen(
|
||||
os.open(filename, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600),
|
||||
'w',
|
||||
) as f:
|
||||
f.write(NETSCAPE_HEADER_TEXT)
|
||||
now = time.time()
|
||||
for cookie in self:
|
||||
|
||||
Vendored
+94
-38
@@ -2,18 +2,18 @@
|
||||
|
||||
Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
|
||||
SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST,
|
||||
and CGIHTTPRequestHandler for CGI scripts.
|
||||
and (deprecated) CGIHTTPRequestHandler for CGI scripts.
|
||||
|
||||
It does, however, optionally implement HTTP/1.1 persistent connections,
|
||||
as of version 0.3.
|
||||
It does, however, optionally implement HTTP/1.1 persistent connections.
|
||||
|
||||
Notes on CGIHTTPRequestHandler
|
||||
------------------------------
|
||||
|
||||
This class implements GET and POST requests to cgi-bin scripts.
|
||||
This class is deprecated. It implements GET and POST requests to cgi-bin scripts.
|
||||
|
||||
If the os.fork() function is not present (e.g. on Windows),
|
||||
subprocess.Popen() is used as a fallback, with slightly altered semantics.
|
||||
If the os.fork() function is not present (Windows), subprocess.Popen() is used,
|
||||
with slightly altered but never documented semantics. Use from a threaded
|
||||
process is likely to trigger a warning at os.fork() time.
|
||||
|
||||
In all cases, the implementation is intentionally naive -- all
|
||||
requests are executed synchronously.
|
||||
@@ -93,6 +93,7 @@ import email.utils
|
||||
import html
|
||||
import http.client
|
||||
import io
|
||||
import itertools
|
||||
import mimetypes
|
||||
import os
|
||||
import posixpath
|
||||
@@ -109,11 +110,10 @@ from http import HTTPStatus
|
||||
|
||||
# Default error message template
|
||||
DEFAULT_ERROR_MESSAGE = """\
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<meta charset="utf-8">
|
||||
<title>Error response</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -127,6 +127,10 @@ DEFAULT_ERROR_MESSAGE = """\
|
||||
|
||||
DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
|
||||
|
||||
# Data larger than this will be read in chunks, to prevent extreme
|
||||
# overallocation.
|
||||
_MIN_READ_BUF_SIZE = 1 << 20
|
||||
|
||||
class HTTPServer(socketserver.TCPServer):
|
||||
|
||||
allow_reuse_address = 1 # Seems to make sense in testing environment
|
||||
@@ -275,6 +279,7 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
error response has already been sent back.
|
||||
|
||||
"""
|
||||
is_http_0_9 = False
|
||||
self.command = None # set in case of error on the first line
|
||||
self.request_version = version = self.default_request_version
|
||||
self.close_connection = True
|
||||
@@ -300,6 +305,10 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
# - Leading zeros MUST be ignored by recipients.
|
||||
if len(version_number) != 2:
|
||||
raise ValueError
|
||||
if any(not component.isdigit() for component in version_number):
|
||||
raise ValueError("non digit in http version")
|
||||
if any(len(component) > 10 for component in version_number):
|
||||
raise ValueError("unreasonable length http version")
|
||||
version_number = int(version_number[0]), int(version_number[1])
|
||||
except (ValueError, IndexError):
|
||||
self.send_error(
|
||||
@@ -328,8 +337,21 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Bad HTTP/0.9 request type (%r)" % command)
|
||||
return False
|
||||
is_http_0_9 = True
|
||||
self.command, self.path = command, path
|
||||
|
||||
# gh-87389: The purpose of replacing '//' with '/' is to protect
|
||||
# against open redirect attacks possibly triggered if the path starts
|
||||
# with '//' because http clients treat //path as an absolute URI
|
||||
# without scheme (similar to http://path) rather than a path.
|
||||
if self.path.startswith('//'):
|
||||
self.path = '/' + self.path.lstrip('/') # Reduce to a single /
|
||||
|
||||
# For HTTP/0.9, headers are not expected at all.
|
||||
if is_http_0_9:
|
||||
self.headers = {}
|
||||
return True
|
||||
|
||||
# Examine the headers and look for a Connection directive.
|
||||
try:
|
||||
self.headers = http.client.parse_headers(self.rfile,
|
||||
@@ -556,6 +578,11 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
|
||||
self.log_message(format, *args)
|
||||
|
||||
# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
|
||||
_control_char_table = str.maketrans(
|
||||
{c: fr'\x{c:02x}' for c in itertools.chain(range(0x20), range(0x7f,0xa0))})
|
||||
_control_char_table[ord('\\')] = r'\\'
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Log an arbitrary message.
|
||||
|
||||
@@ -571,12 +598,16 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
|
||||
The client ip and current date/time are prefixed to
|
||||
every message.
|
||||
|
||||
Unicode control characters are replaced with escaped hex
|
||||
before writing the output to stderr.
|
||||
|
||||
"""
|
||||
|
||||
message = format % args
|
||||
sys.stderr.write("%s - - [%s] %s\n" %
|
||||
(self.address_string(),
|
||||
self.log_date_time_string(),
|
||||
format%args))
|
||||
message.translate(self._control_char_table)))
|
||||
|
||||
def version_string(self):
|
||||
"""Return the server software version string."""
|
||||
@@ -637,6 +668,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
"""
|
||||
|
||||
server_version = "SimpleHTTP/" + __version__
|
||||
index_pages = ("index.html", "index.htm")
|
||||
extensions_map = _encodings_map_default = {
|
||||
'.gz': 'application/gzip',
|
||||
'.Z': 'application/octet-stream',
|
||||
@@ -680,7 +712,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
f = None
|
||||
if os.path.isdir(path):
|
||||
parts = urllib.parse.urlsplit(self.path)
|
||||
if not parts.path.endswith('/'):
|
||||
if not parts.path.endswith(('/', '%2f', '%2F')):
|
||||
# redirect browser - doing basically what apache does
|
||||
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
|
||||
new_parts = (parts[0], parts[1], parts[2] + '/',
|
||||
@@ -690,9 +722,9 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return None
|
||||
for index in "index.html", "index.htm":
|
||||
for index in self.index_pages:
|
||||
index = os.path.join(path, index)
|
||||
if os.path.exists(index):
|
||||
if os.path.isfile(index):
|
||||
path = index
|
||||
break
|
||||
else:
|
||||
@@ -702,7 +734,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
# The test for this was added in test_httpserver.py
|
||||
# However, some OS platforms accept a trailingSlash as a filename
|
||||
# See discussion on python-dev and Issue34711 regarding
|
||||
# parseing and rejection of filenames with a trailing slash
|
||||
# parsing and rejection of filenames with a trailing slash
|
||||
if path.endswith("/"):
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
||||
return None
|
||||
@@ -770,21 +802,23 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
list.sort(key=lambda a: a.lower())
|
||||
r = []
|
||||
displaypath = self.path
|
||||
displaypath = displaypath.split('#', 1)[0]
|
||||
displaypath = displaypath.split('?', 1)[0]
|
||||
try:
|
||||
displaypath = urllib.parse.unquote(self.path,
|
||||
displaypath = urllib.parse.unquote(displaypath,
|
||||
errors='surrogatepass')
|
||||
except UnicodeDecodeError:
|
||||
displaypath = urllib.parse.unquote(path)
|
||||
displaypath = urllib.parse.unquote(displaypath)
|
||||
displaypath = html.escape(displaypath, quote=False)
|
||||
enc = sys.getfilesystemencoding()
|
||||
title = 'Directory listing for %s' % displaypath
|
||||
r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
|
||||
'"http://www.w3.org/TR/html4/strict.dtd">')
|
||||
r.append('<html>\n<head>')
|
||||
r.append('<meta http-equiv="Content-Type" '
|
||||
'content="text/html; charset=%s">' % enc)
|
||||
r.append('<title>%s</title>\n</head>' % title)
|
||||
r.append('<body>\n<h1>%s</h1>' % title)
|
||||
title = f'Directory listing for {displaypath}'
|
||||
r.append('<!DOCTYPE HTML>')
|
||||
r.append('<html lang="en">')
|
||||
r.append('<head>')
|
||||
r.append(f'<meta charset="{enc}">')
|
||||
r.append(f'<title>{title}</title>\n</head>')
|
||||
r.append(f'<body>\n<h1>{title}</h1>')
|
||||
r.append('<hr>\n<ul>')
|
||||
for name in list:
|
||||
fullname = os.path.join(path, name)
|
||||
@@ -820,14 +854,14 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
|
||||
"""
|
||||
# abandon query parameters
|
||||
path = path.split('?',1)[0]
|
||||
path = path.split('#',1)[0]
|
||||
path = path.split('#', 1)[0]
|
||||
path = path.split('?', 1)[0]
|
||||
# Don't forget explicit trailing slash when normalizing. Issue17324
|
||||
trailing_slash = path.rstrip().endswith('/')
|
||||
try:
|
||||
path = urllib.parse.unquote(path, errors='surrogatepass')
|
||||
except UnicodeDecodeError:
|
||||
path = urllib.parse.unquote(path)
|
||||
trailing_slash = path.endswith('/')
|
||||
path = posixpath.normpath(path)
|
||||
words = path.split('/')
|
||||
words = filter(None, words)
|
||||
@@ -877,7 +911,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||
ext = ext.lower()
|
||||
if ext in self.extensions_map:
|
||||
return self.extensions_map[ext]
|
||||
guess, _ = mimetypes.guess_type(path)
|
||||
guess, _ = mimetypes.guess_file_type(path)
|
||||
if guess:
|
||||
return guess
|
||||
return 'application/octet-stream'
|
||||
@@ -966,6 +1000,12 @@ class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
import warnings
|
||||
warnings._deprecated("http.server.CGIHTTPRequestHandler",
|
||||
remove=(3, 15))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Determine platform specifics
|
||||
have_fork = hasattr(os, 'fork')
|
||||
|
||||
@@ -1078,7 +1118,7 @@ class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
|
||||
"CGI script is not executable (%r)" % scriptname)
|
||||
return
|
||||
|
||||
# Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
|
||||
# Reference: https://www6.uniovi.es/~antonio/ncsa_httpd/cgi/env.html
|
||||
# XXX Much of the following could be prepared ahead of time!
|
||||
env = copy.deepcopy(os.environ)
|
||||
env['SERVER_SOFTWARE'] = self.version_string()
|
||||
@@ -1198,7 +1238,18 @@ class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
|
||||
env = env
|
||||
)
|
||||
if self.command.lower() == "post" and nbytes > 0:
|
||||
data = self.rfile.read(nbytes)
|
||||
cursize = 0
|
||||
data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE))
|
||||
while len(data) < nbytes and len(data) != cursize:
|
||||
cursize = len(data)
|
||||
# This is a geometric increase in read size (never more
|
||||
# than doubling out the current length of data per loop
|
||||
# iteration).
|
||||
delta = min(cursize, nbytes - cursize)
|
||||
try:
|
||||
data += self.rfile.read(delta)
|
||||
except TimeoutError:
|
||||
break
|
||||
else:
|
||||
data = None
|
||||
# throw away additional data [see bug #427345]
|
||||
@@ -1258,15 +1309,19 @@ if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cgi', action='store_true',
|
||||
help='run as CGI server')
|
||||
parser.add_argument('--bind', '-b', metavar='ADDRESS',
|
||||
help='specify alternate bind address '
|
||||
parser.add_argument('-b', '--bind', metavar='ADDRESS',
|
||||
help='bind to this address '
|
||||
'(default: all interfaces)')
|
||||
parser.add_argument('--directory', '-d', default=os.getcwd(),
|
||||
help='specify alternate directory '
|
||||
parser.add_argument('-d', '--directory', default=os.getcwd(),
|
||||
help='serve this directory '
|
||||
'(default: current directory)')
|
||||
parser.add_argument('port', action='store', default=8000, type=int,
|
||||
nargs='?',
|
||||
help='specify alternate port (default: 8000)')
|
||||
parser.add_argument('-p', '--protocol', metavar='VERSION',
|
||||
default='HTTP/1.0',
|
||||
help='conform to this HTTP version '
|
||||
'(default: %(default)s)')
|
||||
parser.add_argument('port', default=8000, type=int, nargs='?',
|
||||
help='bind to this port '
|
||||
'(default: %(default)s)')
|
||||
args = parser.parse_args()
|
||||
if args.cgi:
|
||||
handler_class = CGIHTTPRequestHandler
|
||||
@@ -1292,4 +1347,5 @@ if __name__ == '__main__':
|
||||
ServerClass=DualStackServer,
|
||||
port=args.port,
|
||||
bind=args.bind,
|
||||
protocol=args.protocol,
|
||||
)
|
||||
Vendored
+214
-120
@@ -1,14 +1,16 @@
|
||||
"""Tests for http/cookiejar.py."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import re
|
||||
import test.support
|
||||
from test import support
|
||||
from test.support import os_helper
|
||||
from test.support import warnings_helper
|
||||
from test.support.testcase import ExtraAssertions
|
||||
import time
|
||||
import unittest
|
||||
import urllib.request
|
||||
import pathlib
|
||||
|
||||
from http.cookiejar import (time2isoz, http2time, iso2time, time2netscape,
|
||||
parse_ns_headers, join_header_words, split_header_words, Cookie,
|
||||
@@ -17,6 +19,7 @@ from http.cookiejar import (time2isoz, http2time, iso2time, time2netscape,
|
||||
reach, is_HDN, domain_match, user_domain_match, request_path,
|
||||
request_port, request_host)
|
||||
|
||||
mswindows = (sys.platform == "win32")
|
||||
|
||||
class DateTimeTests(unittest.TestCase):
|
||||
|
||||
@@ -104,8 +107,7 @@ class DateTimeTests(unittest.TestCase):
|
||||
self.assertEqual(http2time(s.lower()), test_t, s.lower())
|
||||
self.assertEqual(http2time(s.upper()), test_t, s.upper())
|
||||
|
||||
def test_http2time_garbage(self):
|
||||
for test in [
|
||||
@support.subTests('test', [
|
||||
'',
|
||||
'Garbage',
|
||||
'Mandag 16. September 1996',
|
||||
@@ -120,10 +122,9 @@ class DateTimeTests(unittest.TestCase):
|
||||
'08-01-3697739',
|
||||
'09 Feb 19942632 22:23:32 GMT',
|
||||
'Wed, 09 Feb 1994834 22:23:32 GMT',
|
||||
]:
|
||||
self.assertIsNone(http2time(test),
|
||||
"http2time(%s) is not None\n"
|
||||
"http2time(test) %s" % (test, http2time(test)))
|
||||
])
|
||||
def test_http2time_garbage(self, test):
|
||||
self.assertIsNone(http2time(test))
|
||||
|
||||
def test_http2time_redos_regression_actually_completes(self):
|
||||
# LOOSE_HTTP_DATE_RE was vulnerable to malicious input which caused catastrophic backtracking (REDoS).
|
||||
@@ -148,9 +149,7 @@ class DateTimeTests(unittest.TestCase):
|
||||
self.assertEqual(parse_date("1994-02-03 19:45:29 +0530"),
|
||||
(1994, 2, 3, 14, 15, 29))
|
||||
|
||||
def test_iso2time_formats(self):
|
||||
# test iso2time for supported dates.
|
||||
tests = [
|
||||
@support.subTests('s', [
|
||||
'1994-02-03 00:00:00 -0000', # ISO 8601 format
|
||||
'1994-02-03 00:00:00 +0000', # ISO 8601 format
|
||||
'1994-02-03 00:00:00', # zone is optional
|
||||
@@ -163,16 +162,15 @@ class DateTimeTests(unittest.TestCase):
|
||||
# A few tests with extra space at various places
|
||||
' 1994-02-03 ',
|
||||
' 1994-02-03T00:00:00 ',
|
||||
]
|
||||
|
||||
])
|
||||
def test_iso2time_formats(self, s):
|
||||
# test iso2time for supported dates.
|
||||
test_t = 760233600 # assume broken POSIX counting of seconds
|
||||
for s in tests:
|
||||
self.assertEqual(iso2time(s), test_t, s)
|
||||
self.assertEqual(iso2time(s.lower()), test_t, s.lower())
|
||||
self.assertEqual(iso2time(s.upper()), test_t, s.upper())
|
||||
self.assertEqual(iso2time(s), test_t, s)
|
||||
self.assertEqual(iso2time(s.lower()), test_t, s.lower())
|
||||
self.assertEqual(iso2time(s.upper()), test_t, s.upper())
|
||||
|
||||
def test_iso2time_garbage(self):
|
||||
for test in [
|
||||
@support.subTests('test', [
|
||||
'',
|
||||
'Garbage',
|
||||
'Thursday, 03-Feb-94 00:00:00 GMT',
|
||||
@@ -185,11 +183,10 @@ class DateTimeTests(unittest.TestCase):
|
||||
'01-01-1980 00:00:62',
|
||||
'01-01-1980T00:00:62',
|
||||
'19800101T250000Z',
|
||||
]:
|
||||
self.assertIsNone(iso2time(test),
|
||||
"iso2time(%r)" % test)
|
||||
])
|
||||
def test_iso2time_garbage(self, test):
|
||||
self.assertIsNone(iso2time(test))
|
||||
|
||||
@unittest.skip("TODO, RUSTPYTHON, regressed to quadratic complexity")
|
||||
def test_iso2time_performance_regression(self):
|
||||
# If ISO_DATE_RE regresses to quadratic complexity, this test will take a very long time to succeed.
|
||||
# If fixed, it should complete within a fraction of a second.
|
||||
@@ -199,24 +196,23 @@ class DateTimeTests(unittest.TestCase):
|
||||
|
||||
class HeaderTests(unittest.TestCase):
|
||||
|
||||
def test_parse_ns_headers(self):
|
||||
# quotes should be stripped
|
||||
expected = [[('foo', 'bar'), ('expires', 2209069412), ('version', '0')]]
|
||||
for hdr in [
|
||||
@support.subTests('hdr', [
|
||||
'foo=bar; expires=01 Jan 2040 22:23:32 GMT',
|
||||
'foo=bar; expires="01 Jan 2040 22:23:32 GMT"',
|
||||
]:
|
||||
self.assertEqual(parse_ns_headers([hdr]), expected)
|
||||
|
||||
def test_parse_ns_headers_version(self):
|
||||
|
||||
])
|
||||
def test_parse_ns_headers(self, hdr):
|
||||
# quotes should be stripped
|
||||
expected = [[('foo', 'bar'), ('version', '1')]]
|
||||
for hdr in [
|
||||
expected = [[('foo', 'bar'), ('expires', 2209069412), ('version', '0')]]
|
||||
self.assertEqual(parse_ns_headers([hdr]), expected)
|
||||
|
||||
@support.subTests('hdr', [
|
||||
'foo=bar; version="1"',
|
||||
'foo=bar; Version="1"',
|
||||
]:
|
||||
self.assertEqual(parse_ns_headers([hdr]), expected)
|
||||
])
|
||||
def test_parse_ns_headers_version(self, hdr):
|
||||
# quotes should be stripped
|
||||
expected = [[('foo', 'bar'), ('version', '1')]]
|
||||
self.assertEqual(parse_ns_headers([hdr]), expected)
|
||||
|
||||
def test_parse_ns_headers_special_names(self):
|
||||
# names such as 'expires' are not special in first name=value pair
|
||||
@@ -232,8 +228,7 @@ class HeaderTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(join_header_words([[]]), "")
|
||||
|
||||
def test_split_header_words(self):
|
||||
tests = [
|
||||
@support.subTests('arg,expect', [
|
||||
("foo", [[("foo", None)]]),
|
||||
("foo=bar", [[("foo", "bar")]]),
|
||||
(" foo ", [[("foo", None)]]),
|
||||
@@ -250,24 +245,22 @@ class HeaderTests(unittest.TestCase):
|
||||
(r'foo; bar=baz, spam=, foo="\,\;\"", bar= ',
|
||||
[[("foo", None), ("bar", "baz")],
|
||||
[("spam", "")], [("foo", ',;"')], [("bar", "")]]),
|
||||
]
|
||||
|
||||
for arg, expect in tests:
|
||||
try:
|
||||
result = split_header_words([arg])
|
||||
except:
|
||||
import traceback, io
|
||||
f = io.StringIO()
|
||||
traceback.print_exc(None, f)
|
||||
result = "(error -- traceback follows)\n\n%s" % f.getvalue()
|
||||
self.assertEqual(result, expect, """
|
||||
])
|
||||
def test_split_header_words(self, arg, expect):
|
||||
try:
|
||||
result = split_header_words([arg])
|
||||
except:
|
||||
import traceback, io
|
||||
f = io.StringIO()
|
||||
traceback.print_exc(None, f)
|
||||
result = "(error -- traceback follows)\n\n%s" % f.getvalue()
|
||||
self.assertEqual(result, expect, """
|
||||
When parsing: '%s'
|
||||
Expected: '%s'
|
||||
Got: '%s'
|
||||
""" % (arg, expect, result))
|
||||
|
||||
def test_roundtrip(self):
|
||||
tests = [
|
||||
@support.subTests('arg,expect', [
|
||||
("foo", "foo"),
|
||||
("foo=bar", "foo=bar"),
|
||||
(" foo ", "foo"),
|
||||
@@ -276,23 +269,35 @@ Got: '%s'
|
||||
("foo=bar;bar=baz", "foo=bar; bar=baz"),
|
||||
('foo bar baz', "foo; bar; baz"),
|
||||
(r'foo="\"" bar="\\"', r'foo="\""; bar="\\"'),
|
||||
("föo=bär", 'föo="bär"'),
|
||||
('foo,,,bar', 'foo, bar'),
|
||||
('foo=bar,bar=baz', 'foo=bar, bar=baz'),
|
||||
("foo=\n", 'foo=""'),
|
||||
('foo="\n"', 'foo="\n"'),
|
||||
('foo=bar\n', 'foo=bar'),
|
||||
('foo="bar\n"', 'foo="bar\n"'),
|
||||
('foo=bar\nbaz', 'foo=bar; baz'),
|
||||
('foo="bar\nbaz"', 'foo="bar\nbaz"'),
|
||||
|
||||
('text/html; charset=iso-8859-1',
|
||||
'text/html; charset="iso-8859-1"'),
|
||||
'text/html; charset=iso-8859-1'),
|
||||
|
||||
('text/html; charset="iso-8859/1"',
|
||||
'text/html; charset="iso-8859/1"'),
|
||||
|
||||
('foo="bar"; port="80,81"; discard, bar=baz',
|
||||
'foo=bar; port="80,81"; discard, bar=baz'),
|
||||
|
||||
(r'Basic realm="\"foo\\\\bar\""',
|
||||
r'Basic; realm="\"foo\\\\bar\""')
|
||||
]
|
||||
r'Basic; realm="\"foo\\\\bar\""'),
|
||||
|
||||
for arg, expect in tests:
|
||||
input = split_header_words([arg])
|
||||
res = join_header_words(input)
|
||||
self.assertEqual(res, expect, """
|
||||
('n; foo="foo;_", bar="foo,_"',
|
||||
'n; foo="foo;_", bar="foo,_"'),
|
||||
])
|
||||
def test_roundtrip(self, arg, expect):
|
||||
input = split_header_words([arg])
|
||||
res = join_header_words(input)
|
||||
self.assertEqual(res, expect, """
|
||||
When parsing: '%s'
|
||||
Expected: '%s'
|
||||
Got: '%s'
|
||||
@@ -336,9 +341,9 @@ class FileCookieJarTests(unittest.TestCase):
|
||||
self.assertEqual(c.filename, filename)
|
||||
|
||||
def test_constructor_with_path_like(self):
|
||||
filename = pathlib.Path(os_helper.TESTFN)
|
||||
c = LWPCookieJar(filename)
|
||||
self.assertEqual(c.filename, os.fspath(filename))
|
||||
filename = os_helper.TESTFN
|
||||
c = LWPCookieJar(os_helper.FakePath(filename))
|
||||
self.assertEqual(c.filename, filename)
|
||||
|
||||
def test_constructor_with_none(self):
|
||||
c = LWPCookieJar(None)
|
||||
@@ -365,10 +370,63 @@ class FileCookieJarTests(unittest.TestCase):
|
||||
c = LWPCookieJar()
|
||||
c.load(filename, ignore_discard=True)
|
||||
finally:
|
||||
try: os.unlink(filename)
|
||||
except OSError: pass
|
||||
os_helper.unlink(filename)
|
||||
self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
|
||||
|
||||
@unittest.skipIf(mswindows, "windows file permissions are incompatible with file modes")
|
||||
@os_helper.skip_unless_working_chmod
|
||||
def test_lwp_filepermissions(self):
|
||||
# Cookie file should only be readable by the creator
|
||||
filename = os_helper.TESTFN
|
||||
c = LWPCookieJar()
|
||||
interact_netscape(c, "http://www.acme.com/", 'boo')
|
||||
try:
|
||||
c.save(filename, ignore_discard=True)
|
||||
st = os.stat(filename)
|
||||
self.assertEqual(stat.S_IMODE(st.st_mode), 0o600)
|
||||
finally:
|
||||
os_helper.unlink(filename)
|
||||
|
||||
@unittest.skipIf(mswindows, "windows file permissions are incompatible with file modes")
|
||||
@os_helper.skip_unless_working_chmod
|
||||
def test_mozilla_filepermissions(self):
|
||||
# Cookie file should only be readable by the creator
|
||||
filename = os_helper.TESTFN
|
||||
c = MozillaCookieJar()
|
||||
interact_netscape(c, "http://www.acme.com/", 'boo')
|
||||
try:
|
||||
c.save(filename, ignore_discard=True)
|
||||
st = os.stat(filename)
|
||||
self.assertEqual(stat.S_IMODE(st.st_mode), 0o600)
|
||||
finally:
|
||||
os_helper.unlink(filename)
|
||||
|
||||
@unittest.skipIf(mswindows, "windows file permissions are incompatible with file modes")
|
||||
@os_helper.skip_unless_working_chmod
|
||||
def test_cookie_files_are_truncated(self):
|
||||
filename = os_helper.TESTFN
|
||||
for cookiejar_class in (LWPCookieJar, MozillaCookieJar):
|
||||
c = cookiejar_class(filename)
|
||||
|
||||
req = urllib.request.Request("http://www.acme.com/")
|
||||
headers = ["Set-Cookie: pll_lang=en; Max-Age=31536000; path=/"]
|
||||
res = FakeResponse(headers, "http://www.acme.com/")
|
||||
c.extract_cookies(res, req)
|
||||
self.assertEqual(len(c), 1)
|
||||
|
||||
try:
|
||||
# Save the first version with contents:
|
||||
c.save()
|
||||
# Now, clear cookies and re-save:
|
||||
c.clear()
|
||||
c.save()
|
||||
# Check that file was truncated:
|
||||
c.load()
|
||||
finally:
|
||||
os_helper.unlink(filename)
|
||||
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_bad_magic(self):
|
||||
# OSErrors (eg. file doesn't exist) are allowed to propagate
|
||||
filename = os_helper.TESTFN
|
||||
@@ -392,8 +450,7 @@ class FileCookieJarTests(unittest.TestCase):
|
||||
c = cookiejar_class()
|
||||
self.assertRaises(LoadError, c.load, filename)
|
||||
finally:
|
||||
try: os.unlink(filename)
|
||||
except OSError: pass
|
||||
os_helper.unlink(filename)
|
||||
|
||||
class CookieTests(unittest.TestCase):
|
||||
# XXX
|
||||
@@ -442,14 +499,7 @@ class CookieTests(unittest.TestCase):
|
||||
## just the 7 special TLD's listed in their spec. And folks rely on
|
||||
## that...
|
||||
|
||||
def test_domain_return_ok(self):
|
||||
# test optimization: .domain_return_ok() should filter out most
|
||||
# domains in the CookieJar before we try to access them (because that
|
||||
# may require disk access -- in particular, with MSIECookieJar)
|
||||
# This is only a rough check for performance reasons, so it's not too
|
||||
# critical as long as it's sufficiently liberal.
|
||||
pol = DefaultCookiePolicy()
|
||||
for url, domain, ok in [
|
||||
@support.subTests('url,domain,ok', [
|
||||
("http://foo.bar.com/", "blah.com", False),
|
||||
("http://foo.bar.com/", "rhubarb.blah.com", False),
|
||||
("http://foo.bar.com/", "rhubarb.foo.bar.com", False),
|
||||
@@ -469,11 +519,18 @@ class CookieTests(unittest.TestCase):
|
||||
("http://foo/", ".local", True),
|
||||
("http://barfoo.com", ".foo.com", False),
|
||||
("http://barfoo.com", "foo.com", False),
|
||||
]:
|
||||
request = urllib.request.Request(url)
|
||||
r = pol.domain_return_ok(domain, request)
|
||||
if ok: self.assertTrue(r)
|
||||
else: self.assertFalse(r)
|
||||
])
|
||||
def test_domain_return_ok(self, url, domain, ok):
|
||||
# test optimization: .domain_return_ok() should filter out most
|
||||
# domains in the CookieJar before we try to access them (because that
|
||||
# may require disk access -- in particular, with MSIECookieJar)
|
||||
# This is only a rough check for performance reasons, so it's not too
|
||||
# critical as long as it's sufficiently liberal.
|
||||
pol = DefaultCookiePolicy()
|
||||
request = urllib.request.Request(url)
|
||||
r = pol.domain_return_ok(domain, request)
|
||||
if ok: self.assertTrue(r)
|
||||
else: self.assertFalse(r)
|
||||
|
||||
def test_missing_value(self):
|
||||
# missing = sign in Cookie: header is regarded by Mozilla as a missing
|
||||
@@ -489,7 +546,7 @@ class CookieTests(unittest.TestCase):
|
||||
self.assertIsNone(cookie.value)
|
||||
self.assertEqual(cookie.name, '"spam"')
|
||||
self.assertEqual(lwp_cookie_str(cookie), (
|
||||
r'"spam"; path="/foo/"; domain="www.acme.com"; '
|
||||
r'"spam"; path="/foo/"; domain=www.acme.com; '
|
||||
'path_spec; discard; version=0'))
|
||||
old_str = repr(c)
|
||||
c.save(ignore_expires=True, ignore_discard=True)
|
||||
@@ -497,7 +554,7 @@ class CookieTests(unittest.TestCase):
|
||||
c = MozillaCookieJar(filename)
|
||||
c.revert(ignore_expires=True, ignore_discard=True)
|
||||
finally:
|
||||
os.unlink(c.filename)
|
||||
os_helper.unlink(c.filename)
|
||||
# cookies unchanged apart from lost info re. whether path was specified
|
||||
self.assertEqual(
|
||||
repr(c),
|
||||
@@ -507,10 +564,7 @@ class CookieTests(unittest.TestCase):
|
||||
self.assertEqual(interact_netscape(c, "http://www.acme.com/foo/"),
|
||||
'"spam"; eggs')
|
||||
|
||||
def test_rfc2109_handling(self):
|
||||
# RFC 2109 cookies are handled as RFC 2965 or Netscape cookies,
|
||||
# dependent on policy settings
|
||||
for rfc2109_as_netscape, rfc2965, version in [
|
||||
@support.subTests('rfc2109_as_netscape,rfc2965,version', [
|
||||
# default according to rfc2965 if not explicitly specified
|
||||
(None, False, 0),
|
||||
(None, True, 1),
|
||||
@@ -519,24 +573,27 @@ class CookieTests(unittest.TestCase):
|
||||
(False, True, 1),
|
||||
(True, False, 0),
|
||||
(True, True, 0),
|
||||
]:
|
||||
policy = DefaultCookiePolicy(
|
||||
rfc2109_as_netscape=rfc2109_as_netscape,
|
||||
rfc2965=rfc2965)
|
||||
c = CookieJar(policy)
|
||||
interact_netscape(c, "http://www.example.com/", "ni=ni; Version=1")
|
||||
try:
|
||||
cookie = c._cookies["www.example.com"]["/"]["ni"]
|
||||
except KeyError:
|
||||
self.assertIsNone(version) # didn't expect a stored cookie
|
||||
else:
|
||||
self.assertEqual(cookie.version, version)
|
||||
# 2965 cookies are unaffected
|
||||
interact_2965(c, "http://www.example.com/",
|
||||
"foo=bar; Version=1")
|
||||
if rfc2965:
|
||||
cookie2965 = c._cookies["www.example.com"]["/"]["foo"]
|
||||
self.assertEqual(cookie2965.version, 1)
|
||||
])
|
||||
def test_rfc2109_handling(self, rfc2109_as_netscape, rfc2965, version):
|
||||
# RFC 2109 cookies are handled as RFC 2965 or Netscape cookies,
|
||||
# dependent on policy settings
|
||||
policy = DefaultCookiePolicy(
|
||||
rfc2109_as_netscape=rfc2109_as_netscape,
|
||||
rfc2965=rfc2965)
|
||||
c = CookieJar(policy)
|
||||
interact_netscape(c, "http://www.example.com/", "ni=ni; Version=1")
|
||||
try:
|
||||
cookie = c._cookies["www.example.com"]["/"]["ni"]
|
||||
except KeyError:
|
||||
self.assertIsNone(version) # didn't expect a stored cookie
|
||||
else:
|
||||
self.assertEqual(cookie.version, version)
|
||||
# 2965 cookies are unaffected
|
||||
interact_2965(c, "http://www.example.com/",
|
||||
"foo=bar; Version=1")
|
||||
if rfc2965:
|
||||
cookie2965 = c._cookies["www.example.com"]["/"]["foo"]
|
||||
self.assertEqual(cookie2965.version, 1)
|
||||
|
||||
def test_ns_parser(self):
|
||||
c = CookieJar()
|
||||
@@ -597,8 +654,6 @@ class CookieTests(unittest.TestCase):
|
||||
self.assertIn('expires', cookies)
|
||||
self.assertIn('version', cookies)
|
||||
|
||||
# TODO: RUSTPYTHON; need to update http library to remove warnings
|
||||
@unittest.expectedFailure
|
||||
def test_expires(self):
|
||||
# if expires is in future, keep cookie...
|
||||
c = CookieJar()
|
||||
@@ -706,8 +761,7 @@ class CookieTests(unittest.TestCase):
|
||||
# Cookie is sent back to the same URI.
|
||||
self.assertEqual(interact_netscape(cj, uri), value)
|
||||
|
||||
def test_escape_path(self):
|
||||
cases = [
|
||||
@support.subTests('arg,result', [
|
||||
# quoted safe
|
||||
("/foo%2f/bar", "/foo%2F/bar"),
|
||||
("/foo%2F/bar", "/foo%2F/bar"),
|
||||
@@ -727,9 +781,9 @@ class CookieTests(unittest.TestCase):
|
||||
("/foo/bar\u00fc", "/foo/bar%C3%BC"), # UTF-8 encoded
|
||||
# unicode
|
||||
("/foo/bar\uabcd", "/foo/bar%EA%AF%8D"), # UTF-8 encoded
|
||||
]
|
||||
for arg, result in cases:
|
||||
self.assertEqual(escape_path(arg), result)
|
||||
])
|
||||
def test_escape_path(self, arg, result):
|
||||
self.assertEqual(escape_path(arg), result)
|
||||
|
||||
def test_request_path(self):
|
||||
# with parameters
|
||||
@@ -923,6 +977,48 @@ class CookieTests(unittest.TestCase):
|
||||
## self.assertEqual(len(c), 2)
|
||||
self.assertEqual(len(c), 4)
|
||||
|
||||
def test_localhost_domain(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://localhost", "foo=bar; domain=localhost;")
|
||||
|
||||
self.assertEqual(len(c), 1)
|
||||
|
||||
def test_localhost_domain_contents(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://localhost", "foo=bar; domain=localhost;")
|
||||
|
||||
self.assertEqual(c._cookies[".localhost"]["/"]["foo"].value, "bar")
|
||||
|
||||
def test_localhost_domain_contents_2(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://localhost", "foo=bar;")
|
||||
|
||||
self.assertEqual(c._cookies["localhost.local"]["/"]["foo"].value, "bar")
|
||||
|
||||
def test_evil_nonlocal_domain(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://evil.com", "foo=bar; domain=.localhost")
|
||||
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_evil_local_domain(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://localhost", "foo=bar; domain=.evil.com")
|
||||
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_evil_local_domain_2(self):
|
||||
c = CookieJar()
|
||||
|
||||
interact_netscape(c, "http://localhost", "foo=bar; domain=.someother.local")
|
||||
|
||||
self.assertEqual(len(c), 0)
|
||||
|
||||
def test_two_component_domain_rfc2965(self):
|
||||
pol = DefaultCookiePolicy(rfc2965=True)
|
||||
c = CookieJar(pol)
|
||||
@@ -1254,11 +1350,11 @@ class CookieTests(unittest.TestCase):
|
||||
r'port="90,100, 80,8080"; '
|
||||
r'max-age=100; Comment = "Just kidding! (\"|\\\\) "')
|
||||
|
||||
versions = [1, 1, 1, 0, 1]
|
||||
names = ["bang", "foo", "foo", "spam", "foo"]
|
||||
domains = [".sol.no", "blah.spam.org", "www.acme.com",
|
||||
"www.acme.com", "www.acme.com"]
|
||||
paths = ["/", "/", "/", "/blah", "/blah/"]
|
||||
versions = [1, 0, 1, 1, 1]
|
||||
names = ["foo", "spam", "foo", "foo", "bang"]
|
||||
domains = ["blah.spam.org", "www.acme.com", "www.acme.com",
|
||||
"www.acme.com", ".sol.no"]
|
||||
paths = ["/", "/blah", "/blah/", "/", "/"]
|
||||
|
||||
for i in range(4):
|
||||
i = 0
|
||||
@@ -1331,7 +1427,7 @@ class CookieTests(unittest.TestCase):
|
||||
self.assertIsNone(cookie.expires)
|
||||
|
||||
|
||||
class LWPCookieTests(unittest.TestCase):
|
||||
class LWPCookieTests(unittest.TestCase, ExtraAssertions):
|
||||
# Tests taken from libwww-perl, with a few modifications and additions.
|
||||
|
||||
def test_netscape_example_1(self):
|
||||
@@ -1423,7 +1519,7 @@ class LWPCookieTests(unittest.TestCase):
|
||||
h = req.get_header("Cookie")
|
||||
self.assertIn("PART_NUMBER=ROCKET_LAUNCHER_0001", h)
|
||||
self.assertIn("CUSTOMER=WILE_E_COYOTE", h)
|
||||
self.assertTrue(h.startswith("SHIPPING=FEDEX;"))
|
||||
self.assertStartsWith(h, "SHIPPING=FEDEX;")
|
||||
|
||||
def test_netscape_example_2(self):
|
||||
# Second Example transaction sequence:
|
||||
@@ -1727,8 +1823,7 @@ class LWPCookieTests(unittest.TestCase):
|
||||
c = LWPCookieJar(policy=pol)
|
||||
c.load(filename, ignore_discard=True)
|
||||
finally:
|
||||
try: os.unlink(filename)
|
||||
except OSError: pass
|
||||
os_helper.unlink(filename)
|
||||
|
||||
self.assertEqual(old, repr(c))
|
||||
|
||||
@@ -1787,8 +1882,7 @@ class LWPCookieTests(unittest.TestCase):
|
||||
DefaultCookiePolicy(rfc2965=True))
|
||||
new_c.load(ignore_discard=ignore_discard)
|
||||
finally:
|
||||
try: os.unlink(filename)
|
||||
except OSError: pass
|
||||
os_helper.unlink(filename)
|
||||
return new_c
|
||||
|
||||
new_c = save_and_restore(c, True)
|
||||
|
||||
Vendored
+545
-70
@@ -1,4 +1,4 @@
|
||||
import sys
|
||||
import enum
|
||||
import errno
|
||||
from http import client, HTTPStatus
|
||||
import io
|
||||
@@ -8,7 +8,6 @@ import array
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
@@ -17,16 +16,19 @@ TestCase = unittest.TestCase
|
||||
from test import support
|
||||
from test.support import os_helper
|
||||
from test.support import socket_helper
|
||||
from test.support import warnings_helper
|
||||
from test.support.testcase import ExtraAssertions
|
||||
|
||||
support.requires_working_socket(module=True)
|
||||
|
||||
here = os.path.dirname(__file__)
|
||||
# Self-signed cert file for 'localhost'
|
||||
CERT_localhost = os.path.join(here, 'certdata/keycert.pem')
|
||||
CERT_localhost = os.path.join(here, 'certdata', 'keycert.pem')
|
||||
# Self-signed cert file for 'fakehostname'
|
||||
CERT_fakehostname = os.path.join(here, 'certdata/keycert2.pem')
|
||||
CERT_fakehostname = os.path.join(here, 'certdata', 'keycert2.pem')
|
||||
# Self-signed cert file for self-signed.pythontest.net
|
||||
CERT_selfsigned_pythontestdotnet = os.path.join(here, 'certdata/selfsigned_pythontestdotnet.pem')
|
||||
CERT_selfsigned_pythontestdotnet = os.path.join(
|
||||
here, 'certdata', 'selfsigned_pythontestdotnet.pem',
|
||||
)
|
||||
|
||||
# constants for testing chunked encoding
|
||||
chunked_start = (
|
||||
@@ -133,7 +135,7 @@ class FakeSocketHTTPConnection(client.HTTPConnection):
|
||||
def create_connection(self, *pos, **kw):
|
||||
return FakeSocket(*self.fake_socket_args)
|
||||
|
||||
class HeaderTests(TestCase):
|
||||
class HeaderTests(TestCase, ExtraAssertions):
|
||||
def test_auto_headers(self):
|
||||
# Some headers are added automatically, but should not be added by
|
||||
# .request() if they are explicitly set.
|
||||
@@ -272,7 +274,7 @@ class HeaderTests(TestCase):
|
||||
sock = FakeSocket('')
|
||||
conn.sock = sock
|
||||
conn.request('GET', '/foo')
|
||||
self.assertTrue(sock.data.startswith(expected))
|
||||
self.assertStartsWith(sock.data, expected)
|
||||
|
||||
expected = b'GET /foo HTTP/1.1\r\nHost: [2001:102A::]\r\n' \
|
||||
b'Accept-Encoding: identity\r\n\r\n'
|
||||
@@ -280,7 +282,23 @@ class HeaderTests(TestCase):
|
||||
sock = FakeSocket('')
|
||||
conn.sock = sock
|
||||
conn.request('GET', '/foo')
|
||||
self.assertTrue(sock.data.startswith(expected))
|
||||
self.assertStartsWith(sock.data, expected)
|
||||
|
||||
expected = b'GET /foo HTTP/1.1\r\nHost: [fe80::]\r\n' \
|
||||
b'Accept-Encoding: identity\r\n\r\n'
|
||||
conn = client.HTTPConnection('[fe80::%2]')
|
||||
sock = FakeSocket('')
|
||||
conn.sock = sock
|
||||
conn.request('GET', '/foo')
|
||||
self.assertStartsWith(sock.data, expected)
|
||||
|
||||
expected = b'GET /foo HTTP/1.1\r\nHost: [fe80::]:81\r\n' \
|
||||
b'Accept-Encoding: identity\r\n\r\n'
|
||||
conn = client.HTTPConnection('[fe80::%2]:81')
|
||||
sock = FakeSocket('')
|
||||
conn.sock = sock
|
||||
conn.request('GET', '/foo')
|
||||
self.assertStartsWith(sock.data, expected)
|
||||
|
||||
def test_malformed_headers_coped_with(self):
|
||||
# Issue 19996
|
||||
@@ -318,9 +336,9 @@ class HeaderTests(TestCase):
|
||||
self.assertIsNotNone(resp.getheader('obs-text'))
|
||||
self.assertIn('obs-text', resp.msg)
|
||||
for folded in (resp.getheader('obs-fold'), resp.msg['obs-fold']):
|
||||
self.assertTrue(folded.startswith('text'))
|
||||
self.assertStartsWith(folded, 'text')
|
||||
self.assertIn(' folded with space', folded)
|
||||
self.assertTrue(folded.endswith('folded with tab'))
|
||||
self.assertEndsWith(folded, 'folded with tab')
|
||||
|
||||
def test_invalid_headers(self):
|
||||
conn = client.HTTPConnection('example.com')
|
||||
@@ -520,11 +538,203 @@ class TransferEncodingTest(TestCase):
|
||||
return b''.join(body)
|
||||
|
||||
|
||||
class BasicTest(TestCase):
|
||||
class BasicTest(TestCase, ExtraAssertions):
|
||||
def test_dir_with_added_behavior_on_status(self):
|
||||
# see issue40084
|
||||
self.assertTrue({'description', 'name', 'phrase', 'value'} <= set(dir(HTTPStatus(404))))
|
||||
|
||||
def test_simple_httpstatus(self):
|
||||
class CheckedHTTPStatus(enum.IntEnum):
|
||||
"""HTTP status codes and reason phrases
|
||||
|
||||
Status codes from the following RFCs are all observed:
|
||||
|
||||
* RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
|
||||
* RFC 6585: Additional HTTP Status Codes
|
||||
* RFC 3229: Delta encoding in HTTP
|
||||
* RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
|
||||
* RFC 5842: Binding Extensions to WebDAV
|
||||
* RFC 7238: Permanent Redirect
|
||||
* RFC 2295: Transparent Content Negotiation in HTTP
|
||||
* RFC 2774: An HTTP Extension Framework
|
||||
* RFC 7725: An HTTP Status Code to Report Legal Obstacles
|
||||
* RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
|
||||
* RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
|
||||
* RFC 8297: An HTTP Status Code for Indicating Hints
|
||||
* RFC 8470: Using Early Data in HTTP
|
||||
"""
|
||||
def __new__(cls, value, phrase, description=''):
|
||||
obj = int.__new__(cls, value)
|
||||
obj._value_ = value
|
||||
|
||||
obj.phrase = phrase
|
||||
obj.description = description
|
||||
return obj
|
||||
|
||||
@property
|
||||
def is_informational(self):
|
||||
return 100 <= self <= 199
|
||||
|
||||
@property
|
||||
def is_success(self):
|
||||
return 200 <= self <= 299
|
||||
|
||||
@property
|
||||
def is_redirection(self):
|
||||
return 300 <= self <= 399
|
||||
|
||||
@property
|
||||
def is_client_error(self):
|
||||
return 400 <= self <= 499
|
||||
|
||||
@property
|
||||
def is_server_error(self):
|
||||
return 500 <= self <= 599
|
||||
|
||||
# informational
|
||||
CONTINUE = 100, 'Continue', 'Request received, please continue'
|
||||
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
|
||||
'Switching to new protocol; obey Upgrade header')
|
||||
PROCESSING = 102, 'Processing'
|
||||
EARLY_HINTS = 103, 'Early Hints'
|
||||
# success
|
||||
OK = 200, 'OK', 'Request fulfilled, document follows'
|
||||
CREATED = 201, 'Created', 'Document created, URL follows'
|
||||
ACCEPTED = (202, 'Accepted',
|
||||
'Request accepted, processing continues off-line')
|
||||
NON_AUTHORITATIVE_INFORMATION = (203,
|
||||
'Non-Authoritative Information', 'Request fulfilled from cache')
|
||||
NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows'
|
||||
RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input'
|
||||
PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows'
|
||||
MULTI_STATUS = 207, 'Multi-Status'
|
||||
ALREADY_REPORTED = 208, 'Already Reported'
|
||||
IM_USED = 226, 'IM Used'
|
||||
# redirection
|
||||
MULTIPLE_CHOICES = (300, 'Multiple Choices',
|
||||
'Object has several resources -- see URI list')
|
||||
MOVED_PERMANENTLY = (301, 'Moved Permanently',
|
||||
'Object moved permanently -- see URI list')
|
||||
FOUND = 302, 'Found', 'Object moved temporarily -- see URI list'
|
||||
SEE_OTHER = 303, 'See Other', 'Object moved -- see Method and URL list'
|
||||
NOT_MODIFIED = (304, 'Not Modified',
|
||||
'Document has not changed since given time')
|
||||
USE_PROXY = (305, 'Use Proxy',
|
||||
'You must use proxy specified in Location to access this resource')
|
||||
TEMPORARY_REDIRECT = (307, 'Temporary Redirect',
|
||||
'Object moved temporarily -- see URI list')
|
||||
PERMANENT_REDIRECT = (308, 'Permanent Redirect',
|
||||
'Object moved permanently -- see URI list')
|
||||
# client error
|
||||
BAD_REQUEST = (400, 'Bad Request',
|
||||
'Bad request syntax or unsupported method')
|
||||
UNAUTHORIZED = (401, 'Unauthorized',
|
||||
'No permission -- see authorization schemes')
|
||||
PAYMENT_REQUIRED = (402, 'Payment Required',
|
||||
'No payment -- see charging schemes')
|
||||
FORBIDDEN = (403, 'Forbidden',
|
||||
'Request forbidden -- authorization will not help')
|
||||
NOT_FOUND = (404, 'Not Found',
|
||||
'Nothing matches the given URI')
|
||||
METHOD_NOT_ALLOWED = (405, 'Method Not Allowed',
|
||||
'Specified method is invalid for this resource')
|
||||
NOT_ACCEPTABLE = (406, 'Not Acceptable',
|
||||
'URI not available in preferred format')
|
||||
PROXY_AUTHENTICATION_REQUIRED = (407,
|
||||
'Proxy Authentication Required',
|
||||
'You must authenticate with this proxy before proceeding')
|
||||
REQUEST_TIMEOUT = (408, 'Request Timeout',
|
||||
'Request timed out; try again later')
|
||||
CONFLICT = 409, 'Conflict', 'Request conflict'
|
||||
GONE = (410, 'Gone',
|
||||
'URI no longer exists and has been permanently removed')
|
||||
LENGTH_REQUIRED = (411, 'Length Required',
|
||||
'Client must specify Content-Length')
|
||||
PRECONDITION_FAILED = (412, 'Precondition Failed',
|
||||
'Precondition in headers is false')
|
||||
CONTENT_TOO_LARGE = (413, 'Content Too Large',
|
||||
'Content is too large')
|
||||
REQUEST_ENTITY_TOO_LARGE = CONTENT_TOO_LARGE
|
||||
URI_TOO_LONG = (414, 'URI Too Long', 'URI is too long')
|
||||
REQUEST_URI_TOO_LONG = URI_TOO_LONG
|
||||
UNSUPPORTED_MEDIA_TYPE = (415, 'Unsupported Media Type',
|
||||
'Entity body in unsupported format')
|
||||
RANGE_NOT_SATISFIABLE = (416,
|
||||
'Range Not Satisfiable',
|
||||
'Cannot satisfy request range')
|
||||
REQUESTED_RANGE_NOT_SATISFIABLE = RANGE_NOT_SATISFIABLE
|
||||
EXPECTATION_FAILED = (417, 'Expectation Failed',
|
||||
'Expect condition could not be satisfied')
|
||||
IM_A_TEAPOT = (418, 'I\'m a Teapot',
|
||||
'Server refuses to brew coffee because it is a teapot.')
|
||||
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
|
||||
'Server is not able to produce a response')
|
||||
UNPROCESSABLE_CONTENT = 422, 'Unprocessable Content'
|
||||
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
|
||||
LOCKED = 423, 'Locked'
|
||||
FAILED_DEPENDENCY = 424, 'Failed Dependency'
|
||||
TOO_EARLY = 425, 'Too Early'
|
||||
UPGRADE_REQUIRED = 426, 'Upgrade Required'
|
||||
PRECONDITION_REQUIRED = (428, 'Precondition Required',
|
||||
'The origin server requires the request to be conditional')
|
||||
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
|
||||
'The user has sent too many requests in '
|
||||
'a given amount of time ("rate limiting")')
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE = (431,
|
||||
'Request Header Fields Too Large',
|
||||
'The server is unwilling to process the request because its header '
|
||||
'fields are too large')
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS = (451,
|
||||
'Unavailable For Legal Reasons',
|
||||
'The server is denying access to the '
|
||||
'resource as a consequence of a legal demand')
|
||||
# server errors
|
||||
INTERNAL_SERVER_ERROR = (500, 'Internal Server Error',
|
||||
'Server got itself in trouble')
|
||||
NOT_IMPLEMENTED = (501, 'Not Implemented',
|
||||
'Server does not support this operation')
|
||||
BAD_GATEWAY = (502, 'Bad Gateway',
|
||||
'Invalid responses from another server/proxy')
|
||||
SERVICE_UNAVAILABLE = (503, 'Service Unavailable',
|
||||
'The server cannot process the request due to a high load')
|
||||
GATEWAY_TIMEOUT = (504, 'Gateway Timeout',
|
||||
'The gateway server did not receive a timely response')
|
||||
HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported',
|
||||
'Cannot fulfill request')
|
||||
VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates'
|
||||
INSUFFICIENT_STORAGE = 507, 'Insufficient Storage'
|
||||
LOOP_DETECTED = 508, 'Loop Detected'
|
||||
NOT_EXTENDED = 510, 'Not Extended'
|
||||
NETWORK_AUTHENTICATION_REQUIRED = (511,
|
||||
'Network Authentication Required',
|
||||
'The client needs to authenticate to gain network access')
|
||||
enum._test_simple_enum(CheckedHTTPStatus, HTTPStatus)
|
||||
|
||||
def test_httpstatus_range(self):
|
||||
"""Checks that the statuses are in the 100-599 range"""
|
||||
|
||||
for member in HTTPStatus.__members__.values():
|
||||
self.assertGreaterEqual(member, 100)
|
||||
self.assertLessEqual(member, 599)
|
||||
|
||||
def test_httpstatus_category(self):
|
||||
"""Checks that the statuses belong to the standard categories"""
|
||||
|
||||
categories = (
|
||||
((100, 199), "is_informational"),
|
||||
((200, 299), "is_success"),
|
||||
((300, 399), "is_redirection"),
|
||||
((400, 499), "is_client_error"),
|
||||
((500, 599), "is_server_error"),
|
||||
)
|
||||
for member in HTTPStatus.__members__.values():
|
||||
for (lower, upper), category in categories:
|
||||
category_indicator = getattr(member, category)
|
||||
if lower <= member <= upper:
|
||||
self.assertTrue(category_indicator)
|
||||
else:
|
||||
self.assertFalse(category_indicator)
|
||||
|
||||
def test_status_lines(self):
|
||||
# Test HTTP status lines
|
||||
|
||||
@@ -780,8 +990,7 @@ class BasicTest(TestCase):
|
||||
sock = FakeSocket(body)
|
||||
conn.sock = sock
|
||||
conn.request('GET', '/foo', body)
|
||||
self.assertTrue(sock.data.startswith(expected), '%r != %r' %
|
||||
(sock.data[:len(expected)], expected))
|
||||
self.assertStartsWith(sock.data, expected)
|
||||
|
||||
def test_send(self):
|
||||
expected = b'this is a test this is only a test'
|
||||
@@ -872,6 +1081,25 @@ class BasicTest(TestCase):
|
||||
self.assertEqual(resp.read(), expected)
|
||||
resp.close()
|
||||
|
||||
# Explicit full read
|
||||
for n in (-123, -1, None):
|
||||
with self.subTest('full read', n=n):
|
||||
sock = FakeSocket(chunked_start + last_chunk + chunked_end)
|
||||
resp = client.HTTPResponse(sock, method="GET")
|
||||
resp.begin()
|
||||
self.assertTrue(resp.chunked)
|
||||
self.assertEqual(resp.read(n), expected)
|
||||
resp.close()
|
||||
|
||||
# Read first chunk
|
||||
with self.subTest('read1(-1)'):
|
||||
sock = FakeSocket(chunked_start + last_chunk + chunked_end)
|
||||
resp = client.HTTPResponse(sock, method="GET")
|
||||
resp.begin()
|
||||
self.assertTrue(resp.chunked)
|
||||
self.assertEqual(resp.read1(-1), b"hello worl")
|
||||
resp.close()
|
||||
|
||||
# Various read sizes
|
||||
for n in range(1, 12):
|
||||
sock = FakeSocket(chunked_start + last_chunk + chunked_end)
|
||||
@@ -1227,6 +1455,72 @@ class BasicTest(TestCase):
|
||||
thread.join()
|
||||
self.assertEqual(result, b"proxied data\n")
|
||||
|
||||
def test_large_content_length(self):
|
||||
serv = socket.create_server((HOST, 0))
|
||||
self.addCleanup(serv.close)
|
||||
|
||||
def run_server():
|
||||
[conn, address] = serv.accept()
|
||||
with conn:
|
||||
while conn.recv(1024):
|
||||
conn.sendall(
|
||||
b"HTTP/1.1 200 Ok\r\n"
|
||||
b"Content-Length: %d\r\n"
|
||||
b"\r\n" % size)
|
||||
conn.sendall(b'A' * (size//3))
|
||||
conn.sendall(b'B' * (size - size//3))
|
||||
|
||||
thread = threading.Thread(target=run_server)
|
||||
thread.start()
|
||||
self.addCleanup(thread.join, 1.0)
|
||||
|
||||
conn = client.HTTPConnection(*serv.getsockname())
|
||||
try:
|
||||
for w in range(15, 27):
|
||||
size = 1 << w
|
||||
conn.request("GET", "/")
|
||||
with conn.getresponse() as response:
|
||||
self.assertEqual(len(response.read()), size)
|
||||
finally:
|
||||
conn.close()
|
||||
thread.join(1.0)
|
||||
|
||||
def test_large_content_length_truncated(self):
|
||||
serv = socket.create_server((HOST, 0))
|
||||
self.addCleanup(serv.close)
|
||||
|
||||
def run_server():
|
||||
while True:
|
||||
[conn, address] = serv.accept()
|
||||
with conn:
|
||||
conn.recv(1024)
|
||||
if not size:
|
||||
break
|
||||
conn.sendall(
|
||||
b"HTTP/1.1 200 Ok\r\n"
|
||||
b"Content-Length: %d\r\n"
|
||||
b"\r\n"
|
||||
b"Text" % size)
|
||||
|
||||
thread = threading.Thread(target=run_server)
|
||||
thread.start()
|
||||
self.addCleanup(thread.join, 1.0)
|
||||
|
||||
conn = client.HTTPConnection(*serv.getsockname())
|
||||
try:
|
||||
for w in range(18, 65):
|
||||
size = 1 << w
|
||||
conn.request("GET", "/")
|
||||
with conn.getresponse() as response:
|
||||
self.assertRaises(client.IncompleteRead, response.read)
|
||||
conn.close()
|
||||
finally:
|
||||
conn.close()
|
||||
size = 0
|
||||
conn.request("GET", "/")
|
||||
conn.close()
|
||||
thread.join(1.0)
|
||||
|
||||
def test_putrequest_override_domain_validation(self):
|
||||
"""
|
||||
It should be possible to override the default validation
|
||||
@@ -1266,7 +1560,7 @@ class BasicTest(TestCase):
|
||||
conn.putrequest('GET', '/☃')
|
||||
|
||||
|
||||
class ExtendedReadTest(TestCase):
|
||||
class ExtendedReadTest(TestCase, ExtraAssertions):
|
||||
"""
|
||||
Test peek(), read1(), readline()
|
||||
"""
|
||||
@@ -1325,7 +1619,7 @@ class ExtendedReadTest(TestCase):
|
||||
# then unbounded peek
|
||||
p2 = resp.peek()
|
||||
self.assertGreaterEqual(len(p2), len(p))
|
||||
self.assertTrue(p2.startswith(p))
|
||||
self.assertStartsWith(p2, p)
|
||||
next = resp.read(len(p2))
|
||||
self.assertEqual(next, p2)
|
||||
else:
|
||||
@@ -1340,18 +1634,22 @@ class ExtendedReadTest(TestCase):
|
||||
resp = self.resp
|
||||
self._verify_readline(self.resp.readline, self.lines_expected)
|
||||
|
||||
def _verify_readline(self, readline, expected):
|
||||
def test_readline_without_limit(self):
|
||||
self._verify_readline(self.resp.readline, self.lines_expected, limit=-1)
|
||||
|
||||
def _verify_readline(self, readline, expected, limit=5):
|
||||
all = []
|
||||
while True:
|
||||
# short readlines
|
||||
line = readline(5)
|
||||
line = readline(limit)
|
||||
if line and line != b"foo":
|
||||
if len(line) < 5:
|
||||
self.assertTrue(line.endswith(b"\n"))
|
||||
self.assertEndsWith(line, b"\n")
|
||||
all.append(line)
|
||||
if not line:
|
||||
break
|
||||
self.assertEqual(b"".join(all), expected)
|
||||
self.assertTrue(self.resp.isclosed())
|
||||
|
||||
def test_read1(self):
|
||||
resp = self.resp
|
||||
@@ -1371,6 +1669,7 @@ class ExtendedReadTest(TestCase):
|
||||
break
|
||||
all.append(data)
|
||||
self.assertEqual(b"".join(all), self.lines_expected)
|
||||
self.assertTrue(resp.isclosed())
|
||||
|
||||
def test_read1_bounded(self):
|
||||
resp = self.resp
|
||||
@@ -1382,15 +1681,22 @@ class ExtendedReadTest(TestCase):
|
||||
self.assertLessEqual(len(data), 10)
|
||||
all.append(data)
|
||||
self.assertEqual(b"".join(all), self.lines_expected)
|
||||
self.assertTrue(resp.isclosed())
|
||||
|
||||
def test_read1_0(self):
|
||||
self.assertEqual(self.resp.read1(0), b"")
|
||||
self.assertFalse(self.resp.isclosed())
|
||||
|
||||
def test_peek_0(self):
|
||||
p = self.resp.peek(0)
|
||||
self.assertLessEqual(0, len(p))
|
||||
|
||||
|
||||
class ExtendedReadTestContentLengthKnown(ExtendedReadTest):
|
||||
_header, _body = ExtendedReadTest.lines.split('\r\n\r\n', 1)
|
||||
lines = _header + f'\r\nContent-Length: {len(_body)}\r\n\r\n' + _body
|
||||
|
||||
|
||||
class ExtendedReadTestChunked(ExtendedReadTest):
|
||||
"""
|
||||
Test peek(), read1(), readline() in chunked mode
|
||||
@@ -1447,7 +1753,7 @@ class Readliner:
|
||||
raise
|
||||
|
||||
|
||||
class OfflineTest(TestCase):
|
||||
class OfflineTest(TestCase, ExtraAssertions):
|
||||
def test_all(self):
|
||||
# Documented objects defined in the module should be in __all__
|
||||
expected = {"responses"} # Allowlist documented dict() object
|
||||
@@ -1500,13 +1806,17 @@ class OfflineTest(TestCase):
|
||||
'GONE',
|
||||
'LENGTH_REQUIRED',
|
||||
'PRECONDITION_FAILED',
|
||||
'CONTENT_TOO_LARGE',
|
||||
'REQUEST_ENTITY_TOO_LARGE',
|
||||
'URI_TOO_LONG',
|
||||
'REQUEST_URI_TOO_LONG',
|
||||
'UNSUPPORTED_MEDIA_TYPE',
|
||||
'RANGE_NOT_SATISFIABLE',
|
||||
'REQUESTED_RANGE_NOT_SATISFIABLE',
|
||||
'EXPECTATION_FAILED',
|
||||
'IM_A_TEAPOT',
|
||||
'MISDIRECTED_REQUEST',
|
||||
'UNPROCESSABLE_CONTENT',
|
||||
'UNPROCESSABLE_ENTITY',
|
||||
'LOCKED',
|
||||
'FAILED_DEPENDENCY',
|
||||
@@ -1529,7 +1839,7 @@ class OfflineTest(TestCase):
|
||||
]
|
||||
for const in expected:
|
||||
with self.subTest(constant=const):
|
||||
self.assertTrue(hasattr(client, const))
|
||||
self.assertHasAttr(client, const)
|
||||
|
||||
|
||||
class SourceAddressTest(TestCase):
|
||||
@@ -1766,6 +2076,7 @@ class HTTPSTest(TestCase):
|
||||
h.close()
|
||||
self.assertIn('nginx', server_string)
|
||||
|
||||
@support.requires_resource('walltime')
|
||||
def test_networked_bad_cert(self):
|
||||
# We feed a "CA" cert that is unrelated to the server's cert
|
||||
import ssl
|
||||
@@ -1778,7 +2089,6 @@ class HTTPSTest(TestCase):
|
||||
h.request('GET', '/')
|
||||
self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
|
||||
|
||||
@unittest.skipIf(sys.platform == 'darwin', 'Occasionally success on macOS')
|
||||
def test_local_unknown_cert(self):
|
||||
# The custom cert isn't known to the default trust bundle
|
||||
import ssl
|
||||
@@ -1788,8 +2098,9 @@ class HTTPSTest(TestCase):
|
||||
h.request('GET', '/')
|
||||
self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_local_good_hostname(self):
|
||||
# The (valid) cert validates the HTTP hostname
|
||||
# The (valid) cert validates the HTTPS hostname
|
||||
import ssl
|
||||
server = self.make_server(CERT_localhost)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
@@ -1801,8 +2112,9 @@ class HTTPSTest(TestCase):
|
||||
self.addCleanup(resp.close)
|
||||
self.assertEqual(resp.status, 404)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_local_bad_hostname(self):
|
||||
# The (valid) cert doesn't validate the HTTP hostname
|
||||
# The (valid) cert doesn't validate the HTTPS hostname
|
||||
import ssl
|
||||
server = self.make_server(CERT_fakehostname)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
@@ -1810,38 +2122,21 @@ class HTTPSTest(TestCase):
|
||||
h = client.HTTPSConnection('localhost', server.port, context=context)
|
||||
with self.assertRaises(ssl.CertificateError):
|
||||
h.request('GET', '/')
|
||||
# Same with explicit check_hostname=True
|
||||
with warnings_helper.check_warnings(('', DeprecationWarning)):
|
||||
h = client.HTTPSConnection('localhost', server.port,
|
||||
context=context, check_hostname=True)
|
||||
|
||||
# Same with explicit context.check_hostname=True
|
||||
context.check_hostname = True
|
||||
h = client.HTTPSConnection('localhost', server.port, context=context)
|
||||
with self.assertRaises(ssl.CertificateError):
|
||||
h.request('GET', '/')
|
||||
# With check_hostname=False, the mismatching is ignored
|
||||
context.check_hostname = False
|
||||
with warnings_helper.check_warnings(('', DeprecationWarning)):
|
||||
h = client.HTTPSConnection('localhost', server.port,
|
||||
context=context, check_hostname=False)
|
||||
h.request('GET', '/nonexistent')
|
||||
resp = h.getresponse()
|
||||
resp.close()
|
||||
h.close()
|
||||
self.assertEqual(resp.status, 404)
|
||||
# The context's check_hostname setting is used if one isn't passed to
|
||||
# HTTPSConnection.
|
||||
|
||||
# With context.check_hostname=False, the mismatching is ignored
|
||||
context.check_hostname = False
|
||||
h = client.HTTPSConnection('localhost', server.port, context=context)
|
||||
h.request('GET', '/nonexistent')
|
||||
resp = h.getresponse()
|
||||
self.assertEqual(resp.status, 404)
|
||||
resp.close()
|
||||
h.close()
|
||||
# Passing check_hostname to HTTPSConnection should override the
|
||||
# context's setting.
|
||||
with warnings_helper.check_warnings(('', DeprecationWarning)):
|
||||
h = client.HTTPSConnection('localhost', server.port,
|
||||
context=context, check_hostname=True)
|
||||
with self.assertRaises(ssl.CertificateError):
|
||||
h.request('GET', '/')
|
||||
self.assertEqual(resp.status, 404)
|
||||
|
||||
@unittest.skipIf(not hasattr(client, 'HTTPSConnection'),
|
||||
'http.client.HTTPSConnection not available')
|
||||
@@ -1877,11 +2172,9 @@ class HTTPSTest(TestCase):
|
||||
self.assertIs(h._context, context)
|
||||
self.assertFalse(h._context.post_handshake_auth)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', 'key_file, cert_file and check_hostname are deprecated',
|
||||
DeprecationWarning)
|
||||
h = client.HTTPSConnection('localhost', 443, context=context,
|
||||
cert_file=CERT_localhost)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT, cert_file=CERT_localhost)
|
||||
context.post_handshake_auth = True
|
||||
h = client.HTTPSConnection('localhost', 443, context=context)
|
||||
self.assertTrue(h._context.post_handshake_auth)
|
||||
|
||||
|
||||
@@ -2016,14 +2309,15 @@ class HTTPResponseTest(TestCase):
|
||||
header = self.resp.getheader('No-Such-Header',default=42)
|
||||
self.assertEqual(header, 42)
|
||||
|
||||
class TunnelTests(TestCase):
|
||||
class TunnelTests(TestCase, ExtraAssertions):
|
||||
def setUp(self):
|
||||
response_text = (
|
||||
'HTTP/1.0 200 OK\r\n\r\n' # Reply to CONNECT
|
||||
'HTTP/1.1 200 OK\r\n\r\n' # Reply to CONNECT
|
||||
'HTTP/1.1 200 OK\r\n' # Reply to HEAD
|
||||
'Content-Length: 42\r\n\r\n'
|
||||
)
|
||||
self.host = 'proxy.com'
|
||||
self.port = client.HTTP_PORT
|
||||
self.conn = client.HTTPConnection(self.host)
|
||||
self.conn._create_connection = self._create_connection(response_text)
|
||||
|
||||
@@ -2035,15 +2329,45 @@ class TunnelTests(TestCase):
|
||||
return FakeSocket(response_text, host=address[0], port=address[1])
|
||||
return create_connection
|
||||
|
||||
def test_set_tunnel_host_port_headers(self):
|
||||
def test_set_tunnel_host_port_headers_add_host_missing(self):
|
||||
tunnel_host = 'destination.com'
|
||||
tunnel_port = 8888
|
||||
tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)'}
|
||||
tunnel_headers_after = tunnel_headers.copy()
|
||||
tunnel_headers_after['Host'] = '%s:%d' % (tunnel_host, tunnel_port)
|
||||
self.conn.set_tunnel(tunnel_host, port=tunnel_port,
|
||||
headers=tunnel_headers)
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, client.HTTP_PORT)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertEqual(self.conn._tunnel_host, tunnel_host)
|
||||
self.assertEqual(self.conn._tunnel_port, tunnel_port)
|
||||
self.assertEqual(self.conn._tunnel_headers, tunnel_headers_after)
|
||||
|
||||
def test_set_tunnel_host_port_headers_set_host_identical(self):
|
||||
tunnel_host = 'destination.com'
|
||||
tunnel_port = 8888
|
||||
tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)',
|
||||
'Host': '%s:%d' % (tunnel_host, tunnel_port)}
|
||||
self.conn.set_tunnel(tunnel_host, port=tunnel_port,
|
||||
headers=tunnel_headers)
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertEqual(self.conn._tunnel_host, tunnel_host)
|
||||
self.assertEqual(self.conn._tunnel_port, tunnel_port)
|
||||
self.assertEqual(self.conn._tunnel_headers, tunnel_headers)
|
||||
|
||||
def test_set_tunnel_host_port_headers_set_host_different(self):
|
||||
tunnel_host = 'destination.com'
|
||||
tunnel_port = 8888
|
||||
tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)',
|
||||
'Host': '%s:%d' % ('example.com', 4200)}
|
||||
self.conn.set_tunnel(tunnel_host, port=tunnel_port,
|
||||
headers=tunnel_headers)
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertEqual(self.conn._tunnel_host, tunnel_host)
|
||||
self.assertEqual(self.conn._tunnel_port, tunnel_port)
|
||||
self.assertEqual(self.conn._tunnel_headers, tunnel_headers)
|
||||
@@ -2055,17 +2379,96 @@ class TunnelTests(TestCase):
|
||||
'destination.com')
|
||||
|
||||
def test_connect_with_tunnel(self):
|
||||
self.conn.set_tunnel('destination.com')
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'port': client.HTTP_PORT,
|
||||
}
|
||||
self.conn.set_tunnel(d[b'host'].decode('ascii'))
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'Host: %(host)s:%(port)d\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
def test_connect_with_tunnel_with_default_port(self):
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'port': client.HTTP_PORT,
|
||||
}
|
||||
self.conn.set_tunnel(d[b'host'].decode('ascii'), port=d[b'port'])
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'Host: %(host)s:%(port)d\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
def test_connect_with_tunnel_with_nonstandard_port(self):
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'port': 8888,
|
||||
}
|
||||
self.conn.set_tunnel(d[b'host'].decode('ascii'), port=d[b'port'])
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'Host: %(host)s:%(port)d\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s:%(port)d\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
# This request is not RFC-valid, but it's been possible with the library
|
||||
# for years, so don't break it unexpectedly... This also tests
|
||||
# case-insensitivity when injecting Host: headers if they're missing.
|
||||
def test_connect_with_tunnel_with_different_host_header(self):
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'tunnel_host_header': b'example.com:9876',
|
||||
b'port': client.HTTP_PORT,
|
||||
}
|
||||
self.conn.set_tunnel(
|
||||
d[b'host'].decode('ascii'),
|
||||
headers={'HOST': d[b'tunnel_host_header'].decode('ascii')})
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'HOST: %(tunnel_host_header)s\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
def test_connect_with_tunnel_different_host(self):
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'port': client.HTTP_PORT,
|
||||
}
|
||||
self.conn.set_tunnel(d[b'host'].decode('ascii'))
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'Host: %(host)s:%(port)d\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
def test_connect_with_tunnel_idna(self):
|
||||
dest = '\u03b4\u03c0\u03b8.gr'
|
||||
dest_port = b'%s:%d' % (dest.encode('idna'), client.HTTP_PORT)
|
||||
expected = b'CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n' % (
|
||||
dest_port, dest_port)
|
||||
self.conn.set_tunnel(dest)
|
||||
self.conn.request('HEAD', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, client.HTTP_PORT)
|
||||
self.assertIn(b'CONNECT destination.com', self.conn.sock.data)
|
||||
# issue22095
|
||||
self.assertNotIn(b'Host: destination.com:None', self.conn.sock.data)
|
||||
self.assertIn(b'Host: destination.com', self.conn.sock.data)
|
||||
|
||||
# This test should be removed when CONNECT gets the HTTP/1.1 blessing
|
||||
self.assertNotIn(b'Host: proxy.com', self.conn.sock.data)
|
||||
self.assertIn(expected, self.conn.sock.data)
|
||||
|
||||
def test_tunnel_connect_single_send_connection_setup(self):
|
||||
"""Regresstion test for https://bugs.python.org/issue43332."""
|
||||
@@ -2080,17 +2483,39 @@ class TunnelTests(TestCase):
|
||||
msg=f'unexpected number of send calls: {mock_send.mock_calls}')
|
||||
proxy_setup_data_sent = mock_send.mock_calls[0][1][0]
|
||||
self.assertIn(b'CONNECT destination.com', proxy_setup_data_sent)
|
||||
self.assertTrue(
|
||||
proxy_setup_data_sent.endswith(b'\r\n\r\n'),
|
||||
self.assertEndsWith(proxy_setup_data_sent, b'\r\n\r\n',
|
||||
msg=f'unexpected proxy data sent {proxy_setup_data_sent!r}')
|
||||
|
||||
def test_connect_put_request(self):
|
||||
self.conn.set_tunnel('destination.com')
|
||||
d = {
|
||||
b'host': b'destination.com',
|
||||
b'port': client.HTTP_PORT,
|
||||
}
|
||||
self.conn.set_tunnel(d[b'host'].decode('ascii'))
|
||||
self.conn.request('PUT', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, self.port)
|
||||
self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n'
|
||||
b'Host: %(host)s:%(port)d\r\n\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
self.assertIn(b'PUT / HTTP/1.1\r\nHost: %(host)s\r\n' % d,
|
||||
self.conn.sock.data)
|
||||
|
||||
def test_connect_put_request_ipv6(self):
|
||||
self.conn.set_tunnel('[1:2:3::4]', 1234)
|
||||
self.conn.request('PUT', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, client.HTTP_PORT)
|
||||
self.assertIn(b'CONNECT destination.com', self.conn.sock.data)
|
||||
self.assertIn(b'Host: destination.com', self.conn.sock.data)
|
||||
self.assertIn(b'CONNECT [1:2:3::4]:1234', self.conn.sock.data)
|
||||
self.assertIn(b'Host: [1:2:3::4]:1234', self.conn.sock.data)
|
||||
|
||||
def test_connect_put_request_ipv6_port(self):
|
||||
self.conn.set_tunnel('[1:2:3::4]:1234')
|
||||
self.conn.request('PUT', '/', '')
|
||||
self.assertEqual(self.conn.sock.host, self.host)
|
||||
self.assertEqual(self.conn.sock.port, client.HTTP_PORT)
|
||||
self.assertIn(b'CONNECT [1:2:3::4]:1234', self.conn.sock.data)
|
||||
self.assertIn(b'Host: [1:2:3::4]:1234', self.conn.sock.data)
|
||||
|
||||
def test_tunnel_debuglog(self):
|
||||
expected_header = 'X-Dummy: 1'
|
||||
@@ -2105,6 +2530,56 @@ class TunnelTests(TestCase):
|
||||
lines = output.getvalue().splitlines()
|
||||
self.assertIn('header: {}'.format(expected_header), lines)
|
||||
|
||||
def test_proxy_response_headers(self):
|
||||
expected_header = ('X-Dummy', '1')
|
||||
response_text = (
|
||||
'HTTP/1.0 200 OK\r\n'
|
||||
'{0}\r\n\r\n'.format(':'.join(expected_header))
|
||||
)
|
||||
|
||||
self.conn._create_connection = self._create_connection(response_text)
|
||||
self.conn.set_tunnel('destination.com')
|
||||
|
||||
self.conn.request('PUT', '/', '')
|
||||
headers = self.conn.get_proxy_response_headers()
|
||||
self.assertIn(expected_header, headers.items())
|
||||
|
||||
def test_no_proxy_response_headers(self):
|
||||
expected_header = ('X-Dummy', '1')
|
||||
response_text = (
|
||||
'HTTP/1.0 200 OK\r\n'
|
||||
'{0}\r\n\r\n'.format(':'.join(expected_header))
|
||||
)
|
||||
|
||||
self.conn._create_connection = self._create_connection(response_text)
|
||||
|
||||
self.conn.request('PUT', '/', '')
|
||||
headers = self.conn.get_proxy_response_headers()
|
||||
self.assertIsNone(headers)
|
||||
|
||||
def test_tunnel_leak(self):
|
||||
sock = None
|
||||
|
||||
def _create_connection(address, timeout=None, source_address=None):
|
||||
nonlocal sock
|
||||
sock = FakeSocket(
|
||||
'HTTP/1.1 404 NOT FOUND\r\n\r\n',
|
||||
host=address[0],
|
||||
port=address[1],
|
||||
)
|
||||
return sock
|
||||
|
||||
self.conn._create_connection = _create_connection
|
||||
self.conn.set_tunnel('destination.com')
|
||||
exc = None
|
||||
try:
|
||||
self.conn.request('HEAD', '/', '')
|
||||
except OSError as e:
|
||||
# keeping a reference to exc keeps response alive in the traceback
|
||||
exc = e
|
||||
self.assertIsNotNone(exc)
|
||||
self.assertTrue(sock.file_closed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
Vendored
+358
-90
@@ -8,6 +8,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer, \
|
||||
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
|
||||
from http import server, HTTPStatus
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
@@ -26,13 +27,16 @@ import time
|
||||
import datetime
|
||||
import threading
|
||||
from unittest import mock
|
||||
from io import BytesIO
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
import unittest
|
||||
from test import support
|
||||
from test.support import os_helper
|
||||
from test.support import threading_helper
|
||||
from test.support import (
|
||||
is_apple, os_helper, requires_subprocess, threading_helper
|
||||
)
|
||||
from test.support.testcase import ExtraAssertions
|
||||
|
||||
support.requires_working_socket(module=True)
|
||||
|
||||
class NoLogRequestHandler:
|
||||
def log_message(self, *args):
|
||||
@@ -64,7 +68,7 @@ class TestServerThread(threading.Thread):
|
||||
self.join()
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
class BaseTestCase(unittest.TestCase, ExtraAssertions):
|
||||
def setUp(self):
|
||||
self._threads = threading_helper.threading_setup()
|
||||
os.environ = os_helper.EnvironmentVarGuard()
|
||||
@@ -163,6 +167,27 @@ class BaseHTTPServerTestCase(BaseTestCase):
|
||||
res = self.con.getresponse()
|
||||
self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def test_version_signs_and_underscores(self):
|
||||
self.con._http_vsn_str = 'HTTP/-9_9_9.+9_9_9'
|
||||
self.con.putrequest('GET', '/')
|
||||
self.con.endheaders()
|
||||
res = self.con.getresponse()
|
||||
self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def test_major_version_number_too_long(self):
|
||||
self.con._http_vsn_str = 'HTTP/909876543210.0'
|
||||
self.con.putrequest('GET', '/')
|
||||
self.con.endheaders()
|
||||
res = self.con.getresponse()
|
||||
self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def test_minor_version_number_too_long(self):
|
||||
self.con._http_vsn_str = 'HTTP/1.909876543210'
|
||||
self.con.putrequest('GET', '/')
|
||||
self.con.endheaders()
|
||||
res = self.con.getresponse()
|
||||
self.assertEqual(res.status, HTTPStatus.BAD_REQUEST)
|
||||
|
||||
def test_version_none_get(self):
|
||||
self.con._http_vsn_str = ''
|
||||
self.con.putrequest('GET', '/')
|
||||
@@ -292,6 +317,44 @@ class BaseHTTPServerTestCase(BaseTestCase):
|
||||
self.assertEqual(b'', data)
|
||||
|
||||
|
||||
class HTTP09ServerTestCase(BaseTestCase):
|
||||
|
||||
class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
|
||||
"""Request handler for HTTP/0.9 server."""
|
||||
|
||||
def do_GET(self):
|
||||
self.wfile.write(f'OK: here is {self.path}\r\n'.encode())
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock = self.enterContext(self.sock)
|
||||
self.sock.connect((self.HOST, self.PORT))
|
||||
|
||||
def test_simple_get(self):
|
||||
self.sock.send(b'GET /index.html\r\n')
|
||||
res = self.sock.recv(1024)
|
||||
self.assertEqual(res, b"OK: here is /index.html\r\n")
|
||||
|
||||
def test_invalid_request(self):
|
||||
self.sock.send(b'POST /index.html\r\n')
|
||||
res = self.sock.recv(1024)
|
||||
self.assertIn(b"Bad HTTP/0.9 request type ('POST')", res)
|
||||
|
||||
def test_single_request(self):
|
||||
self.sock.send(b'GET /foo.html\r\n')
|
||||
res = self.sock.recv(1024)
|
||||
self.assertEqual(res, b"OK: here is /foo.html\r\n")
|
||||
|
||||
# Ignore errors if the connection is already closed,
|
||||
# as this is the expected behavior of HTTP/0.9.
|
||||
with contextlib.suppress(OSError):
|
||||
self.sock.send(b'GET /bar.html\r\n')
|
||||
res = self.sock.recv(1024)
|
||||
# The server should not process our request.
|
||||
self.assertEqual(res, b'')
|
||||
|
||||
|
||||
class RequestHandlerLoggingTestCase(BaseTestCase):
|
||||
class request_handler(BaseHTTPRequestHandler):
|
||||
protocol_version = 'HTTP/1.1'
|
||||
@@ -312,8 +375,7 @@ class RequestHandlerLoggingTestCase(BaseTestCase):
|
||||
self.con.request('GET', '/')
|
||||
self.con.getresponse()
|
||||
|
||||
self.assertTrue(
|
||||
err.getvalue().endswith('"GET / HTTP/1.1" 200 -\n'))
|
||||
self.assertEndsWith(err.getvalue(), '"GET / HTTP/1.1" 200 -\n')
|
||||
|
||||
def test_err(self):
|
||||
self.con = http.client.HTTPConnection(self.HOST, self.PORT)
|
||||
@@ -324,8 +386,8 @@ class RequestHandlerLoggingTestCase(BaseTestCase):
|
||||
self.con.getresponse()
|
||||
|
||||
lines = err.getvalue().split('\n')
|
||||
self.assertTrue(lines[0].endswith('code 404, message File not found'))
|
||||
self.assertTrue(lines[1].endswith('"ERROR / HTTP/1.1" 404 -'))
|
||||
self.assertEndsWith(lines[0], 'code 404, message File not found')
|
||||
self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -')
|
||||
|
||||
|
||||
class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
@@ -333,7 +395,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
BaseTestCase.setUp(self)
|
||||
super().setUp()
|
||||
self.cwd = os.getcwd()
|
||||
basetempdir = tempfile.gettempdir()
|
||||
os.chdir(basetempdir)
|
||||
@@ -361,7 +423,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
BaseTestCase.tearDown(self)
|
||||
super().tearDown()
|
||||
|
||||
def check_status_and_reason(self, response, status, data=None):
|
||||
def close_conn():
|
||||
@@ -388,35 +450,175 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
reader.close()
|
||||
return body
|
||||
|
||||
@unittest.skipIf(sys.platform == 'darwin',
|
||||
'undecodable name cannot always be decoded on macOS')
|
||||
def check_list_dir_dirname(self, dirname, quotedname=None):
|
||||
fullpath = os.path.join(self.tempdir, dirname)
|
||||
try:
|
||||
os.mkdir(os.path.join(self.tempdir, dirname))
|
||||
except (OSError, UnicodeEncodeError):
|
||||
self.skipTest(f'Can not create directory {dirname!a} '
|
||||
f'on current file system')
|
||||
|
||||
if quotedname is None:
|
||||
quotedname = urllib.parse.quote(dirname, errors='surrogatepass')
|
||||
response = self.request(self.base_url + '/' + quotedname + '/')
|
||||
body = self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
displaypath = html.escape(f'{self.base_url}/{dirname}/', quote=False)
|
||||
enc = sys.getfilesystemencoding()
|
||||
prefix = f'listing for {displaypath}</'.encode(enc, 'surrogateescape')
|
||||
self.assertIn(prefix + b'title>', body)
|
||||
self.assertIn(prefix + b'h1>', body)
|
||||
|
||||
def check_list_dir_filename(self, filename):
|
||||
fullpath = os.path.join(self.tempdir, filename)
|
||||
content = ascii(fullpath).encode() + (os_helper.TESTFN_UNDECODABLE or b'\xff')
|
||||
try:
|
||||
with open(fullpath, 'wb') as f:
|
||||
f.write(content)
|
||||
except OSError:
|
||||
self.skipTest(f'Can not create file {filename!a} '
|
||||
f'on current file system')
|
||||
|
||||
response = self.request(self.base_url + '/')
|
||||
body = self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
quotedname = urllib.parse.quote(filename, errors='surrogatepass')
|
||||
enc = response.headers.get_content_charset()
|
||||
self.assertIsNotNone(enc)
|
||||
self.assertIn((f'href="{quotedname}"').encode('ascii'), body)
|
||||
displayname = html.escape(filename, quote=False)
|
||||
self.assertIn(f'>{displayname}<'.encode(enc, 'surrogateescape'), body)
|
||||
|
||||
response = self.request(self.base_url + '/' + quotedname)
|
||||
self.check_status_and_reason(response, HTTPStatus.OK, data=content)
|
||||
|
||||
@unittest.skipUnless(os_helper.TESTFN_NONASCII,
|
||||
'need os_helper.TESTFN_NONASCII')
|
||||
def test_list_dir_nonascii_dirname(self):
|
||||
dirname = os_helper.TESTFN_NONASCII + '.dir'
|
||||
self.check_list_dir_dirname(dirname)
|
||||
|
||||
@unittest.skipUnless(os_helper.TESTFN_NONASCII,
|
||||
'need os_helper.TESTFN_NONASCII')
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_list_dir_nonascii_filename(self):
|
||||
filename = os_helper.TESTFN_NONASCII + '.txt'
|
||||
self.check_list_dir_filename(filename)
|
||||
|
||||
@unittest.skipIf(is_apple,
|
||||
'undecodable name cannot always be decoded on Apple platforms')
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'undecodable name cannot be decoded on win32')
|
||||
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
|
||||
'need os_helper.TESTFN_UNDECODABLE')
|
||||
def test_undecodable_filename(self):
|
||||
enc = sys.getfilesystemencoding()
|
||||
filename = os.fsdecode(os_helper.TESTFN_UNDECODABLE) + '.txt'
|
||||
with open(os.path.join(self.tempdir, filename), 'wb') as f:
|
||||
f.write(os_helper.TESTFN_UNDECODABLE)
|
||||
response = self.request(self.base_url + '/')
|
||||
if sys.platform == 'darwin':
|
||||
# On Mac OS the HFS+ filesystem replaces bytes that aren't valid
|
||||
# UTF-8 into a percent-encoded value.
|
||||
for name in os.listdir(self.tempdir):
|
||||
if name != 'test': # Ignore a filename created in setUp().
|
||||
filename = name
|
||||
break
|
||||
body = self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
quotedname = urllib.parse.quote(filename, errors='surrogatepass')
|
||||
self.assertIn(('href="%s"' % quotedname)
|
||||
.encode(enc, 'surrogateescape'), body)
|
||||
self.assertIn(('>%s<' % html.escape(filename, quote=False))
|
||||
.encode(enc, 'surrogateescape'), body)
|
||||
response = self.request(self.base_url + '/' + quotedname)
|
||||
self.check_status_and_reason(response, HTTPStatus.OK,
|
||||
data=os_helper.TESTFN_UNDECODABLE)
|
||||
def test_list_dir_undecodable_dirname(self):
|
||||
dirname = os.fsdecode(os_helper.TESTFN_UNDECODABLE) + '.dir'
|
||||
self.check_list_dir_dirname(dirname)
|
||||
|
||||
@unittest.skipIf(is_apple,
|
||||
'undecodable name cannot always be decoded on Apple platforms')
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'undecodable name cannot be decoded on win32')
|
||||
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
|
||||
'need os_helper.TESTFN_UNDECODABLE')
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_list_dir_undecodable_filename(self):
|
||||
filename = os.fsdecode(os_helper.TESTFN_UNDECODABLE) + '.txt'
|
||||
self.check_list_dir_filename(filename)
|
||||
|
||||
def test_list_dir_undecodable_dirname2(self):
|
||||
dirname = '\ufffd.dir'
|
||||
self.check_list_dir_dirname(dirname, quotedname='%ff.dir')
|
||||
|
||||
@unittest.skipUnless(os_helper.TESTFN_UNENCODABLE,
|
||||
'need os_helper.TESTFN_UNENCODABLE')
|
||||
def test_list_dir_unencodable_dirname(self):
|
||||
dirname = os_helper.TESTFN_UNENCODABLE + '.dir'
|
||||
self.check_list_dir_dirname(dirname)
|
||||
|
||||
@unittest.skipUnless(os_helper.TESTFN_UNENCODABLE,
|
||||
'need os_helper.TESTFN_UNENCODABLE')
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_list_dir_unencodable_filename(self):
|
||||
filename = os_helper.TESTFN_UNENCODABLE + '.txt'
|
||||
self.check_list_dir_filename(filename)
|
||||
|
||||
def test_list_dir_escape_dirname(self):
|
||||
# Characters that need special treating in URL or HTML.
|
||||
for name in ('q?', 'f#', '&', '&', '<i>', '"dq"', "'sq'",
|
||||
'%A4', '%E2%82%AC'):
|
||||
with self.subTest(name=name):
|
||||
dirname = name + '.dir'
|
||||
self.check_list_dir_dirname(dirname,
|
||||
quotedname=urllib.parse.quote(dirname, safe='&<>\'"'))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_list_dir_escape_filename(self):
|
||||
# Characters that need special treating in URL or HTML.
|
||||
for name in ('q?', 'f#', '&', '&', '<i>', '"dq"', "'sq'",
|
||||
'%A4', '%E2%82%AC'):
|
||||
with self.subTest(name=name):
|
||||
filename = name + '.txt'
|
||||
self.check_list_dir_filename(filename)
|
||||
os_helper.unlink(os.path.join(self.tempdir, filename))
|
||||
|
||||
def test_list_dir_with_query_and_fragment(self):
|
||||
prefix = f'listing for {self.base_url}/</'.encode('latin1')
|
||||
response = self.request(self.base_url + '/#123').read()
|
||||
self.assertIn(prefix + b'title>', response)
|
||||
self.assertIn(prefix + b'h1>', response)
|
||||
response = self.request(self.base_url + '/?x=123').read()
|
||||
self.assertIn(prefix + b'title>', response)
|
||||
self.assertIn(prefix + b'h1>', response)
|
||||
|
||||
def test_get_dir_redirect_location_domain_injection_bug(self):
|
||||
"""Ensure //evil.co/..%2f../../X does not put //evil.co/ in Location.
|
||||
|
||||
//netloc/ in a Location header is a redirect to a new host.
|
||||
https://github.com/python/cpython/issues/87389
|
||||
|
||||
This checks that a path resolving to a directory on our server cannot
|
||||
resolve into a redirect to another server.
|
||||
"""
|
||||
os.mkdir(os.path.join(self.tempdir, 'existing_directory'))
|
||||
url = f'/python.org/..%2f..%2f..%2f..%2f..%2f../%0a%0d/../{self.tempdir_name}/existing_directory'
|
||||
expected_location = f'{url}/' # /python.org.../ single slash single prefix, trailing slash
|
||||
# Canonicalizes to /tmp/tempdir_name/existing_directory which does
|
||||
# exist and is a dir, triggering the 301 redirect logic.
|
||||
response = self.request(url)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
location = response.getheader('Location')
|
||||
self.assertEqual(location, expected_location, msg='non-attack failed!')
|
||||
|
||||
# //python.org... multi-slash prefix, no trailing slash
|
||||
attack_url = f'/{url}'
|
||||
response = self.request(attack_url)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
location = response.getheader('Location')
|
||||
self.assertNotStartsWith(location, '//')
|
||||
self.assertEqual(location, expected_location,
|
||||
msg='Expected Location header to start with a single / and '
|
||||
'end with a / as this is a directory redirect.')
|
||||
|
||||
# ///python.org... triple-slash prefix, no trailing slash
|
||||
attack3_url = f'//{url}'
|
||||
response = self.request(attack3_url)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
self.assertEqual(response.getheader('Location'), expected_location)
|
||||
|
||||
# If the second word in the http request (Request-URI for the http
|
||||
# method) is a full URI, we don't worry about it, as that'll be parsed
|
||||
# and reassembled as a full URI within BaseHTTPRequestHandler.send_head
|
||||
# so no errant scheme-less //netloc//evil.co/ domain mixup can happen.
|
||||
attack_scheme_netloc_2slash_url = f'https://pypi.org/{url}'
|
||||
expected_scheme_netloc_location = f'{attack_scheme_netloc_2slash_url}/'
|
||||
response = self.request(attack_scheme_netloc_2slash_url)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
location = response.getheader('Location')
|
||||
# We're just ensuring that the scheme and domain make it through, if
|
||||
# there are or aren't multiple slashes at the start of the path that
|
||||
# follows that isn't important in this Location: header.
|
||||
self.assertStartsWith(location, 'https://pypi.org/')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_get(self):
|
||||
#constructs the path relative to the root directory of the HTTPServer
|
||||
response = self.request(self.base_url + '/test')
|
||||
@@ -424,10 +626,19 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
# check for trailing "/" which should return 404. See Issue17324
|
||||
response = self.request(self.base_url + '/test/')
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_FOUND)
|
||||
response = self.request(self.base_url + '/test%2f')
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_FOUND)
|
||||
response = self.request(self.base_url + '/test%2F')
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_FOUND)
|
||||
response = self.request(self.base_url + '/')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
response = self.request(self.base_url + '%2f')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
response = self.request(self.base_url + '%2F')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
response = self.request(self.base_url)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
self.assertEqual(response.getheader("Location"), self.base_url + "/")
|
||||
self.assertEqual(response.getheader("Content-Length"), "0")
|
||||
response = self.request(self.base_url + '/?hi=2')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
@@ -439,6 +650,9 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_FOUND)
|
||||
response = self.request('/' + 'ThisDoesNotExist' + '/')
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_FOUND)
|
||||
os.makedirs(os.path.join(self.tempdir, 'spam', 'index.html'))
|
||||
response = self.request(self.base_url + '/spam/')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
|
||||
data = b"Dummy index file\r\n"
|
||||
with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f:
|
||||
@@ -456,6 +670,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
finally:
|
||||
os.chmod(self.tempdir, 0o755)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_head(self):
|
||||
response = self.request(
|
||||
self.base_url + '/test', method='HEAD')
|
||||
@@ -465,6 +680,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
self.assertEqual(response.getheader('content-type'),
|
||||
'application/octet-stream')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_browser_cache(self):
|
||||
"""Check that when a request to /test is sent with the request header
|
||||
If-Modified-Since set to date of last modification, the server returns
|
||||
@@ -483,6 +699,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
response = self.request(self.base_url + '/test', headers=headers)
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_MODIFIED)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_browser_cache_file_changed(self):
|
||||
# with If-Modified-Since earlier than Last-Modified, must return 200
|
||||
dt = self.last_modif_datetime
|
||||
@@ -494,6 +711,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
response = self.request(self.base_url + '/test', headers=headers)
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_browser_cache_with_If_None_Match_header(self):
|
||||
# if If-None-Match header is present, ignore If-Modified-Since
|
||||
|
||||
@@ -512,6 +730,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
response = self.request('/', method='GETs')
|
||||
self.check_status_and_reason(response, HTTPStatus.NOT_IMPLEMENTED)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_last_modified(self):
|
||||
"""Checks that the datetime returned in Last-Modified response header
|
||||
is the actual datetime of last modification, rounded to the second
|
||||
@@ -521,6 +740,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
last_modif_header = response.headers['Last-modified']
|
||||
self.assertEqual(last_modif_header, self.last_modif_header)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
def test_path_without_leading_slash(self):
|
||||
response = self.request(self.tempdir_name + '/test')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK, data=self.data)
|
||||
@@ -530,6 +750,8 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
response = self.request(self.tempdir_name)
|
||||
self.check_status_and_reason(response, HTTPStatus.MOVED_PERMANENTLY)
|
||||
self.assertEqual(response.getheader("Location"),
|
||||
self.tempdir_name + "/")
|
||||
response = self.request(self.tempdir_name + '/?hi=2')
|
||||
self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
response = self.request(self.tempdir_name + '?hi=1')
|
||||
@@ -537,27 +759,6 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||
self.assertEqual(response.getheader("Location"),
|
||||
self.tempdir_name + "/?hi=1")
|
||||
|
||||
def test_html_escape_filename(self):
|
||||
filename = '<test&>.txt'
|
||||
fullpath = os.path.join(self.tempdir, filename)
|
||||
|
||||
try:
|
||||
open(fullpath, 'wb').close()
|
||||
except OSError:
|
||||
raise unittest.SkipTest('Can not create file %s on current file '
|
||||
'system' % filename)
|
||||
|
||||
try:
|
||||
response = self.request(self.base_url + '/')
|
||||
body = self.check_status_and_reason(response, HTTPStatus.OK)
|
||||
enc = response.headers.get_content_charset()
|
||||
finally:
|
||||
os.unlink(fullpath) # avoid affecting test_undecodable_filename
|
||||
|
||||
self.assertIsNotNone(enc)
|
||||
html_text = '>%s<' % html.escape(filename, quote=False)
|
||||
self.assertIn(html_text.encode(enc), body)
|
||||
|
||||
|
||||
cgi_file1 = """\
|
||||
#!%s
|
||||
@@ -569,14 +770,19 @@ print("Hello World")
|
||||
|
||||
cgi_file2 = """\
|
||||
#!%s
|
||||
import cgi
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
print("Content-type: text/html")
|
||||
print()
|
||||
|
||||
form = cgi.FieldStorage()
|
||||
print("%%s, %%s, %%s" %% (form.getfirst("spam"), form.getfirst("eggs"),
|
||||
form.getfirst("bacon")))
|
||||
content_length = int(os.environ["CONTENT_LENGTH"])
|
||||
query_string = sys.stdin.buffer.read(content_length)
|
||||
params = {key.decode("utf-8"): val.decode("utf-8")
|
||||
for key, val in urllib.parse.parse_qsl(query_string)}
|
||||
|
||||
print("%%s, %%s, %%s" %% (params["spam"], params["eggs"], params["bacon"]))
|
||||
"""
|
||||
|
||||
cgi_file4 = """\
|
||||
@@ -607,17 +813,40 @@ for k, v in os.environ.items():
|
||||
print("</pre>")
|
||||
"""
|
||||
|
||||
@unittest.skipIf(not hasattr(os, '_exit'),
|
||||
"TODO: RUSTPYTHON, run_cgi in http/server.py gets stuck as os._exit(127) doesn't currently kill forked processes")
|
||||
cgi_file7 = """\
|
||||
#!%s
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("Content-type: text/plain")
|
||||
print()
|
||||
|
||||
content_length = int(os.environ["CONTENT_LENGTH"])
|
||||
body = sys.stdin.buffer.read(content_length)
|
||||
|
||||
print(f"{content_length} {len(body)}")
|
||||
"""
|
||||
|
||||
|
||||
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
|
||||
"This test can't be run reliably as root (issue #13308).")
|
||||
@requires_subprocess()
|
||||
class CGIHTTPServerTestCase(BaseTestCase):
|
||||
class request_handler(NoLogRequestHandler, CGIHTTPRequestHandler):
|
||||
pass
|
||||
_test_case_self = None # populated by each setUp() method call.
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
with self._test_case_self.assertWarnsRegex(
|
||||
DeprecationWarning,
|
||||
r'http\.server\.CGIHTTPRequestHandler'):
|
||||
# This context also happens to catch and silence the
|
||||
# threading DeprecationWarning from os.fork().
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
linesep = os.linesep.encode('ascii')
|
||||
|
||||
def setUp(self):
|
||||
self.request_handler._test_case_self = self # practical, but yuck.
|
||||
BaseTestCase.setUp(self)
|
||||
self.cwd = os.getcwd()
|
||||
self.parent_dir = tempfile.mkdtemp()
|
||||
@@ -637,12 +866,13 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
self.file3_path = None
|
||||
self.file4_path = None
|
||||
self.file5_path = None
|
||||
self.file6_path = None
|
||||
self.file7_path = None
|
||||
|
||||
# The shebang line should be pure ASCII: use symlink if possible.
|
||||
# See issue #7668.
|
||||
self._pythonexe_symlink = None
|
||||
# TODO: RUSTPYTHON; dl_nt not supported yet
|
||||
if os_helper.can_symlink() and sys.platform != 'win32':
|
||||
if os_helper.can_symlink():
|
||||
self.pythonexe = os.path.join(self.parent_dir, 'python')
|
||||
self._pythonexe_symlink = support.PythonSymlink(self.pythonexe).__enter__()
|
||||
else:
|
||||
@@ -692,9 +922,15 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
file6.write(cgi_file6 % self.pythonexe)
|
||||
os.chmod(self.file6_path, 0o777)
|
||||
|
||||
self.file7_path = os.path.join(self.cgi_dir, 'file7.py')
|
||||
with open(self.file7_path, 'w', encoding='utf-8') as file7:
|
||||
file7.write(cgi_file7 % self.pythonexe)
|
||||
os.chmod(self.file7_path, 0o777)
|
||||
|
||||
os.chdir(self.parent_dir)
|
||||
|
||||
def tearDown(self):
|
||||
self.request_handler._test_case_self = None
|
||||
try:
|
||||
os.chdir(self.cwd)
|
||||
if self._pythonexe_symlink:
|
||||
@@ -713,11 +949,16 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
os.remove(self.file5_path)
|
||||
if self.file6_path:
|
||||
os.remove(self.file6_path)
|
||||
if self.file7_path:
|
||||
os.remove(self.file7_path)
|
||||
os.rmdir(self.cgi_child_dir)
|
||||
os.rmdir(self.cgi_dir)
|
||||
os.rmdir(self.cgi_dir_in_sub_dir)
|
||||
os.rmdir(self.sub_dir_2)
|
||||
os.rmdir(self.sub_dir_1)
|
||||
# The 'gmon.out' file can be written in the current working
|
||||
# directory if C-level code profiling with gprof is enabled.
|
||||
os_helper.unlink(os.path.join(self.parent_dir, 'gmon.out'))
|
||||
os.rmdir(self.parent_dir)
|
||||
finally:
|
||||
BaseTestCase.tearDown(self)
|
||||
@@ -764,8 +1005,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
msg='path = %r\nGot: %r\nWanted: %r' %
|
||||
(path, actual, expected))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: Tuples differ: (b"", None, 200) != (b"Hello World\n", "text/html", <HTTPStatus.OK: 200>)')
|
||||
def test_headers_and_content(self):
|
||||
res = self.request('/cgi-bin/file1.py')
|
||||
self.assertEqual(
|
||||
@@ -776,9 +1016,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
res = self.request('///////////nocgi.py/../cgi-bin/nothere.sh')
|
||||
self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailure
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; b"" != b"1, python, 123456\n"')
|
||||
def test_post(self):
|
||||
params = urllib.parse.urlencode(
|
||||
{'spam' : 1, 'eggs' : 'python', 'bacon' : 123456})
|
||||
@@ -787,13 +1025,30 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
|
||||
self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
|
||||
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: b"" != b"32768 32768\n"')
|
||||
def test_large_content_length(self):
|
||||
for w in range(15, 25):
|
||||
size = 1 << w
|
||||
body = b'X' * size
|
||||
headers = {'Content-Length' : str(size)}
|
||||
res = self.request('/cgi-bin/file7.py', 'POST', body, headers)
|
||||
self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep)
|
||||
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: b"" != b"Hello World\n"')
|
||||
def test_large_content_length_truncated(self):
|
||||
with support.swap_attr(self.request_handler, 'timeout', 0.001):
|
||||
for w in range(18, 65):
|
||||
size = 1 << w
|
||||
headers = {'Content-Length' : str(size)}
|
||||
res = self.request('/cgi-bin/file1.py', 'POST', b'x', headers)
|
||||
self.assertEqual(res.read(), b'Hello World' + self.linesep)
|
||||
|
||||
def test_invaliduri(self):
|
||||
res = self.request('/cgi-bin/invalid')
|
||||
res.read()
|
||||
self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: Tuples differ: (b"Hello World\n", "text/html", <HTTPStatus.OK: 200>) != (b"", None, 200)')
|
||||
def test_authorization(self):
|
||||
headers = {b'Authorization' : b'Basic ' +
|
||||
base64.b64encode(b'username:pass')}
|
||||
@@ -802,8 +1057,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: Tuples differ: (b"Hello World\n", "text/html", <HTTPStatus.OK: 200>) != (b"", None, 200)')
|
||||
def test_no_leading_slash(self):
|
||||
# http://bugs.python.org/issue2254
|
||||
res = self.request('cgi-bin/file1.py')
|
||||
@@ -811,8 +1065,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; ValueError: signal only works in main thread')
|
||||
def test_os_environ_is_not_altered(self):
|
||||
signature = "Test CGI Server"
|
||||
os.environ['SERVER_SOFTWARE'] = signature
|
||||
@@ -822,32 +1075,28 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
self.assertEqual(os.environ['SERVER_SOFTWARE'], signature)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; ValueError: signal only works in main thread')
|
||||
def test_urlquote_decoding_in_cgi_check(self):
|
||||
res = self.request('/cgi-bin%2ffile1.py')
|
||||
self.assertEqual(
|
||||
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: Tuples differ: (b"Hello World\n", "text/html", <HTTPStatus.OK: 200>) != (b"", None, 200)')
|
||||
def test_nested_cgi_path_issue21323(self):
|
||||
res = self.request('/cgi-bin/child-dir/file3.py')
|
||||
self.assertEqual(
|
||||
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; ValueError: signal only works in main thread')
|
||||
def test_query_with_multiple_question_mark(self):
|
||||
res = self.request('/cgi-bin/file4.py?a=b?c=d')
|
||||
self.assertEqual(
|
||||
(b'a=b?c=d' + self.linesep, 'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: Tuples differ: (b"k=aa%2F%2Fbb&//q//p//=//a//b//\n", "text/html", <HTTPStatus.OK: 200>) != (b"", None, 200)')
|
||||
def test_query_with_continuous_slashes(self):
|
||||
res = self.request('/cgi-bin/file4.py?k=aa%2F%2Fbb&//q//p//=//a//b//')
|
||||
self.assertEqual(
|
||||
@@ -855,8 +1104,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
'text/html', HTTPStatus.OK),
|
||||
(res.read(), res.getheader('Content-type'), res.status))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; Tuples differ: (b"", None, 200) != (b"Hello World\n", "text/html", <HTTPStatus.OK: 200>)')
|
||||
def test_cgi_path_in_sub_directories(self):
|
||||
try:
|
||||
CGIHTTPRequestHandler.cgi_directories.append('/sub/dir/cgi-bin')
|
||||
@@ -867,8 +1115,7 @@ class CGIHTTPServerTestCase(BaseTestCase):
|
||||
finally:
|
||||
CGIHTTPRequestHandler.cgi_directories.remove('/sub/dir/cgi-bin')
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.skipIf(sys.platform != 'win32', "TODO: RUSTPYTHON; works only on windows")
|
||||
@unittest.expectedFailureIf(sys.platform != 'win32', 'TODO: RUSTPYTHON; AssertionError: b"HTTP_ACCEPT=text/html,text/plain" not found in b""')
|
||||
def test_accept(self):
|
||||
browser_accept = \
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
@@ -929,7 +1176,7 @@ class AuditableBytesIO:
|
||||
return len(self.datas)
|
||||
|
||||
|
||||
class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
|
||||
class BaseHTTPRequestHandlerTestCase(unittest.TestCase, ExtraAssertions):
|
||||
"""Test the functionality of the BaseHTTPServer.
|
||||
|
||||
Test the support for the Expect 100-continue header.
|
||||
@@ -960,6 +1207,27 @@ class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
|
||||
match = self.HTTPResponseMatch.search(response)
|
||||
self.assertIsNotNone(match)
|
||||
|
||||
def test_unprintable_not_logged(self):
|
||||
# We call the method from the class directly as our Socketless
|
||||
# Handler subclass overrode it... nice for everything BUT this test.
|
||||
self.handler.client_address = ('127.0.0.1', 1337)
|
||||
log_message = BaseHTTPRequestHandler.log_message
|
||||
with mock.patch.object(sys, 'stderr', StringIO()) as fake_stderr:
|
||||
log_message(self.handler, '/foo')
|
||||
log_message(self.handler, '/\033bar\000\033')
|
||||
log_message(self.handler, '/spam %s.', 'a')
|
||||
log_message(self.handler, '/spam %s.', '\033\x7f\x9f\xa0beans')
|
||||
log_message(self.handler, '"GET /foo\\b"ar\007 HTTP/1.0"')
|
||||
stderr = fake_stderr.getvalue()
|
||||
self.assertNotIn('\033', stderr) # non-printable chars are caught.
|
||||
self.assertNotIn('\000', stderr) # non-printable chars are caught.
|
||||
lines = stderr.splitlines()
|
||||
self.assertIn('/foo', lines[0])
|
||||
self.assertIn(r'/\x1bbar\x00\x1b', lines[1])
|
||||
self.assertIn('/spam a.', lines[2])
|
||||
self.assertIn('/spam \\x1b\\x7f\\x9f\xa0beans.', lines[3])
|
||||
self.assertIn(r'"GET /foo\\b"ar\x07 HTTP/1.0"', lines[4])
|
||||
|
||||
def test_http_1_1(self):
|
||||
result = self.send_typical_request(b'GET / HTTP/1.1\r\n\r\n')
|
||||
self.verify_http_server_response(result[0])
|
||||
@@ -996,7 +1264,7 @@ class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
|
||||
b'Host: dummy\r\n'
|
||||
b'\r\n'
|
||||
)
|
||||
self.assertTrue(result[0].startswith(b'HTTP/1.1 400 '))
|
||||
self.assertStartsWith(result[0], b'HTTP/1.1 400 ')
|
||||
self.verify_expected_headers(result[1:result.index(b'\r\n')])
|
||||
self.assertFalse(self.handler.get_called)
|
||||
|
||||
@@ -1110,7 +1378,7 @@ class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
|
||||
# Issue #10714: huge request lines are discarded, to avoid Denial
|
||||
# of Service attacks.
|
||||
result = self.send_typical_request(b'GET ' + b'x' * 65537)
|
||||
self.assertEqual(result[0], b'HTTP/1.1 414 Request-URI Too Long\r\n')
|
||||
self.assertEqual(result[0], b'HTTP/1.1 414 URI Too Long\r\n')
|
||||
self.assertFalse(self.handler.get_called)
|
||||
self.assertIsInstance(self.handler.requestline, str)
|
||||
|
||||
|
||||
Vendored
+1
@@ -736,6 +736,7 @@ class HandlerTest(BaseTest):
|
||||
@threading_helper.requires_working_threading()
|
||||
@skip_if_asan_fork
|
||||
@skip_if_tsan_fork
|
||||
@unittest.skip("TODO: RUSTPYTHON; Flaky")
|
||||
def test_post_fork_child_no_deadlock(self):
|
||||
"""Ensure child logging locks are not held; bpo-6721 & bpo-36533."""
|
||||
class _OurHandler(logging.Handler):
|
||||
|
||||
Vendored
+4
@@ -259,6 +259,10 @@ Disallow: /another/path?
|
||||
good = ['/some/path?']
|
||||
bad = ['/another/path?']
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; self.assertFalse(self.parser.can_fetch(agent, url))\nAssertionError: True is not false
|
||||
def test_bad_urls(self):
|
||||
super().test_bad_urls()
|
||||
|
||||
|
||||
class DefaultEntryTest(BaseRequestRateTest, unittest.TestCase):
|
||||
robots_txt = """\
|
||||
|
||||
Vendored
+2
-1
@@ -3525,6 +3525,7 @@ class ThreadedTests(unittest.TestCase):
|
||||
else:
|
||||
s.close()
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_socketserver(self):
|
||||
"""Using socketserver to create and manage SSL connections."""
|
||||
server = make_https_server(self, certfile=SIGNED_CERTFILE)
|
||||
@@ -4596,7 +4597,7 @@ class ThreadedTests(unittest.TestCase):
|
||||
with client_context.wrap_socket(socket.socket()) as s:
|
||||
s.connect((HOST, server.port))
|
||||
|
||||
@unittest.skip("TODO: rustpython")
|
||||
@unittest.skip("TODO: RUSTPYTHON; Hangs")
|
||||
def test_thread_recv_while_main_thread_sends(self):
|
||||
# GH-137583: Locking was added to calls to send() and recv() on SSL
|
||||
# socket objects. This seemed fine at the surface level because those
|
||||
|
||||
Vendored
-2
@@ -1556,7 +1556,6 @@ class Pathname_Tests(unittest.TestCase):
|
||||
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'test specific to POSIX pathnames')
|
||||
@unittest.expectedFailure # AssertionError: '//a/b.c' != '////a/b.c'
|
||||
def test_pathname2url_posix(self):
|
||||
fn = urllib.request.pathname2url
|
||||
self.assertEqual(fn('/'), '/')
|
||||
@@ -1617,7 +1616,6 @@ class Pathname_Tests(unittest.TestCase):
|
||||
|
||||
@unittest.skipIf(sys.platform == 'win32',
|
||||
'test specific to POSIX pathnames')
|
||||
@unittest.expectedFailure # AssertionError: '///foo/bar' != '/foo/bar'
|
||||
def test_url2pathname_posix(self):
|
||||
fn = urllib.request.url2pathname
|
||||
self.assertEqual(fn('/foo/bar'), '/foo/bar')
|
||||
|
||||
Vendored
+136
-49
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
from test import support
|
||||
from test.support import os_helper
|
||||
from test.support import socket_helper
|
||||
from test.support import requires_subprocess
|
||||
from test.support import warnings_helper
|
||||
from test.support.testcase import ExtraAssertions
|
||||
from test import test_urllib
|
||||
from unittest import mock
|
||||
|
||||
import os
|
||||
import io
|
||||
@@ -14,16 +16,19 @@ import tempfile
|
||||
import subprocess
|
||||
|
||||
import urllib.request
|
||||
# The proxy bypass method imported below has logic specific to the OSX
|
||||
# proxy config data structure but is testable on all platforms.
|
||||
# The proxy bypass method imported below has logic specific to the
|
||||
# corresponding system but is testable on all platforms.
|
||||
from urllib.request import (Request, OpenerDirector, HTTPBasicAuthHandler,
|
||||
HTTPPasswordMgrWithPriorAuth, _parse_proxy,
|
||||
_proxy_bypass_winreg_override,
|
||||
_proxy_bypass_macosx_sysconf,
|
||||
AbstractDigestAuthHandler)
|
||||
from urllib.parse import urlparse
|
||||
import urllib.error
|
||||
import http.client
|
||||
|
||||
support.requires_working_socket(module=True)
|
||||
|
||||
# XXX
|
||||
# Request
|
||||
# CacheFTPHandler (hard to write)
|
||||
@@ -483,7 +488,18 @@ def build_test_opener(*handler_instances):
|
||||
return opener
|
||||
|
||||
|
||||
class MockHTTPHandler(urllib.request.BaseHandler):
|
||||
class MockHTTPHandler(urllib.request.HTTPHandler):
|
||||
# Very simple mock HTTP handler with no special behavior other than using a mock HTTP connection
|
||||
|
||||
def __init__(self, debuglevel=None):
|
||||
super(MockHTTPHandler, self).__init__(debuglevel=debuglevel)
|
||||
self.httpconn = MockHTTPClass()
|
||||
|
||||
def http_open(self, req):
|
||||
return self.do_open(self.httpconn, req)
|
||||
|
||||
|
||||
class MockHTTPHandlerRedirect(urllib.request.BaseHandler):
|
||||
# useful for testing redirections and auth
|
||||
# sends supplied headers and code as first response
|
||||
# sends 200 OK as second response
|
||||
@@ -511,16 +527,17 @@ class MockHTTPHandler(urllib.request.BaseHandler):
|
||||
return MockResponse(200, "OK", msg, "", req.get_full_url())
|
||||
|
||||
|
||||
class MockHTTPSHandler(urllib.request.AbstractHTTPHandler):
|
||||
# Useful for testing the Proxy-Authorization request by verifying the
|
||||
# properties of httpcon
|
||||
if hasattr(http.client, 'HTTPSConnection'):
|
||||
class MockHTTPSHandler(urllib.request.HTTPSHandler):
|
||||
# Useful for testing the Proxy-Authorization request by verifying the
|
||||
# properties of httpcon
|
||||
|
||||
def __init__(self, debuglevel=0):
|
||||
urllib.request.AbstractHTTPHandler.__init__(self, debuglevel=debuglevel)
|
||||
self.httpconn = MockHTTPClass()
|
||||
def __init__(self, debuglevel=None, context=None, check_hostname=None):
|
||||
super(MockHTTPSHandler, self).__init__(debuglevel, context, check_hostname)
|
||||
self.httpconn = MockHTTPClass()
|
||||
|
||||
def https_open(self, req):
|
||||
return self.do_open(self.httpconn, req)
|
||||
def https_open(self, req):
|
||||
return self.do_open(self.httpconn, req)
|
||||
|
||||
|
||||
class MockHTTPHandlerCheckAuth(urllib.request.BaseHandler):
|
||||
@@ -701,10 +718,6 @@ class OpenerDirectorTests(unittest.TestCase):
|
||||
|
||||
|
||||
def sanepathname2url(path):
|
||||
try:
|
||||
path.encode("utf-8")
|
||||
except UnicodeEncodeError:
|
||||
raise unittest.SkipTest("path is not encodable to utf8")
|
||||
urlpath = urllib.request.pathname2url(path)
|
||||
if os.name == "nt" and urlpath.startswith("///"):
|
||||
urlpath = urlpath[2:]
|
||||
@@ -712,8 +725,9 @@ def sanepathname2url(path):
|
||||
return urlpath
|
||||
|
||||
|
||||
class HandlerTests(unittest.TestCase):
|
||||
class HandlerTests(unittest.TestCase, ExtraAssertions):
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'image/gif'
|
||||
def test_ftp(self):
|
||||
class MockFTPWrapper:
|
||||
def __init__(self, data):
|
||||
@@ -761,7 +775,7 @@ class HandlerTests(unittest.TestCase):
|
||||
["foo", "bar"], "", None),
|
||||
("ftp://localhost/baz.gif;type=a",
|
||||
"localhost", ftplib.FTP_PORT, "", "", "A",
|
||||
[], "baz.gif", None), # XXX really this should guess image/gif
|
||||
[], "baz.gif", "image/gif"),
|
||||
]:
|
||||
req = Request(url)
|
||||
req.timeout = None
|
||||
@@ -777,6 +791,7 @@ class HandlerTests(unittest.TestCase):
|
||||
headers = r.info()
|
||||
self.assertEqual(headers.get("Content-type"), mimetype)
|
||||
self.assertEqual(int(headers["Content-length"]), len(data))
|
||||
r.close()
|
||||
|
||||
def test_file(self):
|
||||
import email.utils
|
||||
@@ -984,6 +999,7 @@ class HandlerTests(unittest.TestCase):
|
||||
|
||||
file_obj.close()
|
||||
|
||||
@requires_subprocess()
|
||||
def test_http_body_pipe(self):
|
||||
# A file reading from a pipe.
|
||||
# A pipe cannot be seek'ed. There is no way to determine the
|
||||
@@ -1047,12 +1063,37 @@ class HandlerTests(unittest.TestCase):
|
||||
newreq = h.do_request_(req)
|
||||
self.assertEqual(int(newreq.get_header('Content-length')),16)
|
||||
|
||||
def test_http_handler_debuglevel(self):
|
||||
def test_http_handler_global_debuglevel(self):
|
||||
with mock.patch.object(http.client.HTTPConnection, 'debuglevel', 6):
|
||||
o = OpenerDirector()
|
||||
h = MockHTTPHandler()
|
||||
o.add_handler(h)
|
||||
o.open("http://www.example.com")
|
||||
self.assertEqual(h._debuglevel, 6)
|
||||
|
||||
def test_http_handler_local_debuglevel(self):
|
||||
o = OpenerDirector()
|
||||
h = MockHTTPSHandler(debuglevel=1)
|
||||
h = MockHTTPHandler(debuglevel=5)
|
||||
o.add_handler(h)
|
||||
o.open("http://www.example.com")
|
||||
self.assertEqual(h._debuglevel, 5)
|
||||
|
||||
@unittest.skipUnless(hasattr(http.client, 'HTTPSConnection'), 'HTTPSConnection required for HTTPS tests.')
|
||||
def test_https_handler_global_debuglevel(self):
|
||||
with mock.patch.object(http.client.HTTPSConnection, 'debuglevel', 7):
|
||||
o = OpenerDirector()
|
||||
h = MockHTTPSHandler()
|
||||
o.add_handler(h)
|
||||
o.open("https://www.example.com")
|
||||
self.assertEqual(h._debuglevel, 7)
|
||||
|
||||
@unittest.skipUnless(hasattr(http.client, 'HTTPSConnection'), 'HTTPSConnection required for HTTPS tests.')
|
||||
def test_https_handler_local_debuglevel(self):
|
||||
o = OpenerDirector()
|
||||
h = MockHTTPSHandler(debuglevel=4)
|
||||
o.add_handler(h)
|
||||
o.open("https://www.example.com")
|
||||
self.assertEqual(h._debuglevel, 1)
|
||||
self.assertEqual(h._debuglevel, 4)
|
||||
|
||||
def test_http_doubleslash(self):
|
||||
# Checks the presence of any unnecessary double slash in url does not
|
||||
@@ -1140,15 +1181,15 @@ class HandlerTests(unittest.TestCase):
|
||||
r = MockResponse(200, "OK", {}, "", url)
|
||||
newr = h.http_response(req, r)
|
||||
self.assertIs(r, newr)
|
||||
self.assertFalse(hasattr(o, "proto")) # o.error not called
|
||||
self.assertNotHasAttr(o, "proto") # o.error not called
|
||||
r = MockResponse(202, "Accepted", {}, "", url)
|
||||
newr = h.http_response(req, r)
|
||||
self.assertIs(r, newr)
|
||||
self.assertFalse(hasattr(o, "proto")) # o.error not called
|
||||
self.assertNotHasAttr(o, "proto") # o.error not called
|
||||
r = MockResponse(206, "Partial content", {}, "", url)
|
||||
newr = h.http_response(req, r)
|
||||
self.assertIs(r, newr)
|
||||
self.assertFalse(hasattr(o, "proto")) # o.error not called
|
||||
self.assertNotHasAttr(o, "proto") # o.error not called
|
||||
# anything else calls o.error (and MockOpener returns None, here)
|
||||
r = MockResponse(502, "Bad gateway", {}, "", url)
|
||||
self.assertIsNone(h.http_response(req, r))
|
||||
@@ -1179,7 +1220,7 @@ class HandlerTests(unittest.TestCase):
|
||||
o = h.parent = MockOpener()
|
||||
|
||||
# ordinary redirect behaviour
|
||||
for code in 301, 302, 303, 307:
|
||||
for code in 301, 302, 303, 307, 308:
|
||||
for data in None, "blah\nblah\n":
|
||||
method = getattr(h, "http_error_%s" % code)
|
||||
req = Request(from_url, data)
|
||||
@@ -1191,10 +1232,11 @@ class HandlerTests(unittest.TestCase):
|
||||
try:
|
||||
method(req, MockFile(), code, "Blah",
|
||||
MockHeaders({"location": to_url}))
|
||||
except urllib.error.HTTPError:
|
||||
# 307 in response to POST requires user OK
|
||||
self.assertEqual(code, 307)
|
||||
except urllib.error.HTTPError as err:
|
||||
# 307 and 308 in response to POST require user OK
|
||||
self.assertIn(code, (307, 308))
|
||||
self.assertIsNotNone(data)
|
||||
err.close()
|
||||
self.assertEqual(o.req.get_full_url(), to_url)
|
||||
try:
|
||||
self.assertEqual(o.req.get_method(), "GET")
|
||||
@@ -1230,9 +1272,10 @@ class HandlerTests(unittest.TestCase):
|
||||
while 1:
|
||||
redirect(h, req, "http://example.com/")
|
||||
count = count + 1
|
||||
except urllib.error.HTTPError:
|
||||
except urllib.error.HTTPError as err:
|
||||
# don't stop until max_repeats, because cookies may introduce state
|
||||
self.assertEqual(count, urllib.request.HTTPRedirectHandler.max_repeats)
|
||||
err.close()
|
||||
|
||||
# detect endless non-repeating chain of redirects
|
||||
req = Request(from_url, origin_req_host="example.com")
|
||||
@@ -1242,9 +1285,10 @@ class HandlerTests(unittest.TestCase):
|
||||
while 1:
|
||||
redirect(h, req, "http://example.com/%d" % count)
|
||||
count = count + 1
|
||||
except urllib.error.HTTPError:
|
||||
except urllib.error.HTTPError as err:
|
||||
self.assertEqual(count,
|
||||
urllib.request.HTTPRedirectHandler.max_redirections)
|
||||
err.close()
|
||||
|
||||
def test_invalid_redirect(self):
|
||||
from_url = "http://example.com/a.html"
|
||||
@@ -1258,9 +1302,11 @@ class HandlerTests(unittest.TestCase):
|
||||
|
||||
for scheme in invalid_schemes:
|
||||
invalid_url = scheme + '://' + schemeless_url
|
||||
self.assertRaises(urllib.error.HTTPError, h.http_error_302,
|
||||
with self.assertRaises(urllib.error.HTTPError) as cm:
|
||||
h.http_error_302(
|
||||
req, MockFile(), 302, "Security Loophole",
|
||||
MockHeaders({"location": invalid_url}))
|
||||
cm.exception.close()
|
||||
|
||||
for scheme in valid_schemes:
|
||||
valid_url = scheme + '://' + schemeless_url
|
||||
@@ -1288,7 +1334,7 @@ class HandlerTests(unittest.TestCase):
|
||||
|
||||
cj = CookieJar()
|
||||
interact_netscape(cj, "http://www.example.com/", "spam=eggs")
|
||||
hh = MockHTTPHandler(302, "Location: http://www.cracker.com/\r\n\r\n")
|
||||
hh = MockHTTPHandlerRedirect(302, "Location: http://www.cracker.com/\r\n\r\n")
|
||||
hdeh = urllib.request.HTTPDefaultErrorHandler()
|
||||
hrh = urllib.request.HTTPRedirectHandler()
|
||||
cp = urllib.request.HTTPCookieProcessor(cj)
|
||||
@@ -1298,7 +1344,7 @@ class HandlerTests(unittest.TestCase):
|
||||
|
||||
def test_redirect_fragment(self):
|
||||
redirected_url = 'http://www.example.com/index.html#OK\r\n\r\n'
|
||||
hh = MockHTTPHandler(302, 'Location: ' + redirected_url)
|
||||
hh = MockHTTPHandlerRedirect(302, 'Location: ' + redirected_url)
|
||||
hdeh = urllib.request.HTTPDefaultErrorHandler()
|
||||
hrh = urllib.request.HTTPRedirectHandler()
|
||||
o = build_test_opener(hh, hdeh, hrh)
|
||||
@@ -1358,7 +1404,16 @@ class HandlerTests(unittest.TestCase):
|
||||
response = opener.open('http://example.com/')
|
||||
expected = b'GET ' + result + b' '
|
||||
request = handler.last_buf
|
||||
self.assertTrue(request.startswith(expected), repr(request))
|
||||
self.assertStartsWith(request, expected)
|
||||
|
||||
def test_redirect_head_request(self):
|
||||
from_url = "http://example.com/a.html"
|
||||
to_url = "http://example.com/b.html"
|
||||
h = urllib.request.HTTPRedirectHandler()
|
||||
req = Request(from_url, method="HEAD")
|
||||
fp = MockFile()
|
||||
new_req = h.redirect_request(req, fp, 302, "Found", {}, to_url)
|
||||
self.assertEqual(new_req.get_method(), "HEAD")
|
||||
|
||||
def test_proxy(self):
|
||||
u = "proxy.example.com:3128"
|
||||
@@ -1379,7 +1434,8 @@ class HandlerTests(unittest.TestCase):
|
||||
[tup[0:2] for tup in o.calls])
|
||||
|
||||
def test_proxy_no_proxy(self):
|
||||
os.environ['no_proxy'] = 'python.org'
|
||||
env = self.enterContext(os_helper.EnvironmentVarGuard())
|
||||
env['no_proxy'] = 'python.org'
|
||||
o = OpenerDirector()
|
||||
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com"))
|
||||
o.add_handler(ph)
|
||||
@@ -1391,10 +1447,10 @@ class HandlerTests(unittest.TestCase):
|
||||
self.assertEqual(req.host, "www.python.org")
|
||||
o.open(req)
|
||||
self.assertEqual(req.host, "www.python.org")
|
||||
del os.environ['no_proxy']
|
||||
|
||||
def test_proxy_no_proxy_all(self):
|
||||
os.environ['no_proxy'] = '*'
|
||||
env = self.enterContext(os_helper.EnvironmentVarGuard())
|
||||
env['no_proxy'] = '*'
|
||||
o = OpenerDirector()
|
||||
ph = urllib.request.ProxyHandler(dict(http="proxy.example.com"))
|
||||
o.add_handler(ph)
|
||||
@@ -1402,7 +1458,6 @@ class HandlerTests(unittest.TestCase):
|
||||
self.assertEqual(req.host, "www.python.org")
|
||||
o.open(req)
|
||||
self.assertEqual(req.host, "www.python.org")
|
||||
del os.environ['no_proxy']
|
||||
|
||||
def test_proxy_https(self):
|
||||
o = OpenerDirector()
|
||||
@@ -1420,6 +1475,7 @@ class HandlerTests(unittest.TestCase):
|
||||
self.assertEqual([(handlers[0], "https_open")],
|
||||
[tup[0:2] for tup in o.calls])
|
||||
|
||||
@unittest.skipUnless(hasattr(http.client, 'HTTPSConnection'), 'HTTPSConnection required for HTTPS tests.')
|
||||
def test_proxy_https_proxy_authorization(self):
|
||||
o = OpenerDirector()
|
||||
ph = urllib.request.ProxyHandler(dict(https='proxy.example.com:3128'))
|
||||
@@ -1443,6 +1499,30 @@ class HandlerTests(unittest.TestCase):
|
||||
self.assertEqual(req.host, "proxy.example.com:3128")
|
||||
self.assertEqual(req.get_header("Proxy-authorization"), "FooBar")
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "only relevant for Windows")
|
||||
def test_winreg_proxy_bypass(self):
|
||||
proxy_override = "www.example.com;*.example.net; 192.168.0.1"
|
||||
proxy_bypass = _proxy_bypass_winreg_override
|
||||
for host in ("www.example.com", "www.example.net", "192.168.0.1"):
|
||||
self.assertTrue(proxy_bypass(host, proxy_override),
|
||||
"expected bypass of %s to be true" % host)
|
||||
|
||||
for host in ("example.com", "www.example.org", "example.net",
|
||||
"192.168.0.2"):
|
||||
self.assertFalse(proxy_bypass(host, proxy_override),
|
||||
"expected bypass of %s to be False" % host)
|
||||
|
||||
# check intranet address bypass
|
||||
proxy_override = "example.com; <local>"
|
||||
self.assertTrue(proxy_bypass("example.com", proxy_override),
|
||||
"expected bypass of %s to be true" % host)
|
||||
self.assertFalse(proxy_bypass("example.net", proxy_override),
|
||||
"expected bypass of %s to be False" % host)
|
||||
for host in ("test", "localhost"):
|
||||
self.assertTrue(proxy_bypass(host, proxy_override),
|
||||
"expect <local> to bypass intranet address '%s'"
|
||||
% host)
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'darwin', "only relevant for OSX")
|
||||
def test_osx_proxy_bypass(self):
|
||||
bypass = {
|
||||
@@ -1483,7 +1563,7 @@ class HandlerTests(unittest.TestCase):
|
||||
password_manager = MockPasswordManager()
|
||||
auth_handler = urllib.request.HTTPBasicAuthHandler(password_manager)
|
||||
body = '\r\n'.join(headers) + '\r\n\r\n'
|
||||
http_handler = MockHTTPHandler(401, body)
|
||||
http_handler = MockHTTPHandlerRedirect(401, body)
|
||||
opener.add_handler(auth_handler)
|
||||
opener.add_handler(http_handler)
|
||||
self._test_basic_auth(opener, auth_handler, "Authorization",
|
||||
@@ -1543,7 +1623,7 @@ class HandlerTests(unittest.TestCase):
|
||||
password_manager = MockPasswordManager()
|
||||
auth_handler = urllib.request.ProxyBasicAuthHandler(password_manager)
|
||||
realm = "ACME Networks"
|
||||
http_handler = MockHTTPHandler(
|
||||
http_handler = MockHTTPHandlerRedirect(
|
||||
407, 'Proxy-Authenticate: Basic realm="%s"\r\n\r\n' % realm)
|
||||
opener.add_handler(auth_handler)
|
||||
opener.add_handler(http_handler)
|
||||
@@ -1555,11 +1635,11 @@ class HandlerTests(unittest.TestCase):
|
||||
|
||||
def test_basic_and_digest_auth_handlers(self):
|
||||
# HTTPDigestAuthHandler raised an exception if it couldn't handle a 40*
|
||||
# response (http://python.org/sf/1479302), where it should instead
|
||||
# response (https://bugs.python.org/issue1479302), where it should instead
|
||||
# return None to allow another handler (especially
|
||||
# HTTPBasicAuthHandler) to handle the response.
|
||||
|
||||
# Also (http://python.org/sf/14797027, RFC 2617 section 1.2), we must
|
||||
# Also (https://bugs.python.org/issue14797027, RFC 2617 section 1.2), we must
|
||||
# try digest first (since it's the strongest auth scheme), so we record
|
||||
# order of calls here to check digest comes first:
|
||||
class RecordingOpenerDirector(OpenerDirector):
|
||||
@@ -1587,7 +1667,7 @@ class HandlerTests(unittest.TestCase):
|
||||
digest_handler = TestDigestAuthHandler(password_manager)
|
||||
basic_handler = TestBasicAuthHandler(password_manager)
|
||||
realm = "ACME Networks"
|
||||
http_handler = MockHTTPHandler(
|
||||
http_handler = MockHTTPHandlerRedirect(
|
||||
401, 'WWW-Authenticate: Basic realm="%s"\r\n\r\n' % realm)
|
||||
opener.add_handler(basic_handler)
|
||||
opener.add_handler(digest_handler)
|
||||
@@ -1607,7 +1687,7 @@ class HandlerTests(unittest.TestCase):
|
||||
opener = OpenerDirector()
|
||||
# While using DigestAuthHandler
|
||||
digest_auth_handler = urllib.request.HTTPDigestAuthHandler(None)
|
||||
http_handler = MockHTTPHandler(
|
||||
http_handler = MockHTTPHandlerRedirect(
|
||||
401, 'WWW-Authenticate: Kerberos\r\n\r\n')
|
||||
opener.add_handler(digest_auth_handler)
|
||||
opener.add_handler(http_handler)
|
||||
@@ -1617,7 +1697,7 @@ class HandlerTests(unittest.TestCase):
|
||||
# While using BasicAuthHandler
|
||||
opener = OpenerDirector()
|
||||
basic_auth_handler = urllib.request.HTTPBasicAuthHandler(None)
|
||||
http_handler = MockHTTPHandler(
|
||||
http_handler = MockHTTPHandlerRedirect(
|
||||
401, 'WWW-Authenticate: NTLM\r\n\r\n')
|
||||
opener.add_handler(basic_auth_handler)
|
||||
opener.add_handler(http_handler)
|
||||
@@ -1704,7 +1784,7 @@ class HandlerTests(unittest.TestCase):
|
||||
opener = OpenerDirector()
|
||||
opener.add_handler(auth_prior_handler)
|
||||
|
||||
http_handler = MockHTTPHandler(
|
||||
http_handler = MockHTTPHandlerRedirect(
|
||||
401, 'WWW-Authenticate: Basic realm="%s"\r\n\r\n' % None)
|
||||
opener.add_handler(http_handler)
|
||||
|
||||
@@ -1755,7 +1835,7 @@ class HandlerTests(unittest.TestCase):
|
||||
self.assertTrue(conn.fakesock.closed, "Connection not closed")
|
||||
|
||||
|
||||
class MiscTests(unittest.TestCase):
|
||||
class MiscTests(unittest.TestCase, ExtraAssertions):
|
||||
|
||||
def opener_has_handler(self, opener, handler_class):
|
||||
self.assertTrue(any(h.__class__ == handler_class
|
||||
@@ -1814,14 +1894,21 @@ class MiscTests(unittest.TestCase):
|
||||
url = code = fp = None
|
||||
hdrs = 'Content-Length: 42'
|
||||
err = urllib.error.HTTPError(url, code, msg, hdrs, fp)
|
||||
self.assertTrue(hasattr(err, 'reason'))
|
||||
self.assertHasAttr(err, 'reason')
|
||||
self.assertEqual(err.reason, 'something bad happened')
|
||||
self.assertTrue(hasattr(err, 'headers'))
|
||||
self.assertHasAttr(err, 'headers')
|
||||
self.assertEqual(err.headers, 'Content-Length: 42')
|
||||
expected_errmsg = 'HTTP Error %s: %s' % (err.code, err.msg)
|
||||
self.assertEqual(str(err), expected_errmsg)
|
||||
expected_errmsg = '<HTTPError %s: %r>' % (err.code, err.msg)
|
||||
self.assertEqual(repr(err), expected_errmsg)
|
||||
err.close()
|
||||
|
||||
def test_gh_98778(self):
|
||||
x = urllib.error.HTTPError("url", 405, "METHOD NOT ALLOWED", None, None)
|
||||
self.assertEqual(getattr(x, "__notes__", ()), ())
|
||||
self.assertIsInstance(x.fp.read(), bytes)
|
||||
x.close()
|
||||
|
||||
def test_parse_proxy(self):
|
||||
parse_proxy_test_cases = [
|
||||
|
||||
Vendored
+35
-57
@@ -8,15 +8,18 @@ import threading
|
||||
import unittest
|
||||
import hashlib
|
||||
|
||||
from test import support
|
||||
from test.support import hashlib_helper
|
||||
from test.support import threading_helper
|
||||
from test.support import warnings_helper
|
||||
from test.support.testcase import ExtraAssertions
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
support.requires_working_socket(module=True)
|
||||
|
||||
here = os.path.dirname(__file__)
|
||||
# Self-signed cert file for 'localhost'
|
||||
CERT_localhost = os.path.join(here, 'certdata', 'keycert.pem')
|
||||
@@ -314,7 +317,9 @@ class BasicAuthTests(unittest.TestCase):
|
||||
ah = urllib.request.HTTPBasicAuthHandler()
|
||||
ah.add_password(self.REALM, self.server_url, self.USER, self.INCORRECT_PASSWD)
|
||||
urllib.request.install_opener(urllib.request.build_opener(ah))
|
||||
self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, self.server_url)
|
||||
with self.assertRaises(urllib.error.HTTPError) as cm:
|
||||
urllib.request.urlopen(self.server_url)
|
||||
cm.exception.close()
|
||||
|
||||
|
||||
@hashlib_helper.requires_hashdigest("md5", openssl=True)
|
||||
@@ -356,23 +361,23 @@ class ProxyAuthTests(unittest.TestCase):
|
||||
self.server.stop()
|
||||
self.server = None
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_proxy_with_bad_password_raises_httperror(self):
|
||||
self.proxy_digest_handler.add_password(self.REALM, self.URL,
|
||||
self.USER, self.PASSWD+"bad")
|
||||
self.digest_auth_handler.set_qop("auth")
|
||||
self.assertRaises(urllib.error.HTTPError,
|
||||
self.opener.open,
|
||||
self.URL)
|
||||
with self.assertRaises(urllib.error.HTTPError) as cm:
|
||||
self.opener.open(self.URL)
|
||||
cm.exception.close()
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_proxy_with_no_password_raises_httperror(self):
|
||||
self.digest_auth_handler.set_qop("auth")
|
||||
self.assertRaises(urllib.error.HTTPError,
|
||||
self.opener.open,
|
||||
self.URL)
|
||||
with self.assertRaises(urllib.error.HTTPError) as cm:
|
||||
self.opener.open(self.URL)
|
||||
cm.exception.close()
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_proxy_qop_auth_works(self):
|
||||
self.proxy_digest_handler.add_password(self.REALM, self.URL,
|
||||
self.USER, self.PASSWD)
|
||||
@@ -381,7 +386,7 @@ class ProxyAuthTests(unittest.TestCase):
|
||||
while result.read():
|
||||
pass
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_proxy_qop_auth_int_works_or_throws_urlerror(self):
|
||||
self.proxy_digest_handler.add_password(self.REALM, self.URL,
|
||||
self.USER, self.PASSWD)
|
||||
@@ -442,7 +447,7 @@ def GetRequestHandler(responses):
|
||||
return FakeHTTPRequestHandler
|
||||
|
||||
|
||||
class TestUrlopen(unittest.TestCase):
|
||||
class TestUrlopen(unittest.TestCase, ExtraAssertions):
|
||||
"""Tests urllib.request.urlopen using the network.
|
||||
|
||||
These tests are not exhaustive. Assuming that testing using files does a
|
||||
@@ -506,7 +511,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
handler.port = server.port
|
||||
return handler
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_redirection(self):
|
||||
expected_response = b"We got here..."
|
||||
responses = [
|
||||
@@ -520,7 +525,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertEqual(data, expected_response)
|
||||
self.assertEqual(handler.requests, ["/", "/somewhere_else"])
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_chunked(self):
|
||||
expected_response = b"hello world"
|
||||
chunked_start = (
|
||||
@@ -535,7 +540,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
data = self.urlopen("http://localhost:%s/" % handler.port)
|
||||
self.assertEqual(data, expected_response)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_404(self):
|
||||
expected_response = b"Bad bad bad..."
|
||||
handler = self.start_server([(404, [], expected_response)])
|
||||
@@ -551,7 +556,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertEqual(data, expected_response)
|
||||
self.assertEqual(handler.requests, ["/weeble"])
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_200(self):
|
||||
expected_response = b"pycon 2008..."
|
||||
handler = self.start_server([(200, [], expected_response)])
|
||||
@@ -559,7 +564,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertEqual(data, expected_response)
|
||||
self.assertEqual(handler.requests, ["/bizarre"])
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_200_with_parameters(self):
|
||||
expected_response = b"pycon 2008..."
|
||||
handler = self.start_server([(200, [], expected_response)])
|
||||
@@ -568,41 +573,14 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertEqual(data, expected_response)
|
||||
self.assertEqual(handler.requests, ["/bizarre", b"get=with_feeling"])
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_https(self):
|
||||
handler = self.start_https_server()
|
||||
context = ssl.create_default_context(cafile=CERT_localhost)
|
||||
data = self.urlopen("https://localhost:%s/bizarre" % handler.port, context=context)
|
||||
self.assertEqual(data, b"we care a bit")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
def test_https_with_cafile(self):
|
||||
handler = self.start_https_server(certfile=CERT_localhost)
|
||||
with warnings_helper.check_warnings(('', DeprecationWarning)):
|
||||
# Good cert
|
||||
data = self.urlopen("https://localhost:%s/bizarre" % handler.port,
|
||||
cafile=CERT_localhost)
|
||||
self.assertEqual(data, b"we care a bit")
|
||||
# Bad cert
|
||||
with self.assertRaises(urllib.error.URLError) as cm:
|
||||
self.urlopen("https://localhost:%s/bizarre" % handler.port,
|
||||
cafile=CERT_fakehostname)
|
||||
# Good cert, but mismatching hostname
|
||||
handler = self.start_https_server(certfile=CERT_fakehostname)
|
||||
with self.assertRaises(urllib.error.URLError) as cm:
|
||||
self.urlopen("https://localhost:%s/bizarre" % handler.port,
|
||||
cafile=CERT_fakehostname)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
def test_https_with_cadefault(self):
|
||||
handler = self.start_https_server(certfile=CERT_localhost)
|
||||
# Self-signed cert should fail verification with system certificate store
|
||||
with warnings_helper.check_warnings(('', DeprecationWarning)):
|
||||
with self.assertRaises(urllib.error.URLError) as cm:
|
||||
self.urlopen("https://localhost:%s/bizarre" % handler.port,
|
||||
cadefault=True)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_https_sni(self):
|
||||
if ssl is None:
|
||||
self.skipTest("ssl module required")
|
||||
@@ -619,7 +597,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.urlopen("https://localhost:%s" % handler.port, context=context)
|
||||
self.assertEqual(sni_name, "localhost")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_sending_headers(self):
|
||||
handler = self.start_server()
|
||||
req = urllib.request.Request("http://localhost:%s/" % handler.port,
|
||||
@@ -628,7 +606,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
pass
|
||||
self.assertEqual(handler.headers_received["Range"], "bytes=20-39")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_sending_headers_camel(self):
|
||||
handler = self.start_server()
|
||||
req = urllib.request.Request("http://localhost:%s/" % handler.port,
|
||||
@@ -638,16 +616,15 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertIn("X-Some-Header", handler.headers_received.keys())
|
||||
self.assertNotIn("X-SoMe-hEader", handler.headers_received.keys())
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_basic(self):
|
||||
handler = self.start_server()
|
||||
with urllib.request.urlopen("http://localhost:%s" % handler.port) as open_url:
|
||||
for attr in ("read", "close", "info", "geturl"):
|
||||
self.assertTrue(hasattr(open_url, attr), "object returned from "
|
||||
"urlopen lacks the %s attribute" % attr)
|
||||
self.assertHasAttr(open_url, attr)
|
||||
self.assertTrue(open_url.read(), "calling 'read' failed")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_info(self):
|
||||
handler = self.start_server()
|
||||
open_url = urllib.request.urlopen(
|
||||
@@ -659,7 +636,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
"instance of email.message.Message")
|
||||
self.assertEqual(info_obj.get_content_subtype(), "plain")
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_geturl(self):
|
||||
# Make sure same URL as opened is returned by geturl.
|
||||
handler = self.start_server()
|
||||
@@ -668,7 +645,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
url = open_url.geturl()
|
||||
self.assertEqual(url, "http://localhost:%s" % handler.port)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_iteration(self):
|
||||
expected_response = b"pycon 2008..."
|
||||
handler = self.start_server([(200, [], expected_response)])
|
||||
@@ -676,7 +653,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
for line in data:
|
||||
self.assertEqual(line, expected_response)
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_line_iteration(self):
|
||||
lines = [b"We\n", b"got\n", b"here\n", b"verylong " * 8192 + b"\n"]
|
||||
expected_response = b"".join(lines)
|
||||
@@ -689,7 +666,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
(index, len(lines[index]), len(line)))
|
||||
self.assertEqual(index + 1, len(lines))
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
|
||||
@unittest.skipIf(os.name == 'nt', 'TODO: RUSTPYTHON; ValueError: illegal environment variable name')
|
||||
def test_issue16464(self):
|
||||
# See https://bugs.python.org/issue16464
|
||||
# and https://bugs.python.org/issue46648
|
||||
@@ -709,6 +686,7 @@ class TestUrlopen(unittest.TestCase):
|
||||
self.assertEqual(b"1234567890", request.data)
|
||||
self.assertEqual("10", request.get_header("Content-length"))
|
||||
|
||||
|
||||
def setUpModule():
|
||||
thread_info = threading_helper.threading_setup()
|
||||
unittest.addModuleCleanup(threading_helper.threading_cleanup, *thread_info)
|
||||
|
||||
Vendored
-1
@@ -137,7 +137,6 @@ class OtherNetworkTests(unittest.TestCase):
|
||||
# XXX The rest of these tests aren't very good -- they don't check much.
|
||||
# They do sometimes catch some major disasters, though.
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON urllib.error.URLError: <urlopen error ftp error: error_reply('200 Switching to Binary mode.')>
|
||||
@support.requires_resource('walltime')
|
||||
def test_ftp(self):
|
||||
# Testing the same URL twice exercises the caching in CacheFTPHandler
|
||||
|
||||
Vendored
+7
@@ -4,6 +4,11 @@ import socket
|
||||
import tempfile
|
||||
import urllib.response
|
||||
import unittest
|
||||
from test import support
|
||||
|
||||
if support.is_wasi:
|
||||
raise unittest.SkipTest("Cannot create socket on WASI")
|
||||
|
||||
|
||||
class TestResponse(unittest.TestCase):
|
||||
|
||||
@@ -43,6 +48,7 @@ class TestResponse(unittest.TestCase):
|
||||
info = urllib.response.addinfo(self.fp, self.test_headers)
|
||||
self.assertEqual(info.info(), self.test_headers)
|
||||
self.assertEqual(info.headers, self.test_headers)
|
||||
info.close()
|
||||
|
||||
def test_addinfourl(self):
|
||||
url = "http://www.python.org"
|
||||
@@ -55,6 +61,7 @@ class TestResponse(unittest.TestCase):
|
||||
self.assertEqual(infourl.headers, self.test_headers)
|
||||
self.assertEqual(infourl.url, url)
|
||||
self.assertEqual(infourl.status, code)
|
||||
infourl.close()
|
||||
|
||||
def tearDown(self):
|
||||
self.sock.close()
|
||||
|
||||
Vendored
-1
@@ -134,7 +134,6 @@ class IntegrationTests(TestCase):
|
||||
b"Python test,Python test 2;query=test;/path/"
|
||||
)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; http library needs to be updated
|
||||
def test_request_length(self):
|
||||
out, err = run_amock(data=b"GET " + (b"x" * 65537) + b" HTTP/1.0\n\n")
|
||||
self.assertEqual(out.splitlines()[0],
|
||||
|
||||
Vendored
+4
-7
@@ -10,7 +10,7 @@ responses, with a status code, headers, and a body. In some contexts,
|
||||
an application may want to handle an exception like a regular
|
||||
response.
|
||||
"""
|
||||
|
||||
import io
|
||||
import urllib.response
|
||||
|
||||
__all__ = ['URLError', 'HTTPError', 'ContentTooShortError']
|
||||
@@ -42,12 +42,9 @@ class HTTPError(URLError, urllib.response.addinfourl):
|
||||
self.hdrs = hdrs
|
||||
self.fp = fp
|
||||
self.filename = url
|
||||
# The addinfourl classes depend on fp being a valid file
|
||||
# object. In some cases, the HTTPError may not have a valid
|
||||
# file object. If this happens, the simplest workaround is to
|
||||
# not initialize the base classes.
|
||||
if fp is not None:
|
||||
self.__super_init(fp, hdrs, url, code)
|
||||
if fp is None:
|
||||
fp = io.BytesIO()
|
||||
self.__super_init(fp, hdrs, url, code)
|
||||
|
||||
def __str__(self):
|
||||
return 'HTTP Error %s: %s' % (self.code, self.msg)
|
||||
|
||||
Vendored
+151
-85
@@ -25,13 +25,19 @@ currently not entirely compliant with this RFC due to defacto
|
||||
scenarios for parsing, and for backward compatibility purposes, some
|
||||
parsing quirks from older RFCs are retained. The testcases in
|
||||
test_urlparse.py provides a good indicator of parsing behavior.
|
||||
|
||||
The WHATWG URL Parser spec should also be considered. We are not compliant with
|
||||
it either due to existing user code API behavior expectations (Hyrum's Law).
|
||||
It serves as a useful guide when making changes.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import collections
|
||||
import warnings
|
||||
import ipaddress
|
||||
|
||||
__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
|
||||
"urlsplit", "urlunsplit", "urlencode", "parse_qs",
|
||||
@@ -46,18 +52,18 @@ __all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
|
||||
|
||||
uses_relative = ['', 'ftp', 'http', 'gopher', 'nntp', 'imap',
|
||||
'wais', 'file', 'https', 'shttp', 'mms',
|
||||
'prospero', 'rtsp', 'rtspu', 'sftp',
|
||||
'prospero', 'rtsp', 'rtsps', 'rtspu', 'sftp',
|
||||
'svn', 'svn+ssh', 'ws', 'wss']
|
||||
|
||||
uses_netloc = ['', 'ftp', 'http', 'gopher', 'nntp', 'telnet',
|
||||
'imap', 'wais', 'file', 'mms', 'https', 'shttp',
|
||||
'snews', 'prospero', 'rtsp', 'rtspu', 'rsync',
|
||||
'snews', 'prospero', 'rtsp', 'rtsps', 'rtspu', 'rsync',
|
||||
'svn', 'svn+ssh', 'sftp', 'nfs', 'git', 'git+ssh',
|
||||
'ws', 'wss']
|
||||
'ws', 'wss', 'itms-services']
|
||||
|
||||
uses_params = ['', 'ftp', 'hdl', 'prospero', 'http', 'imap',
|
||||
'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips',
|
||||
'mms', 'sftp', 'tel']
|
||||
'https', 'shttp', 'rtsp', 'rtsps', 'rtspu', 'sip',
|
||||
'sips', 'mms', 'sftp', 'tel']
|
||||
|
||||
# These are not actually used anymore, but should stay for backwards
|
||||
# compatibility. (They are undocumented, but have a public-looking name.)
|
||||
@@ -66,7 +72,7 @@ non_hierarchical = ['gopher', 'hdl', 'mailto', 'news',
|
||||
'telnet', 'wais', 'imap', 'snews', 'sip', 'sips']
|
||||
|
||||
uses_query = ['', 'http', 'wais', 'imap', 'https', 'shttp', 'mms',
|
||||
'gopher', 'rtsp', 'rtspu', 'sip', 'sips']
|
||||
'gopher', 'rtsp', 'rtsps', 'rtspu', 'sip', 'sips']
|
||||
|
||||
uses_fragment = ['', 'ftp', 'hdl', 'http', 'gopher', 'news',
|
||||
'nntp', 'wais', 'https', 'shttp', 'snews',
|
||||
@@ -78,18 +84,17 @@ scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
|
||||
'0123456789'
|
||||
'+-.')
|
||||
|
||||
# Leading and trailing C0 control and space to be stripped per WHATWG spec.
|
||||
# == "".join([chr(i) for i in range(0, 0x20 + 1)])
|
||||
_WHATWG_C0_CONTROL_OR_SPACE = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '
|
||||
|
||||
# Unsafe bytes to be removed per WHATWG spec
|
||||
_UNSAFE_URL_BYTES_TO_REMOVE = ['\t', '\r', '\n']
|
||||
|
||||
# XXX: Consider replacing with functools.lru_cache
|
||||
MAX_CACHE_SIZE = 20
|
||||
_parse_cache = {}
|
||||
|
||||
def clear_cache():
|
||||
"""Clear the parse cache and the quoters cache."""
|
||||
_parse_cache.clear()
|
||||
_safe_quoters.clear()
|
||||
|
||||
"""Clear internal performance caches. Undocumented; some tests want it."""
|
||||
urlsplit.cache_clear()
|
||||
_byte_quoter_factory.cache_clear()
|
||||
|
||||
# Helpers for bytes handling
|
||||
# For 3.2, we deliberately require applications that
|
||||
@@ -171,12 +176,11 @@ class _NetlocResultMixinBase(object):
|
||||
def port(self):
|
||||
port = self._hostinfo[1]
|
||||
if port is not None:
|
||||
try:
|
||||
port = int(port, 10)
|
||||
except ValueError:
|
||||
message = f'Port could not be cast to integer value as {port!r}'
|
||||
raise ValueError(message) from None
|
||||
if not ( 0 <= port <= 65535):
|
||||
if port.isdigit() and port.isascii():
|
||||
port = int(port)
|
||||
else:
|
||||
raise ValueError(f"Port could not be cast to integer value as {port!r}")
|
||||
if not (0 <= port <= 65535):
|
||||
raise ValueError("Port out of range 0-65535")
|
||||
return port
|
||||
|
||||
@@ -243,8 +247,6 @@ class _NetlocResultMixinBytes(_NetlocResultMixinBase, _ResultMixinBytes):
|
||||
return hostname, port
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
_DefragResultBase = namedtuple('DefragResult', 'url fragment')
|
||||
_SplitResultBase = namedtuple(
|
||||
'SplitResult', 'scheme netloc path query fragment')
|
||||
@@ -434,6 +436,37 @@ def _checknetloc(netloc):
|
||||
raise ValueError("netloc '" + netloc + "' contains invalid " +
|
||||
"characters under NFKC normalization")
|
||||
|
||||
def _check_bracketed_netloc(netloc):
|
||||
# Note that this function must mirror the splitting
|
||||
# done in NetlocResultMixins._hostinfo().
|
||||
hostname_and_port = netloc.rpartition('@')[2]
|
||||
before_bracket, have_open_br, bracketed = hostname_and_port.partition('[')
|
||||
if have_open_br:
|
||||
# No data is allowed before a bracket.
|
||||
if before_bracket:
|
||||
raise ValueError("Invalid IPv6 URL")
|
||||
hostname, _, port = bracketed.partition(']')
|
||||
# No data is allowed after the bracket but before the port delimiter.
|
||||
if port and not port.startswith(":"):
|
||||
raise ValueError("Invalid IPv6 URL")
|
||||
else:
|
||||
hostname, _, port = hostname_and_port.partition(':')
|
||||
_check_bracketed_host(hostname)
|
||||
|
||||
# Valid bracketed hosts are defined in
|
||||
# https://www.rfc-editor.org/rfc/rfc3986#page-49 and https://url.spec.whatwg.org/
|
||||
def _check_bracketed_host(hostname):
|
||||
if hostname.startswith('v'):
|
||||
if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
|
||||
raise ValueError(f"IPvFuture address is invalid")
|
||||
else:
|
||||
ip = ipaddress.ip_address(hostname) # Throws Value Error if not IPv6 or IPv4
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
raise ValueError(f"An IPv4 address cannot be in brackets")
|
||||
|
||||
# typed=True avoids BytesWarnings being emitted during cache key
|
||||
# comparison since this API supports both bytes and str input.
|
||||
@functools.lru_cache(typed=True)
|
||||
def urlsplit(url, scheme='', allow_fragments=True):
|
||||
"""Parse a URL into 5 components:
|
||||
<scheme>://<netloc>/<path>?<query>#<fragment>
|
||||
@@ -456,39 +489,37 @@ def urlsplit(url, scheme='', allow_fragments=True):
|
||||
"""
|
||||
|
||||
url, scheme, _coerce_result = _coerce_args(url, scheme)
|
||||
# Only lstrip url as some applications rely on preserving trailing space.
|
||||
# (https://url.spec.whatwg.org/#concept-basic-url-parser would strip both)
|
||||
url = url.lstrip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||
scheme = scheme.strip(_WHATWG_C0_CONTROL_OR_SPACE)
|
||||
|
||||
for b in _UNSAFE_URL_BYTES_TO_REMOVE:
|
||||
url = url.replace(b, "")
|
||||
scheme = scheme.replace(b, "")
|
||||
|
||||
allow_fragments = bool(allow_fragments)
|
||||
key = url, scheme, allow_fragments, type(url), type(scheme)
|
||||
cached = _parse_cache.get(key, None)
|
||||
if cached:
|
||||
return _coerce_result(cached)
|
||||
if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
|
||||
clear_cache()
|
||||
netloc = query = fragment = ''
|
||||
i = url.find(':')
|
||||
if i > 0:
|
||||
if i > 0 and url[0].isascii() and url[0].isalpha():
|
||||
for c in url[:i]:
|
||||
if c not in scheme_chars:
|
||||
break
|
||||
else:
|
||||
scheme, url = url[:i].lower(), url[i+1:]
|
||||
|
||||
if url[:2] == '//':
|
||||
netloc, url = _splitnetloc(url, 2)
|
||||
if (('[' in netloc and ']' not in netloc) or
|
||||
(']' in netloc and '[' not in netloc)):
|
||||
raise ValueError("Invalid IPv6 URL")
|
||||
if '[' in netloc and ']' in netloc:
|
||||
_check_bracketed_netloc(netloc)
|
||||
if allow_fragments and '#' in url:
|
||||
url, fragment = url.split('#', 1)
|
||||
if '?' in url:
|
||||
url, query = url.split('?', 1)
|
||||
_checknetloc(netloc)
|
||||
v = SplitResult(scheme, netloc, url, query, fragment)
|
||||
_parse_cache[key] = v
|
||||
return _coerce_result(v)
|
||||
|
||||
def urlunparse(components):
|
||||
@@ -510,9 +541,13 @@ def urlunsplit(components):
|
||||
empty query; the RFC states that these are equivalent)."""
|
||||
scheme, netloc, url, query, fragment, _coerce_result = (
|
||||
_coerce_args(*components))
|
||||
if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
|
||||
if netloc:
|
||||
if url and url[:1] != '/': url = '/' + url
|
||||
url = '//' + (netloc or '') + url
|
||||
url = '//' + netloc + url
|
||||
elif url[:2] == '//':
|
||||
url = '//' + url
|
||||
elif scheme and scheme in uses_netloc and (not url or url[:1] == '/'):
|
||||
url = '//' + url
|
||||
if scheme:
|
||||
url = scheme + ':' + url
|
||||
if query:
|
||||
@@ -611,6 +646,9 @@ _hextobyte = None
|
||||
|
||||
def unquote_to_bytes(string):
|
||||
"""unquote_to_bytes('abc%20def') -> b'abc def'."""
|
||||
return bytes(_unquote_impl(string))
|
||||
|
||||
def _unquote_impl(string: bytes | bytearray | str) -> bytes | bytearray:
|
||||
# Note: strings are encoded as UTF-8. This is only an issue if it contains
|
||||
# unescaped non-ASCII characters, which URIs should not.
|
||||
if not string:
|
||||
@@ -622,8 +660,8 @@ def unquote_to_bytes(string):
|
||||
bits = string.split(b'%')
|
||||
if len(bits) == 1:
|
||||
return string
|
||||
res = [bits[0]]
|
||||
append = res.append
|
||||
res = bytearray(bits[0])
|
||||
append = res.extend
|
||||
# Delay the initialization of the table to not waste memory
|
||||
# if the function is never called
|
||||
global _hextobyte
|
||||
@@ -637,10 +675,20 @@ def unquote_to_bytes(string):
|
||||
except KeyError:
|
||||
append(b'%')
|
||||
append(item)
|
||||
return b''.join(res)
|
||||
return res
|
||||
|
||||
_asciire = re.compile('([\x00-\x7f]+)')
|
||||
|
||||
def _generate_unquoted_parts(string, encoding, errors):
|
||||
previous_match_end = 0
|
||||
for ascii_match in _asciire.finditer(string):
|
||||
start, end = ascii_match.span()
|
||||
yield string[previous_match_end:start] # Non-ASCII
|
||||
# The ascii_match[1] group == string[start:end].
|
||||
yield _unquote_impl(ascii_match[1]).decode(encoding, errors)
|
||||
previous_match_end = end
|
||||
yield string[previous_match_end:] # Non-ASCII tail
|
||||
|
||||
def unquote(string, encoding='utf-8', errors='replace'):
|
||||
"""Replace %xx escapes by their single-character equivalent. The optional
|
||||
encoding and errors parameters specify how to decode percent-encoded
|
||||
@@ -652,21 +700,16 @@ def unquote(string, encoding='utf-8', errors='replace'):
|
||||
unquote('abc%20def') -> 'abc def'.
|
||||
"""
|
||||
if isinstance(string, bytes):
|
||||
return unquote_to_bytes(string).decode(encoding, errors)
|
||||
return _unquote_impl(string).decode(encoding, errors)
|
||||
if '%' not in string:
|
||||
# Is it a string-like object?
|
||||
string.split
|
||||
return string
|
||||
if encoding is None:
|
||||
encoding = 'utf-8'
|
||||
if errors is None:
|
||||
errors = 'replace'
|
||||
bits = _asciire.split(string)
|
||||
res = [bits[0]]
|
||||
append = res.append
|
||||
for i in range(1, len(bits), 2):
|
||||
append(unquote_to_bytes(bits[i]).decode(encoding, errors))
|
||||
append(bits[i + 1])
|
||||
return ''.join(res)
|
||||
return ''.join(_generate_unquoted_parts(string, encoding, errors))
|
||||
|
||||
|
||||
def parse_qs(qs, keep_blank_values=False, strict_parsing=False,
|
||||
@@ -740,11 +783,29 @@ def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
|
||||
|
||||
Returns a list, as G-d intended.
|
||||
"""
|
||||
qs, _coerce_result = _coerce_args(qs)
|
||||
separator, _ = _coerce_args(separator)
|
||||
|
||||
if not separator or (not isinstance(separator, (str, bytes))):
|
||||
if not separator or not isinstance(separator, (str, bytes)):
|
||||
raise ValueError("Separator must be of type string or bytes.")
|
||||
if isinstance(qs, str):
|
||||
if not isinstance(separator, str):
|
||||
separator = str(separator, 'ascii')
|
||||
eq = '='
|
||||
def _unquote(s):
|
||||
return unquote_plus(s, encoding=encoding, errors=errors)
|
||||
else:
|
||||
if not qs:
|
||||
return []
|
||||
# Use memoryview() to reject integers and iterables,
|
||||
# acceptable by the bytes constructor.
|
||||
qs = bytes(memoryview(qs))
|
||||
if isinstance(separator, str):
|
||||
separator = bytes(separator, 'ascii')
|
||||
eq = b'='
|
||||
def _unquote(s):
|
||||
return unquote_to_bytes(s.replace(b'+', b' '))
|
||||
|
||||
if not qs:
|
||||
return []
|
||||
|
||||
# If max_num_fields is defined then check that the number of fields
|
||||
# is less than max_num_fields. This prevents a memory exhaustion DOS
|
||||
@@ -756,25 +817,14 @@ def parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
|
||||
|
||||
r = []
|
||||
for name_value in qs.split(separator):
|
||||
if not name_value and not strict_parsing:
|
||||
continue
|
||||
nv = name_value.split('=', 1)
|
||||
if len(nv) != 2:
|
||||
if strict_parsing:
|
||||
if name_value or strict_parsing:
|
||||
name, has_eq, value = name_value.partition(eq)
|
||||
if not has_eq and strict_parsing:
|
||||
raise ValueError("bad query field: %r" % (name_value,))
|
||||
# Handle case of a control-name with no equal sign
|
||||
if keep_blank_values:
|
||||
nv.append('')
|
||||
else:
|
||||
continue
|
||||
if len(nv[1]) or keep_blank_values:
|
||||
name = nv[0].replace('+', ' ')
|
||||
name = unquote(name, encoding=encoding, errors=errors)
|
||||
name = _coerce_result(name)
|
||||
value = nv[1].replace('+', ' ')
|
||||
value = unquote(value, encoding=encoding, errors=errors)
|
||||
value = _coerce_result(value)
|
||||
r.append((name, value))
|
||||
if value or keep_blank_values:
|
||||
name = _unquote(name)
|
||||
value = _unquote(value)
|
||||
r.append((name, value))
|
||||
return r
|
||||
|
||||
def unquote_plus(string, encoding='utf-8', errors='replace'):
|
||||
@@ -791,23 +841,30 @@ _ALWAYS_SAFE = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
b'0123456789'
|
||||
b'_.-~')
|
||||
_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE)
|
||||
_safe_quoters = {}
|
||||
|
||||
class Quoter(collections.defaultdict):
|
||||
"""A mapping from bytes (in range(0,256)) to strings.
|
||||
def __getattr__(name):
|
||||
if name == 'Quoter':
|
||||
warnings.warn('Deprecated in 3.11. '
|
||||
'urllib.parse.Quoter will be removed in Python 3.14. '
|
||||
'It was not intended to be a public API.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return _Quoter
|
||||
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
||||
|
||||
class _Quoter(dict):
|
||||
"""A mapping from bytes numbers (in range(0,256)) to strings.
|
||||
|
||||
String values are percent-encoded byte values, unless the key < 128, and
|
||||
in the "safe" set (either the specified safe set, or default set).
|
||||
in either of the specified safe set, or the always safe set.
|
||||
"""
|
||||
# Keeps a cache internally, using defaultdict, for efficiency (lookups
|
||||
# Keeps a cache internally, via __missing__, for efficiency (lookups
|
||||
# of cached keys don't call Python code at all).
|
||||
def __init__(self, safe):
|
||||
"""safe: bytes object."""
|
||||
self.safe = _ALWAYS_SAFE.union(safe)
|
||||
|
||||
def __repr__(self):
|
||||
# Without this, will just display as a defaultdict
|
||||
return "<%s %r>" % (self.__class__.__name__, dict(self))
|
||||
return f"<Quoter {dict(self)!r}>"
|
||||
|
||||
def __missing__(self, b):
|
||||
# Handle a cache miss. Store quoted string in cache and return.
|
||||
@@ -886,6 +943,11 @@ def quote_plus(string, safe='', encoding=None, errors=None):
|
||||
string = quote(string, safe + space, encoding, errors)
|
||||
return string.replace(' ', '+')
|
||||
|
||||
# Expectation: A typical program is unlikely to create more than 5 of these.
|
||||
@functools.lru_cache
|
||||
def _byte_quoter_factory(safe):
|
||||
return _Quoter(safe).__getitem__
|
||||
|
||||
def quote_from_bytes(bs, safe='/'):
|
||||
"""Like quote(), but accepts a bytes object rather than a str, and does
|
||||
not perform string-to-bytes encoding. It always returns an ASCII string.
|
||||
@@ -899,14 +961,19 @@ def quote_from_bytes(bs, safe='/'):
|
||||
# Normalize 'safe' by converting to bytes and removing non-ASCII chars
|
||||
safe = safe.encode('ascii', 'ignore')
|
||||
else:
|
||||
# List comprehensions are faster than generator expressions.
|
||||
safe = bytes([c for c in safe if c < 128])
|
||||
if not bs.rstrip(_ALWAYS_SAFE_BYTES + safe):
|
||||
return bs.decode()
|
||||
try:
|
||||
quoter = _safe_quoters[safe]
|
||||
except KeyError:
|
||||
_safe_quoters[safe] = quoter = Quoter(safe).__getitem__
|
||||
return ''.join([quoter(char) for char in bs])
|
||||
quoter = _byte_quoter_factory(safe)
|
||||
if (bs_len := len(bs)) < 200_000:
|
||||
return ''.join(map(quoter, bs))
|
||||
else:
|
||||
# This saves memory - https://github.com/python/cpython/issues/95865
|
||||
chunk_size = math.isqrt(bs_len)
|
||||
chunks = [''.join(map(quoter, bs[i:i+chunk_size]))
|
||||
for i in range(0, bs_len, chunk_size)]
|
||||
return ''.join(chunks)
|
||||
|
||||
def urlencode(query, doseq=False, safe='', encoding=None, errors=None,
|
||||
quote_via=quote_plus):
|
||||
@@ -939,10 +1006,9 @@ def urlencode(query, doseq=False, safe='', encoding=None, errors=None,
|
||||
# but that's a minor nit. Since the original implementation
|
||||
# allowed empty dicts that type of behavior probably should be
|
||||
# preserved for consistency
|
||||
except TypeError:
|
||||
ty, va, tb = sys.exc_info()
|
||||
except TypeError as err:
|
||||
raise TypeError("not a valid non-string sequence "
|
||||
"or mapping object").with_traceback(tb)
|
||||
"or mapping object") from err
|
||||
|
||||
l = []
|
||||
if not doseq:
|
||||
@@ -1125,15 +1191,15 @@ def splitnport(host, defport=-1):
|
||||
def _splitnport(host, defport=-1):
|
||||
"""Split host and port, returning numeric port.
|
||||
Return given default port if no ':' found; defaults to -1.
|
||||
Return numerical port if a valid number are found after ':'.
|
||||
Return numerical port if a valid number is found after ':'.
|
||||
Return None if ':' but not a valid number."""
|
||||
host, delim, port = host.rpartition(':')
|
||||
if not delim:
|
||||
host = port
|
||||
elif port:
|
||||
try:
|
||||
if port.isdigit() and port.isascii():
|
||||
nport = int(port)
|
||||
except ValueError:
|
||||
else:
|
||||
nport = None
|
||||
return host, nport
|
||||
return host, defport
|
||||
|
||||
Vendored
+131
-119
@@ -11,8 +11,8 @@ option. The OpenerDirector is a composite object that invokes the
|
||||
Handlers needed to open the requested URL. For example, the
|
||||
HTTPHandler performs HTTP GET and POST requests and deals with
|
||||
non-error returns. The HTTPRedirectHandler automatically deals with
|
||||
HTTP 301, 302, 303 and 307 redirect errors, and the HTTPDigestAuthHandler
|
||||
deals with digest authentication.
|
||||
HTTP 301, 302, 303, 307, and 308 redirect errors, and the
|
||||
HTTPDigestAuthHandler deals with digest authentication.
|
||||
|
||||
urlopen(url, data=None) -- Basic usage is the same as original
|
||||
urllib. pass the url and optionally data to post to an HTTP URL, and
|
||||
@@ -88,7 +88,6 @@ import hashlib
|
||||
import http.client
|
||||
import io
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import socket
|
||||
import string
|
||||
@@ -137,7 +136,7 @@ __version__ = '%d.%d' % sys.version_info[:2]
|
||||
|
||||
_opener = None
|
||||
def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
*, cafile=None, capath=None, cadefault=False, context=None):
|
||||
*, context=None):
|
||||
'''Open the URL url, which can be either a string or a Request object.
|
||||
|
||||
*data* must be an object specifying additional data to be sent to
|
||||
@@ -155,14 +154,6 @@ def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
If *context* is specified, it must be a ssl.SSLContext instance describing
|
||||
the various SSL options. See HTTPSConnection for more details.
|
||||
|
||||
The optional *cafile* and *capath* parameters specify a set of trusted CA
|
||||
certificates for HTTPS requests. cafile should point to a single file
|
||||
containing a bundle of CA certificates, whereas capath should point to a
|
||||
directory of hashed certificate files. More information can be found in
|
||||
ssl.SSLContext.load_verify_locations().
|
||||
|
||||
The *cadefault* parameter is ignored.
|
||||
|
||||
|
||||
This function always returns an object which can work as a
|
||||
context manager and has the properties url, headers, and status.
|
||||
@@ -188,25 +179,7 @@ def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||
|
||||
'''
|
||||
global _opener
|
||||
if cafile or capath or cadefault:
|
||||
import warnings
|
||||
warnings.warn("cafile, capath and cadefault are deprecated, use a "
|
||||
"custom context instead.", DeprecationWarning, 2)
|
||||
if context is not None:
|
||||
raise ValueError(
|
||||
"You can't pass both context and any of cafile, capath, and "
|
||||
"cadefault"
|
||||
)
|
||||
if not _have_ssl:
|
||||
raise ValueError('SSL support not available')
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
|
||||
cafile=cafile,
|
||||
capath=capath)
|
||||
# send ALPN extension to indicate HTTP/1.1 protocol
|
||||
context.set_alpn_protocols(['http/1.1'])
|
||||
https_handler = HTTPSHandler(context=context)
|
||||
opener = build_opener(https_handler)
|
||||
elif context:
|
||||
if context:
|
||||
https_handler = HTTPSHandler(context=context)
|
||||
opener = build_opener(https_handler)
|
||||
elif _opener is None:
|
||||
@@ -266,10 +239,7 @@ def urlretrieve(url, filename=None, reporthook=None, data=None):
|
||||
if reporthook:
|
||||
reporthook(blocknum, bs, size)
|
||||
|
||||
while True:
|
||||
block = fp.read(bs)
|
||||
if not block:
|
||||
break
|
||||
while block := fp.read(bs):
|
||||
read += len(block)
|
||||
tfp.write(block)
|
||||
blocknum += 1
|
||||
@@ -661,7 +631,7 @@ class HTTPRedirectHandler(BaseHandler):
|
||||
but another Handler might.
|
||||
"""
|
||||
m = req.get_method()
|
||||
if (not (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
|
||||
if (not (code in (301, 302, 303, 307, 308) and m in ("GET", "HEAD")
|
||||
or code in (301, 302, 303) and m == "POST")):
|
||||
raise HTTPError(req.full_url, code, msg, headers, fp)
|
||||
|
||||
@@ -680,6 +650,7 @@ class HTTPRedirectHandler(BaseHandler):
|
||||
newheaders = {k: v for k, v in req.headers.items()
|
||||
if k.lower() not in CONTENT_HEADERS}
|
||||
return Request(newurl,
|
||||
method="HEAD" if m == "HEAD" else "GET",
|
||||
headers=newheaders,
|
||||
origin_req_host=req.origin_req_host,
|
||||
unverifiable=True)
|
||||
@@ -748,7 +719,7 @@ class HTTPRedirectHandler(BaseHandler):
|
||||
|
||||
return self.parent.open(new, timeout=req.timeout)
|
||||
|
||||
http_error_301 = http_error_303 = http_error_307 = http_error_302
|
||||
http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302
|
||||
|
||||
inf_msg = "The HTTP server returned a redirect error that would " \
|
||||
"lead to an infinite loop.\n" \
|
||||
@@ -907,9 +878,9 @@ class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr):
|
||||
|
||||
class HTTPPasswordMgrWithPriorAuth(HTTPPasswordMgrWithDefaultRealm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self):
|
||||
self.authenticated = {}
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__()
|
||||
|
||||
def add_password(self, realm, uri, user, passwd, is_authenticated=False):
|
||||
self.update_authenticated(uri, is_authenticated)
|
||||
@@ -1255,8 +1226,8 @@ class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):
|
||||
|
||||
class AbstractHTTPHandler(BaseHandler):
|
||||
|
||||
def __init__(self, debuglevel=0):
|
||||
self._debuglevel = debuglevel
|
||||
def __init__(self, debuglevel=None):
|
||||
self._debuglevel = debuglevel if debuglevel is not None else http.client.HTTPConnection.debuglevel
|
||||
|
||||
def set_http_debuglevel(self, level):
|
||||
self._debuglevel = level
|
||||
@@ -1382,14 +1353,19 @@ if hasattr(http.client, 'HTTPSConnection'):
|
||||
|
||||
class HTTPSHandler(AbstractHTTPHandler):
|
||||
|
||||
def __init__(self, debuglevel=0, context=None, check_hostname=None):
|
||||
def __init__(self, debuglevel=None, context=None, check_hostname=None):
|
||||
debuglevel = debuglevel if debuglevel is not None else http.client.HTTPSConnection.debuglevel
|
||||
AbstractHTTPHandler.__init__(self, debuglevel)
|
||||
if context is None:
|
||||
http_version = http.client.HTTPSConnection._http_vsn
|
||||
context = http.client._create_https_context(http_version)
|
||||
if check_hostname is not None:
|
||||
context.check_hostname = check_hostname
|
||||
self._context = context
|
||||
self._check_hostname = check_hostname
|
||||
|
||||
def https_open(self, req):
|
||||
return self.do_open(http.client.HTTPSConnection, req,
|
||||
context=self._context, check_hostname=self._check_hostname)
|
||||
context=self._context)
|
||||
|
||||
https_request = AbstractHTTPHandler.do_request_
|
||||
|
||||
@@ -1561,6 +1537,7 @@ class FTPHandler(BaseHandler):
|
||||
dirs, file = dirs[:-1], dirs[-1]
|
||||
if dirs and not dirs[0]:
|
||||
dirs = dirs[1:]
|
||||
fw = None
|
||||
try:
|
||||
fw = self.connect_ftp(user, passwd, host, port, dirs, req.timeout)
|
||||
type = file and 'I' or 'D'
|
||||
@@ -1578,9 +1555,12 @@ class FTPHandler(BaseHandler):
|
||||
headers += "Content-length: %d\n" % retrlen
|
||||
headers = email.message_from_string(headers)
|
||||
return addinfourl(fp, headers, req.full_url)
|
||||
except ftplib.all_errors as exp:
|
||||
exc = URLError('ftp error: %r' % exp)
|
||||
raise exc.with_traceback(sys.exc_info()[2])
|
||||
except Exception as exp:
|
||||
if fw is not None and not fw.keepalive:
|
||||
fw.close()
|
||||
if isinstance(exp, ftplib.all_errors):
|
||||
raise URLError(exp) from exp
|
||||
raise
|
||||
|
||||
def connect_ftp(self, user, passwd, host, port, dirs, timeout):
|
||||
return ftpwrapper(user, passwd, host, port, dirs, timeout,
|
||||
@@ -1604,14 +1584,15 @@ class CacheFTPHandler(FTPHandler):
|
||||
|
||||
def connect_ftp(self, user, passwd, host, port, dirs, timeout):
|
||||
key = user, host, port, '/'.join(dirs), timeout
|
||||
if key in self.cache:
|
||||
self.timeout[key] = time.time() + self.delay
|
||||
else:
|
||||
self.cache[key] = ftpwrapper(user, passwd, host, port,
|
||||
dirs, timeout)
|
||||
self.timeout[key] = time.time() + self.delay
|
||||
conn = self.cache.get(key)
|
||||
if conn is None or not conn.keepalive:
|
||||
if conn is not None:
|
||||
conn.close()
|
||||
conn = self.cache[key] = ftpwrapper(user, passwd, host, port,
|
||||
dirs, timeout)
|
||||
self.timeout[key] = time.time() + self.delay
|
||||
self.check_cache()
|
||||
return self.cache[key]
|
||||
return conn
|
||||
|
||||
def check_cache(self):
|
||||
# first check for old ones
|
||||
@@ -1681,12 +1662,27 @@ else:
|
||||
def url2pathname(pathname):
|
||||
"""OS-specific conversion from a relative URL of the 'file' scheme
|
||||
to a file system path; not recommended for general use."""
|
||||
return unquote(pathname)
|
||||
if pathname[:3] == '///':
|
||||
# URL has an empty authority section, so the path begins on the
|
||||
# third character.
|
||||
pathname = pathname[2:]
|
||||
elif pathname[:12] == '//localhost/':
|
||||
# Skip past 'localhost' authority.
|
||||
pathname = pathname[11:]
|
||||
encoding = sys.getfilesystemencoding()
|
||||
errors = sys.getfilesystemencodeerrors()
|
||||
return unquote(pathname, encoding=encoding, errors=errors)
|
||||
|
||||
def pathname2url(pathname):
|
||||
"""OS-specific conversion from a file system path to a relative URL
|
||||
of the 'file' scheme; not recommended for general use."""
|
||||
return quote(pathname)
|
||||
if pathname[:2] == '//':
|
||||
# Add explicitly empty authority to avoid interpreting the path
|
||||
# as authority.
|
||||
pathname = '//' + pathname
|
||||
encoding = sys.getfilesystemencoding()
|
||||
errors = sys.getfilesystemencodeerrors()
|
||||
return quote(pathname, encoding=encoding, errors=errors)
|
||||
|
||||
|
||||
ftpcache = {}
|
||||
@@ -1791,7 +1787,7 @@ class URLopener:
|
||||
except (HTTPError, URLError):
|
||||
raise
|
||||
except OSError as msg:
|
||||
raise OSError('socket error', msg).with_traceback(sys.exc_info()[2])
|
||||
raise OSError('socket error', msg) from msg
|
||||
|
||||
def open_unknown(self, fullurl, data=None):
|
||||
"""Overridable interface to open unknown URL type."""
|
||||
@@ -1845,10 +1841,7 @@ class URLopener:
|
||||
size = int(headers["Content-Length"])
|
||||
if reporthook:
|
||||
reporthook(blocknum, bs, size)
|
||||
while 1:
|
||||
block = fp.read(bs)
|
||||
if not block:
|
||||
break
|
||||
while block := fp.read(bs):
|
||||
read += len(block)
|
||||
tfp.write(block)
|
||||
blocknum += 1
|
||||
@@ -1988,9 +1981,17 @@ class URLopener:
|
||||
|
||||
if _have_ssl:
|
||||
def _https_connection(self, host):
|
||||
return http.client.HTTPSConnection(host,
|
||||
key_file=self.key_file,
|
||||
cert_file=self.cert_file)
|
||||
if self.key_file or self.cert_file:
|
||||
http_version = http.client.HTTPSConnection._http_vsn
|
||||
context = http.client._create_https_context(http_version)
|
||||
context.load_cert_chain(self.cert_file, self.key_file)
|
||||
# cert and key file means the user wants to authenticate.
|
||||
# enable TLS 1.3 PHA implicitly even for custom contexts.
|
||||
if context.post_handshake_auth is not None:
|
||||
context.post_handshake_auth = True
|
||||
else:
|
||||
context = None
|
||||
return http.client.HTTPSConnection(host, context=context)
|
||||
|
||||
def open_https(self, url, data=None):
|
||||
"""Use HTTPS protocol."""
|
||||
@@ -2093,7 +2094,7 @@ class URLopener:
|
||||
headers = email.message_from_string(headers)
|
||||
return addinfourl(fp, headers, "ftp:" + url)
|
||||
except ftperrors() as exp:
|
||||
raise URLError('ftp error %r' % exp).with_traceback(sys.exc_info()[2])
|
||||
raise URLError(f'ftp error: {exp}') from exp
|
||||
|
||||
def open_data(self, url, data=None):
|
||||
"""Use "data" URL."""
|
||||
@@ -2211,6 +2212,13 @@ class FancyURLopener(URLopener):
|
||||
else:
|
||||
return self.http_error_default(url, fp, errcode, errmsg, headers)
|
||||
|
||||
def http_error_308(self, url, fp, errcode, errmsg, headers, data=None):
|
||||
"""Error 308 -- relocated, but turn POST into error."""
|
||||
if data is None:
|
||||
return self.http_error_301(url, fp, errcode, errmsg, headers, data)
|
||||
else:
|
||||
return self.http_error_default(url, fp, errcode, errmsg, headers)
|
||||
|
||||
def http_error_401(self, url, fp, errcode, errmsg, headers, data=None,
|
||||
retry=False):
|
||||
"""Error 401 -- authentication required.
|
||||
@@ -2436,8 +2444,7 @@ class ftpwrapper:
|
||||
conn, retrlen = self.ftp.ntransfercmd(cmd)
|
||||
except ftplib.error_perm as reason:
|
||||
if str(reason)[:3] != '550':
|
||||
raise URLError('ftp error: %r' % reason).with_traceback(
|
||||
sys.exc_info()[2])
|
||||
raise URLError(f'ftp error: {reason}') from reason
|
||||
if not conn:
|
||||
# Set transfer mode to ASCII!
|
||||
self.ftp.voidcmd('TYPE A')
|
||||
@@ -2464,7 +2471,13 @@ class ftpwrapper:
|
||||
return (ftpobj, retrlen)
|
||||
|
||||
def endtransfer(self):
|
||||
if not self.busy:
|
||||
return
|
||||
self.busy = 0
|
||||
try:
|
||||
self.ftp.voidresp()
|
||||
except ftperrors():
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.keepalive = False
|
||||
@@ -2492,28 +2505,34 @@ def getproxies_environment():
|
||||
this seems to be the standard convention. If you need a
|
||||
different way, you can pass a proxies dictionary to the
|
||||
[Fancy]URLopener constructor.
|
||||
|
||||
"""
|
||||
proxies = {}
|
||||
# in order to prefer lowercase variables, process environment in
|
||||
# two passes: first matches any, second pass matches lowercase only
|
||||
for name, value in os.environ.items():
|
||||
name = name.lower()
|
||||
if value and name[-6:] == '_proxy':
|
||||
proxies[name[:-6]] = value
|
||||
|
||||
# select only environment variables which end in (after making lowercase) _proxy
|
||||
proxies = {}
|
||||
environment = []
|
||||
for name in os.environ:
|
||||
# fast screen underscore position before more expensive case-folding
|
||||
if len(name) > 5 and name[-6] == "_" and name[-5:].lower() == "proxy":
|
||||
value = os.environ[name]
|
||||
proxy_name = name[:-6].lower()
|
||||
environment.append((name, value, proxy_name))
|
||||
if value:
|
||||
proxies[proxy_name] = value
|
||||
# CVE-2016-1000110 - If we are running as CGI script, forget HTTP_PROXY
|
||||
# (non-all-lowercase) as it may be set from the web server by a "Proxy:"
|
||||
# header from the client
|
||||
# If "proxy" is lowercase, it will still be used thanks to the next block
|
||||
if 'REQUEST_METHOD' in os.environ:
|
||||
proxies.pop('http', None)
|
||||
for name, value in os.environ.items():
|
||||
for name, value, proxy_name in environment:
|
||||
# not case-folded, checking here for lower-case env vars only
|
||||
if name[-6:] == '_proxy':
|
||||
name = name.lower()
|
||||
if value:
|
||||
proxies[name[:-6]] = value
|
||||
proxies[proxy_name] = value
|
||||
else:
|
||||
proxies.pop(name[:-6], None)
|
||||
proxies.pop(proxy_name, None)
|
||||
return proxies
|
||||
|
||||
def proxy_bypass_environment(host, proxies=None):
|
||||
@@ -2566,6 +2585,7 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings):
|
||||
}
|
||||
"""
|
||||
from fnmatch import fnmatch
|
||||
from ipaddress import AddressValueError, IPv4Address
|
||||
|
||||
hostonly, port = _splitport(host)
|
||||
|
||||
@@ -2582,20 +2602,17 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings):
|
||||
return True
|
||||
|
||||
hostIP = None
|
||||
try:
|
||||
hostIP = int(IPv4Address(hostonly))
|
||||
except AddressValueError:
|
||||
pass
|
||||
|
||||
for value in proxy_settings.get('exceptions', ()):
|
||||
# Items in the list are strings like these: *.local, 169.254/16
|
||||
if not value: continue
|
||||
|
||||
m = re.match(r"(\d+(?:\.\d+)*)(/\d+)?", value)
|
||||
if m is not None:
|
||||
if hostIP is None:
|
||||
try:
|
||||
hostIP = socket.gethostbyname(hostonly)
|
||||
hostIP = ip2num(hostIP)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if m is not None and hostIP is not None:
|
||||
base = ip2num(m.group(1))
|
||||
mask = m.group(2)
|
||||
if mask is None:
|
||||
@@ -2618,6 +2635,31 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings):
|
||||
return False
|
||||
|
||||
|
||||
# Same as _proxy_bypass_macosx_sysconf, testable on all platforms
|
||||
def _proxy_bypass_winreg_override(host, override):
|
||||
"""Return True if the host should bypass the proxy server.
|
||||
|
||||
The proxy override list is obtained from the Windows
|
||||
Internet settings proxy override registry value.
|
||||
|
||||
An example of a proxy override value is:
|
||||
"www.example.com;*.example.net; 192.168.0.1"
|
||||
"""
|
||||
from fnmatch import fnmatch
|
||||
|
||||
host, _ = _splitport(host)
|
||||
proxy_override = override.split(';')
|
||||
for test in proxy_override:
|
||||
test = test.strip()
|
||||
# "<local>" should bypass the proxy server for all intranet addresses
|
||||
if test == '<local>':
|
||||
if '.' not in host:
|
||||
return True
|
||||
elif fnmatch(host, test):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
from _scproxy import _get_proxy_settings, _get_proxies
|
||||
|
||||
@@ -2716,7 +2758,7 @@ elif os.name == 'nt':
|
||||
import winreg
|
||||
except ImportError:
|
||||
# Std modules, so should be around - but you never know!
|
||||
return 0
|
||||
return False
|
||||
try:
|
||||
internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
r'Software\Microsoft\Windows\CurrentVersion\Internet Settings')
|
||||
@@ -2726,40 +2768,10 @@ elif os.name == 'nt':
|
||||
'ProxyOverride')[0])
|
||||
# ^^^^ Returned as Unicode but problems if not converted to ASCII
|
||||
except OSError:
|
||||
return 0
|
||||
return False
|
||||
if not proxyEnable or not proxyOverride:
|
||||
return 0
|
||||
# try to make a host list from name and IP address.
|
||||
rawHost, port = _splitport(host)
|
||||
host = [rawHost]
|
||||
try:
|
||||
addr = socket.gethostbyname(rawHost)
|
||||
if addr != rawHost:
|
||||
host.append(addr)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
fqdn = socket.getfqdn(rawHost)
|
||||
if fqdn != rawHost:
|
||||
host.append(fqdn)
|
||||
except OSError:
|
||||
pass
|
||||
# make a check value list from the registry entry: replace the
|
||||
# '<local>' string by the localhost entry and the corresponding
|
||||
# canonical entry.
|
||||
proxyOverride = proxyOverride.split(';')
|
||||
# now check if we match one of the registry values.
|
||||
for test in proxyOverride:
|
||||
if test == '<local>':
|
||||
if '.' not in rawHost:
|
||||
return 1
|
||||
test = test.replace(".", r"\.") # mask dots
|
||||
test = test.replace("*", r".*") # change glob sequence
|
||||
test = test.replace("?", r".") # change glob char
|
||||
for val in host:
|
||||
if re.match(test, val, re.I):
|
||||
return 1
|
||||
return 0
|
||||
return False
|
||||
return _proxy_bypass_winreg_override(host, proxyOverride)
|
||||
|
||||
def proxy_bypass(host):
|
||||
"""Return True, if host should be bypassed.
|
||||
|
||||
Vendored
+24
-11
@@ -11,6 +11,8 @@
|
||||
"""
|
||||
|
||||
import collections
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
@@ -19,6 +21,19 @@ __all__ = ["RobotFileParser"]
|
||||
RequestRate = collections.namedtuple("RequestRate", "requests seconds")
|
||||
|
||||
|
||||
def normalize(path):
|
||||
unquoted = urllib.parse.unquote(path, errors='surrogateescape')
|
||||
return urllib.parse.quote(unquoted, errors='surrogateescape')
|
||||
|
||||
def normalize_path(path):
|
||||
path, sep, query = path.partition('?')
|
||||
path = normalize(path)
|
||||
if sep:
|
||||
query = re.sub(r'[^=&]+', lambda m: normalize(m[0]), query)
|
||||
path += '?' + query
|
||||
return path
|
||||
|
||||
|
||||
class RobotFileParser:
|
||||
""" This class provides a set of methods to read, parse and answer
|
||||
questions about a single robots.txt file.
|
||||
@@ -54,7 +69,7 @@ class RobotFileParser:
|
||||
def set_url(self, url):
|
||||
"""Sets the URL referring to a robots.txt file."""
|
||||
self.url = url
|
||||
self.host, self.path = urllib.parse.urlparse(url)[1:3]
|
||||
self.host, self.path = urllib.parse.urlsplit(url)[1:3]
|
||||
|
||||
def read(self):
|
||||
"""Reads the robots.txt URL and feeds it to the parser."""
|
||||
@@ -65,9 +80,10 @@ class RobotFileParser:
|
||||
self.disallow_all = True
|
||||
elif err.code >= 400 and err.code < 500:
|
||||
self.allow_all = True
|
||||
err.close()
|
||||
else:
|
||||
raw = f.read()
|
||||
self.parse(raw.decode("utf-8").splitlines())
|
||||
self.parse(raw.decode("utf-8", "surrogateescape").splitlines())
|
||||
|
||||
def _add_entry(self, entry):
|
||||
if "*" in entry.useragents:
|
||||
@@ -111,7 +127,7 @@ class RobotFileParser:
|
||||
line = line.split(':', 1)
|
||||
if len(line) == 2:
|
||||
line[0] = line[0].strip().lower()
|
||||
line[1] = urllib.parse.unquote(line[1].strip())
|
||||
line[1] = line[1].strip()
|
||||
if line[0] == "user-agent":
|
||||
if state == 2:
|
||||
self._add_entry(entry)
|
||||
@@ -165,10 +181,9 @@ class RobotFileParser:
|
||||
return False
|
||||
# search for given user agent matches
|
||||
# the first match counts
|
||||
parsed_url = urllib.parse.urlparse(urllib.parse.unquote(url))
|
||||
url = urllib.parse.urlunparse(('','',parsed_url.path,
|
||||
parsed_url.params,parsed_url.query, parsed_url.fragment))
|
||||
url = urllib.parse.quote(url)
|
||||
parsed_url = urllib.parse.urlsplit(url)
|
||||
url = urllib.parse.urlunsplit(('', '', *parsed_url[2:]))
|
||||
url = normalize_path(url)
|
||||
if not url:
|
||||
url = "/"
|
||||
for entry in self.entries:
|
||||
@@ -211,7 +226,6 @@ class RobotFileParser:
|
||||
entries = entries + [self.default_entry]
|
||||
return '\n\n'.join(map(str, entries))
|
||||
|
||||
|
||||
class RuleLine:
|
||||
"""A rule line is a single "Allow:" (allowance==True) or "Disallow:"
|
||||
(allowance==False) followed by a path."""
|
||||
@@ -219,8 +233,7 @@ class RuleLine:
|
||||
if path == '' and not allowance:
|
||||
# an empty value means allow all
|
||||
allowance = True
|
||||
path = urllib.parse.urlunparse(urllib.parse.urlparse(path))
|
||||
self.path = urllib.parse.quote(path)
|
||||
self.path = normalize_path(path)
|
||||
self.allowance = allowance
|
||||
|
||||
def applies_to(self, filename):
|
||||
@@ -266,7 +279,7 @@ class Entry:
|
||||
def allowance(self, filename):
|
||||
"""Preconditions:
|
||||
- our agent applies to this entry
|
||||
- filename is URL decoded"""
|
||||
- filename is URL encoded"""
|
||||
for line in self.rulelines:
|
||||
if line.applies_to(filename):
|
||||
return line.allowance
|
||||
|
||||
Reference in new issue
Block a user