Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions Lib/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ def is_server_error(self):
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'
PROCESSING = 102, 'Processing', 'Server is processing the request'
EARLY_HINTS = (103, 'Early Hints',
'Headers sent to prepare for the response')

# success
OK = 200, 'OK', 'Request fulfilled, document follows'
Expand All @@ -67,9 +68,11 @@ def is_server_error(self):
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'
MULTI_STATUS = (207, 'Multi-Status',
'Response contains multiple statuses in the body')
ALREADY_REPORTED = (208, 'Already Reported',
'Operation has already been reported')
IM_USED = 226, 'IM Used', 'Request completed using instance manipulations'

# redirection
MULTIPLE_CHOICES = (300, 'Multiple Choices',
Expand Down Expand Up @@ -128,15 +131,19 @@ def is_server_error(self):
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.')
'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_CONTENT = (422, 'Unprocessable Content',
'Server is not able to process the contained instructions')
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
LOCKED = 423, 'Locked'
FAILED_DEPENDENCY = 424, 'Failed Dependency'
TOO_EARLY = 425, 'Too Early'
UPGRADE_REQUIRED = 426, 'Upgrade Required'
LOCKED = 423, 'Locked', 'Resource of a method is locked'
FAILED_DEPENDENCY = (424, 'Failed Dependency',
'Dependent action of the request failed')
TOO_EARLY = (425, 'Too Early',
'Server refuses to process a request that might be replayed')
UPGRADE_REQUIRED = (426, 'Upgrade Required',
'Server refuses to perform the request using the current protocol')
PRECONDITION_REQUIRED = (428, 'Precondition Required',
'The origin server requires the request to be conditional')
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
Expand Down Expand Up @@ -164,10 +171,14 @@ def is_server_error(self):
'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'
VARIANT_ALSO_NEGOTIATES = (506, 'Variant Also Negotiates',
'Server has an internal configuration error')
INSUFFICIENT_STORAGE = (507, 'Insufficient Storage',
'Server is not able to store the representation')
LOOP_DETECTED = (508, 'Loop Detected',
'Server encountered an infinite loop while processing a request')
NOT_EXTENDED = (510, 'Not Extended',
'Request does not meet the resource access policy')
NETWORK_AUTHENTICATION_REQUIRED = (511,
'Network Authentication Required',
'The client needs to authenticate to gain network access')
Expand Down
10 changes: 5 additions & 5 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ def close(self):
response.close()

def send(self, data):
"""Send `data' to the server.
"""Send 'data' to the server.
``data`` can be a string object, a bytes object, an array object, a
file-like object that supports a .read() method, or an iterable object.
"""
Expand Down Expand Up @@ -1159,10 +1159,10 @@ def putrequest(self, method, url, skip_host=False,
skip_accept_encoding=False):
"""Send a request to the server.

`method' specifies an HTTP request method, e.g. 'GET'.
`url' specifies the object being requested, e.g. '/index.html'.
`skip_host' if True does not add automatically a 'Host:' header
`skip_accept_encoding' if True does not add automatically an
'method' specifies an HTTP request method, e.g. 'GET'.
'url' specifies the object being requested, e.g. '/index.html'.
'skip_host' if True does not add automatically a 'Host:' header
'skip_accept_encoding' if True does not add automatically an
'Accept-Encoding:' header
"""

Expand Down
2 changes: 1 addition & 1 deletion Lib/http/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1987,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. curl and lynx use 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
Expand Down
33 changes: 27 additions & 6 deletions Lib/http/cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@
such trickeries do not confuse it.

>>> C = cookies.SimpleCookie()
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"

Each element of the Cookie also supports all of the RFC 2109
Cookie attributes. Here's an example which sets the Path
Expand Down Expand Up @@ -170,6 +170,15 @@ class CookieError(Exception):
})

_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
_control_character_re = re.compile(r'[\x00-\x1F\x7F]')


def _has_control_character(*val):
"""Detects control characters within a value.
Supports any type, as header values can be any type.
"""
return any(_control_character_re.search(str(v)) for v in val)


def _quote(str):
r"""Quote a string for use in a cookie header.
Expand Down Expand Up @@ -264,17 +273,19 @@ class Morsel(dict):
"httponly" : "HttpOnly",
"version" : "Version",
"samesite" : "SameSite",
"partitioned": "Partitioned",
}

