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:
Terry Tianlin Luan authored and GitHub committed 2026-01-16 23:03:49 +09:00
1 parent ef871d227e
commit 4cb3b9d8f0
20 files changed
+1931 -785

No files matched your search

+63 -11
View File
@@ -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.'
+133 -88
View File
@@ -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."
+29 -35
View File
@@ -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:
+94 -38
View File
@@ -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,
)
+214 -120
View File
@@ -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)
+545 -70
View File
@@ -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)
+358 -90
View File
@@ -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#', '&amp;', '&amp', '<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#', '&amp;', '&amp', '<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)
+1
View File
@@ -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):
+4
View File
@@ -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 = """\
+2 -1
View File
@@ -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
-2
View File
@@ -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')
+136 -49
View File
@@ -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 = [
+35 -57
View File
@@ -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)
-1
View File
@@ -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
+7
View File
@@ -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()
-1
View File
@@ -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],
+4 -7
View File
@@ -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)
+151 -85
View File
@@ -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
+131 -119
View File
@@ -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.
+24 -11
View File
@@ -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