_flags = {'secure', 'httponly'}
_reserved_defaults = dict.fromkeys(_reserved, "")

_flags = {'secure', 'httponly', 'partitioned'}

def __init__(self):
# Set defaults
self._key = self._value = self._coded_value = None

# Set default attributes
for key in self._reserved:
dict.__setitem__(self, key, "")
dict.update(self, self._reserved_defaults)

@property
def key(self):
Expand All @@ -292,12 +303,16 @@ def __setitem__(self, K, V):
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid attribute %r" % (K,))
if _has_control_character(K, V):
raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
dict.__setitem__(self, K, V)

def setdefault(self, key, val=None):
key = key.lower()
if key not in self._reserved:
raise CookieError("Invalid attribute %r" % (key,))
if _has_control_character(key, val):
raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
return dict.setdefault(self, key, val)

def __eq__(self, morsel):
Expand Down Expand Up @@ -333,6 +348,9 @@ def set(self, key, val, coded_val):
raise CookieError('Attempt to set a reserved key %r' % (key,))
if not _is_legal_key(key):
raise CookieError('Illegal key %r' % (key,))
if _has_control_character(key, val, coded_val):
raise CookieError(
"Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))

# It's a good key, so save it.
self._key = key
Expand Down Expand Up @@ -486,7 +504,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
result = []
items = sorted(self.items())
for key, value in items:
result.append(value.output(attrs, header))
value_output = value.output(attrs, header)
if _has_control_character(value_output):
raise CookieError("Control characters are not allowed in cookies")
result.append(value_output)
return sep.join(result)

__str__ = output
Expand Down
112 changes: 101 additions & 11 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@
__version__ = "0.6"

__all__ = [
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
"HTTPServer", "ThreadingHTTPServer",
"HTTPSServer", "ThreadingHTTPSServer",
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
"CGIHTTPRequestHandler",
]

import copy
Expand All @@ -99,7 +101,7 @@
import posixpath
import select
import shutil
import socket # For gethostbyaddr()
import socket
import socketserver
import sys
import time
Expand All @@ -114,6 +116,11 @@
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
:root {
color-scheme: light dark;
}
</style>
<title>Error response</title>
</head>
<body>
Expand All @@ -133,7 +140,8 @@

class HTTPServer(socketserver.TCPServer):

allow_reuse_address = 1 # Seems to make sense in testing environment
allow_reuse_address = True # Seems to make sense in testing environment
allow_reuse_port = False

def server_bind(self):
"""Override server_bind to store the server name."""
Expand All @@ -147,6 +155,47 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True


class HTTPSServer(HTTPServer):
def __init__(self, server_address, RequestHandlerClass,
bind_and_activate=True, *, certfile, keyfile=None,
password=None, alpn_protocols=None):
try:
import ssl
except ImportError:
raise RuntimeError("SSL module is missing; "
"HTTPS support is unavailable")

self.ssl = ssl
self.certfile = certfile
self.keyfile = keyfile
self.password = password
# Support by default HTTP/1.1
self.alpn_protocols = (
["http/1.1"] if alpn_protocols is None else alpn_protocols
)

super().__init__(server_address,
RequestHandlerClass,
bind_and_activate)

def server_activate(self):
"""Wrap the socket in SSLSocket."""
super().server_activate()
context = self._create_context()
self.socket = context.wrap_socket(self.socket, server_side=True)

def _create_context(self):
"""Create a secure SSL context."""
context = self.ssl.create_default_context(self.ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(self.certfile, self.keyfile, self.password)
context.set_alpn_protocols(self.alpn_protocols)
return context


class ThreadingHTTPSServer(socketserver.ThreadingMixIn, HTTPSServer):
daemon_threads = True


class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):

"""HTTP request handler base class.
Expand Down Expand Up @@ -817,6 +866,7 @@ def list_directory(self, path):
r.append('<html lang="en">')
r.append('<head>')
r.append(f'<meta charset="{enc}">')
r.append('<style type="text/css">\n:root {\ncolor-scheme: light dark;\n}\n</style>')
r.append(f'<title>{title}</title>\n</head>')
r.append(f'<body>\n<h1>{title}</h1>')
r.append('<hr>\n<ul>')
Expand Down Expand Up @@ -1281,20 +1331,29 @@ def _get_best_family(*address):

def test(HandlerClass=BaseHTTPRequestHandler,
ServerClass=ThreadingHTTPServer,
protocol="HTTP/1.0", port=8000, bind=None):
protocol="HTTP/1.0", port=8000, bind=None,
tls_cert=None, tls_key=None, tls_password=None):
"""Test the HTTP request handler class.

This runs an HTTP server on port 8000 (or the port argument).

"""
ServerClass.address_family, addr = _get_best_family(bind, port)
HandlerClass.protocol_version = protocol
with ServerClass(addr, HandlerClass) as httpd:

if tls_cert:
server = ServerClass(addr, HandlerClass, certfile=tls_cert,
keyfile=tls_key, password=tls_password)
else:
server = ServerClass(addr, HandlerClass)

with server as httpd:
host, port = httpd.socket.getsockname()[:2]
url_host = f'[{host}]' if ':' in host else host
protocol = 'HTTPS' if tls_cert else 'HTTP'
print(
f"Serving HTTP on {host} port {port} "
f"(http://{url_host}:{port}/) ..."
f"Serving {protocol} on {host} port {port} "
f"({protocol.lower()}://{url_host}:{port}/) ..."
)
try:
httpd.serve_forever()
Expand All @@ -1306,7 +1365,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
import argparse
import contextlib

parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(color=True)
parser.add_argument('--cgi', action='store_true',
help='run as CGI server')
parser.add_argument('-b', '--bind', metavar='ADDRESS',
Expand All @@ -1319,17 +1378,38 @@ def test(HandlerClass=BaseHTTPRequestHandler,
default='HTTP/1.0',
help='conform to this HTTP version '
'(default: %(default)s)')
parser.add_argument('--tls-cert', metavar='PATH',
help='path to the TLS certificate chain file')
parser.add_argument('--tls-key', metavar='PATH',
help='path to the TLS key file')
parser.add_argument('--tls-password-file', metavar='PATH',
help='path to the password file for the TLS key')
parser.add_argument('port', default=8000, type=int, nargs='?',
help='bind to this port '
'(default: %(default)s)')
args = parser.parse_args()

if not args.tls_cert and args.tls_key:
parser.error("--tls-key requires --tls-cert to be set")

tls_key_password = None
if args.tls_password_file:
if not args.tls_cert:
parser.error("--tls-password-file requires --tls-cert to be set")

try:
with open(args.tls_password_file, "r", encoding="utf-8") as f:
tls_key_password = f.read().strip()
except OSError as e:
parser.error(f"Failed to read TLS password file: {e}")

if args.cgi:
handler_class = CGIHTTPRequestHandler
else:
handler_class = SimpleHTTPRequestHandler

# ensure dual-stack is not disabled; ref #38907
class DualStackServer(ThreadingHTTPServer):
class DualStackServerMixin:

def server_bind(self):
# suppress exception when protocol is IPv4
Expand All @@ -1342,10 +1422,20 @@ def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self,
directory=args.directory)

class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer):
pass
class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer):
pass

ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer

test(
HandlerClass=handler_class,
ServerClass=DualStackServer,
ServerClass=ServerClass,
port=args.port,
bind=args.bind,
protocol=args.protocol,
tls_cert=args.tls_cert,
tls_key=args.tls_key,
tls_password=tls_key_password,
)
Loading
Loading