gh-133810: remove http.server.CGIHTTPRequestHandler and --cgi flag (#133811)

The CGI HTTP request handler has been deprecated since Python 3.13.
This commit is contained in:
Bénédikt Tran 2025-05-17 09:58:16 +02:00 committed by GitHub
parent 2f1ecb3bc4
commit faac627e47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 28 additions and 755 deletions

View File

@ -20,7 +20,7 @@ Pending removal in Python 3.15
* :mod:`http.server`:
* The obsolete and rarely used :class:`~http.server.CGIHTTPRequestHandler`
* The obsolete and rarely used :class:`!CGIHTTPRequestHandler`
has been deprecated since Python 3.13.
No direct replacement exists.
*Anything* is better than CGI to interface

View File

@ -458,55 +458,6 @@ such as using different index file names by overriding the class attribute
:attr:`index_pages`.
.. class:: CGIHTTPRequestHandler(request, client_address, server)
This class is used to serve either files or output of CGI scripts from the
current directory and below. Note that mapping HTTP hierarchic structure to
local directory structure is exactly as in :class:`SimpleHTTPRequestHandler`.
.. note::
CGI scripts run by the :class:`CGIHTTPRequestHandler` class cannot execute
redirects (HTTP code 302), because code 200 (script output follows) is
sent prior to execution of the CGI script. This pre-empts the status
code.
The class will however, run the CGI script, instead of serving it as a file,
if it guesses it to be a CGI script. Only directory-based CGI are used ---
the other common server configuration is to treat special extensions as
denoting CGI scripts.
The :func:`do_GET` and :func:`do_HEAD` functions are modified to run CGI scripts
and serve the output, instead of serving files, if the request leads to
somewhere below the ``cgi_directories`` path.
The :class:`CGIHTTPRequestHandler` defines the following data member:
.. attribute:: cgi_directories
This defaults to ``['/cgi-bin', '/htbin']`` and describes directories to
treat as containing CGI scripts.
The :class:`CGIHTTPRequestHandler` defines the following method:
.. method:: do_POST()
This method serves the ``'POST'`` request type, only allowed for CGI
scripts. Error 501, "Can only POST to CGI scripts", is output when trying
to POST to a non-CGI url.
Note that CGI scripts will be run with UID of user nobody, for security
reasons. Problems with the CGI script will be translated to error 403.
.. deprecated-removed:: 3.13 3.15
:class:`CGIHTTPRequestHandler` is being removed in 3.15. CGI has not
been considered a good way to do things for well over a decade. This code
has been unmaintained for a while now and sees very little practical use.
Retaining it could lead to further :ref:`security considerations
<http.server-security>`.
.. _http-server-cli:
Command-line interface
@ -563,24 +514,6 @@ The following options are accepted:
.. versionadded:: 3.11
.. option:: --cgi
:class:`CGIHTTPRequestHandler` can be enabled in the command line by passing
the ``--cgi`` option::
python -m http.server --cgi
.. deprecated-removed:: 3.13 3.15
:mod:`http.server` command line ``--cgi`` support is being removed
because :class:`CGIHTTPRequestHandler` is being removed.
.. warning::
:class:`CGIHTTPRequestHandler` and the ``--cgi`` command-line option
are not intended for use by untrusted clients and may be vulnerable
to exploitation. Always use within a secure environment.
.. option:: --tls-cert
Specifies a TLS certificate chain for HTTPS connections::

View File

@ -1871,7 +1871,7 @@ New Deprecations
* :mod:`http.server`:
* Deprecate :class:`~http.server.CGIHTTPRequestHandler`,
* Deprecate :class:`!CGIHTTPRequestHandler`,
to be removed in Python 3.15.
Process-based CGI HTTP servers have been out of favor for a very long time.
This code was outdated, unmaintained, and rarely used.

View File

@ -121,6 +121,15 @@ Deprecated
Removed
=======
http.server
-----------
* Removed the :class:`!CGIHTTPRequestHandler` class
and the ``--cgi`` flag from the :program:`python -m http.server`
command-line interface. They were deprecated in Python 3.13.
(Contributed by Bénédikt Tran in :gh:`133810`.)
platform
--------

View File

@ -175,7 +175,6 @@ IMPORT_MAPPING.update({
'SimpleDialog': 'tkinter.simpledialog',
'DocXMLRPCServer': 'xmlrpc.server',
'SimpleHTTPServer': 'http.server',
'CGIHTTPServer': 'http.server',
# For compatibility with broken pickles saved in old Python 3 versions
'UserDict': 'collections',
'UserList': 'collections',
@ -217,8 +216,6 @@ REVERSE_NAME_MAPPING.update({
('DocXMLRPCServer', 'DocCGIXMLRPCRequestHandler'),
('http.server', 'SimpleHTTPRequestHandler'):
('SimpleHTTPServer', 'SimpleHTTPRequestHandler'),
('http.server', 'CGIHTTPRequestHandler'):
('CGIHTTPServer', 'CGIHTTPRequestHandler'),
('_socket', 'socket'): ('socket', '_socketobject'),
})

View File

@ -181,11 +181,10 @@ def _strip_ipv6_iface(enc_name: bytes) -> bytes:
return enc_name
class HTTPMessage(email.message.Message):
# XXX The only usage of this method is in
# http.server.CGIHTTPRequestHandler. Maybe move the code there so
# that it doesn't need to be part of the public API. The API has
# never been defined so this could cause backwards compatibility
# issues.
# The getallmatchingheaders() method was only used by the CGI handler
# that was removed in Python 3.15. However, since the public API was not
# properly defined, it will be kept for backwards compatibility reasons.
def getallmatchingheaders(self, name):
"""Find all header lines matching a given header name.

View File

@ -1,29 +1,10 @@
"""HTTP server classes.
Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST,
and (deprecated) CGIHTTPRequestHandler for CGI scripts.
SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST.
It does, however, optionally implement HTTP/1.1 persistent connections.
Notes on CGIHTTPRequestHandler
------------------------------
This class is deprecated. It implements GET and POST requests to cgi-bin scripts.
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.
SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
-- it may execute arbitrary Python code or external programs.
Note that status code 200 is sent prior to execution of a CGI script, so
scripts cannot send other status codes such as 302 (redirect).
XXX To do:
- log requests even later (to capture byte count)
@ -86,10 +67,8 @@ __all__ = [
"HTTPServer", "ThreadingHTTPServer",
"HTTPSServer", "ThreadingHTTPSServer",
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
"CGIHTTPRequestHandler",
]
import copy
import datetime
import email.utils
import html
@ -99,7 +78,6 @@ import itertools
import mimetypes
import os
import posixpath
import select
import shutil
import socket
import socketserver
@ -953,56 +931,6 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
return 'application/octet-stream'
# Utilities for CGIHTTPRequestHandler
def _url_collapse_path(path):
"""
Given a URL path, remove extra '/'s and '.' path elements and collapse
any '..' references and returns a collapsed path.
Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
The utility of this function is limited to is_cgi method and helps
preventing some security attacks.
Returns: The reconstituted URL, which will always start with a '/'.
Raises: IndexError if too many '..' occur within the path.
"""
# Query component should not be involved.
path, _, query = path.partition('?')
path = urllib.parse.unquote(path)
# Similar to os.path.split(os.path.normpath(path)) but specific to URL
# path semantics rather than local operating system semantics.
path_parts = path.split('/')
head_parts = []
for part in path_parts[:-1]:
if part == '..':
head_parts.pop() # IndexError if more '..' than prior parts
elif part and part != '.':
head_parts.append( part )
if path_parts:
tail_part = path_parts.pop()
if tail_part:
if tail_part == '..':
head_parts.pop()
tail_part = ''
elif tail_part == '.':
tail_part = ''
else:
tail_part = ''
if query:
tail_part = '?'.join((tail_part, query))
splitpath = ('/' + '/'.join(head_parts), tail_part)
collapsed_path = "/".join(splitpath)
return collapsed_path
nobody = None
def nobody_uid():
@ -1026,274 +954,6 @@ def executable(path):
return os.access(path, os.X_OK)
class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
"""Complete HTTP server with GET, HEAD and POST commands.
GET and HEAD also support running CGI scripts.
The POST command is *only* implemented for CGI scripts.
"""
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')
# Make rfile unbuffered -- we need to read one line and then pass
# the rest to a subprocess, so we can't use buffered input.
rbufsize = 0
def do_POST(self):
"""Serve a POST request.
This is only implemented for CGI scripts.
"""
if self.is_cgi():
self.run_cgi()
else:
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Can only POST to CGI scripts")
def send_head(self):
"""Version of send_head that support CGI scripts"""
if self.is_cgi():
return self.run_cgi()
else:
return SimpleHTTPRequestHandler.send_head(self)
def is_cgi(self):
"""Test whether self.path corresponds to a CGI script.
Returns True and updates the cgi_info attribute to the tuple
(dir, rest) if self.path requires running a CGI script.
Returns False otherwise.
If any exception is raised, the caller should assume that
self.path was rejected as invalid and act accordingly.
The default implementation tests whether the normalized url
path begins with one of the strings in self.cgi_directories
(and the next character is a '/' or the end of the string).
"""
collapsed_path = _url_collapse_path(self.path)
dir_sep = collapsed_path.find('/', 1)
while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories:
dir_sep = collapsed_path.find('/', dir_sep+1)
if dir_sep > 0:
head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
self.cgi_info = head, tail
return True
return False
cgi_directories = ['/cgi-bin', '/htbin']
def is_executable(self, path):
"""Test whether argument path is an executable file."""
return executable(path)
def is_python(self, path):
"""Test whether argument path is a Python script."""
head, tail = os.path.splitext(path)
return tail.lower() in (".py", ".pyw")
def run_cgi(self):
"""Execute a CGI script."""
dir, rest = self.cgi_info
path = dir + '/' + rest
i = path.find('/', len(dir)+1)
while i >= 0:
nextdir = path[:i]
nextrest = path[i+1:]
scriptdir = self.translate_path(nextdir)
if os.path.isdir(scriptdir):
dir, rest = nextdir, nextrest
i = path.find('/', len(dir)+1)
else:
break
# find an explicit query string, if present.
rest, _, query = rest.partition('?')
# dissect the part after the directory name into a script name &
# a possible additional path, to be stored in PATH_INFO.
i = rest.find('/')
if i >= 0:
script, rest = rest[:i], rest[i:]
else:
script, rest = rest, ''
scriptname = dir + '/' + script
scriptfile = self.translate_path(scriptname)
if not os.path.exists(scriptfile):
self.send_error(
HTTPStatus.NOT_FOUND,
"No such CGI script (%r)" % scriptname)
return
if not os.path.isfile(scriptfile):
self.send_error(
HTTPStatus.FORBIDDEN,
"CGI script is not a plain file (%r)" % scriptname)
return
ispy = self.is_python(scriptname)
if self.have_fork or not ispy:
if not self.is_executable(scriptfile):
self.send_error(
HTTPStatus.FORBIDDEN,
"CGI script is not executable (%r)" % scriptname)
return
# 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()
env['SERVER_NAME'] = self.server.server_name
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
env['SERVER_PROTOCOL'] = self.protocol_version
env['SERVER_PORT'] = str(self.server.server_port)
env['REQUEST_METHOD'] = self.command
uqrest = urllib.parse.unquote(rest)
env['PATH_INFO'] = uqrest
env['PATH_TRANSLATED'] = self.translate_path(uqrest)
env['SCRIPT_NAME'] = scriptname
env['QUERY_STRING'] = query
env['REMOTE_ADDR'] = self.client_address[0]
authorization = self.headers.get("authorization")
if authorization:
authorization = authorization.split()
if len(authorization) == 2:
import base64, binascii
env['AUTH_TYPE'] = authorization[0]
if authorization[0].lower() == "basic":
try:
authorization = authorization[1].encode('ascii')
authorization = base64.decodebytes(authorization).\
decode('ascii')
except (binascii.Error, UnicodeError):
pass
else:
authorization = authorization.split(':')
if len(authorization) == 2:
env['REMOTE_USER'] = authorization[0]
# XXX REMOTE_IDENT
if self.headers.get('content-type') is None:
env['CONTENT_TYPE'] = self.headers.get_content_type()
else:
env['CONTENT_TYPE'] = self.headers['content-type']
length = self.headers.get('content-length')
if length:
env['CONTENT_LENGTH'] = length
referer = self.headers.get('referer')
if referer:
env['HTTP_REFERER'] = referer
accept = self.headers.get_all('accept', ())
env['HTTP_ACCEPT'] = ','.join(accept)
ua = self.headers.get('user-agent')
if ua:
env['HTTP_USER_AGENT'] = ua
co = filter(None, self.headers.get_all('cookie', []))
cookie_str = ', '.join(co)
if cookie_str:
env['HTTP_COOKIE'] = cookie_str
# XXX Other HTTP_* headers
# Since we're setting the env in the parent, provide empty
# values to override previously set values
for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
env.setdefault(k, "")
self.send_response(HTTPStatus.OK, "Script output follows")
self.flush_headers()
decoded_query = query.replace('+', ' ')
if self.have_fork:
# Unix -- fork as we should
args = [script]
if '=' not in decoded_query:
args.append(decoded_query)
nobody = nobody_uid()
self.wfile.flush() # Always flush before forking
pid = os.fork()
if pid != 0:
# Parent
pid, sts = os.waitpid(pid, 0)
# throw away additional data [see bug #427345]
while select.select([self.rfile], [], [], 0)[0]:
if not self.rfile.read(1):
break
exitcode = os.waitstatus_to_exitcode(sts)
if exitcode:
self.log_error(f"CGI script exit code {exitcode}")
return
# Child
try:
try:
os.setuid(nobody)
except OSError:
pass
os.dup2(self.rfile.fileno(), 0)
os.dup2(self.wfile.fileno(), 1)
os.execve(scriptfile, args, env)
except:
self.server.handle_error(self.request, self.client_address)
os._exit(127)
else:
# Non-Unix -- use subprocess
import subprocess
cmdline = [scriptfile]
if self.is_python(scriptfile):
interp = sys.executable
if interp.lower().endswith("w.exe"):
# On Windows, use python.exe, not pythonw.exe
interp = interp[:-5] + interp[-4:]
cmdline = [interp, '-u'] + cmdline
if '=' not in query:
cmdline.append(query)
self.log_message("command: %s", subprocess.list2cmdline(cmdline))
try:
nbytes = int(length)
except (TypeError, ValueError):
nbytes = 0
p = subprocess.Popen(cmdline,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env = env
)
if self.command.lower() == "post" and nbytes > 0:
data = self.rfile.read(nbytes)
else:
data = None
# throw away additional data [see bug #427345]
while select.select([self.rfile._sock], [], [], 0)[0]:
if not self.rfile._sock.recv(1):
break
stdout, stderr = p.communicate(data)
self.wfile.write(stdout)
if stderr:
self.log_error('%s', stderr)
p.stderr.close()
p.stdout.close()
status = p.returncode
if status:
self.log_error("CGI script exit status %#x", status)
else:
self.log_message("CGI script exited OK")
def _get_best_family(*address):
infos = socket.getaddrinfo(
*address,
@ -1336,13 +996,12 @@ def test(HandlerClass=BaseHTTPRequestHandler,
print("\nKeyboard interrupt received, exiting.")
sys.exit(0)
if __name__ == '__main__':
import argparse
import contextlib
parser = argparse.ArgumentParser(color=True)
parser.add_argument('--cgi', action='store_true',
help='run as CGI server')
parser.add_argument('-b', '--bind', metavar='ADDRESS',
help='bind to this address '
'(default: all interfaces)')
@ -1378,11 +1037,6 @@ if __name__ == '__main__':
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):
@ -1398,7 +1052,7 @@ if __name__ == '__main__':
directory=args.directory)
test(
HandlerClass=handler_class,
HandlerClass=SimpleHTTPRequestHandler,
ServerClass=DualStackServer,
port=args.port,
bind=args.bind,

View File

@ -3,16 +3,15 @@
Written by Cody A.W. Somerville <cody-somerville@ubuntu.com>,
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
"""
from collections import OrderedDict
from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
SimpleHTTPRequestHandler
from http import server, HTTPStatus
import os
import socket
import sys
import re
import base64
import ntpath
import pathlib
import shutil
@ -31,7 +30,7 @@ from io import BytesIO, StringIO
import unittest
from test import support
from test.support import (
is_apple, import_helper, os_helper, requires_subprocess, threading_helper
is_apple, import_helper, os_helper, threading_helper
)
try:
@ -820,329 +819,6 @@ class SimpleHTTPServerTestCase(BaseTestCase):
self.tempdir_name + "/?hi=1")
cgi_file1 = """\
#!%s
print("Content-type: text/html")
print()
print("Hello World")
"""
cgi_file2 = """\
#!%s
import os
import sys
import urllib.parse
print("Content-type: text/html")
print()
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 = """\
#!%s
import os
print("Content-type: text/html")
print()
print(os.environ["%s"])
"""
cgi_file6 = """\
#!%s
import os
print("X-ambv: was here")
print("Content-type: text/html")
print()
print("<pre>")
for k, v in os.environ.items():
try:
k.encode('ascii')
v.encode('ascii')
except UnicodeEncodeError:
continue # see: BPO-44647
print(f"{k}={v}")
print("</pre>")
"""
@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):
_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()
self.cgi_dir = os.path.join(self.parent_dir, 'cgi-bin')
self.cgi_child_dir = os.path.join(self.cgi_dir, 'child-dir')
self.sub_dir_1 = os.path.join(self.parent_dir, 'sub')
self.sub_dir_2 = os.path.join(self.sub_dir_1, 'dir')
self.cgi_dir_in_sub_dir = os.path.join(self.sub_dir_2, 'cgi-bin')
os.mkdir(self.cgi_dir)
os.mkdir(self.cgi_child_dir)
os.mkdir(self.sub_dir_1)
os.mkdir(self.sub_dir_2)
os.mkdir(self.cgi_dir_in_sub_dir)
self.nocgi_path = None
self.file1_path = None
self.file2_path = None
self.file3_path = None
self.file4_path = None
self.file5_path = None
# The shebang line should be pure ASCII: use symlink if possible.
# See issue #7668.
self._pythonexe_symlink = None
if os_helper.can_symlink():
self.pythonexe = os.path.join(self.parent_dir, 'python')
self._pythonexe_symlink = support.PythonSymlink(self.pythonexe).__enter__()
else:
self.pythonexe = sys.executable
try:
# The python executable path is written as the first line of the
# CGI Python script. The encoding cookie cannot be used, and so the
# path should be encodable to the default script encoding (utf-8)
self.pythonexe.encode('utf-8')
except UnicodeEncodeError:
self.tearDown()
self.skipTest("Python executable path is not encodable to utf-8")
self.nocgi_path = os.path.join(self.parent_dir, 'nocgi.py')
with open(self.nocgi_path, 'w', encoding='utf-8') as fp:
fp.write(cgi_file1 % self.pythonexe)
os.chmod(self.nocgi_path, 0o777)
self.file1_path = os.path.join(self.cgi_dir, 'file1.py')
with open(self.file1_path, 'w', encoding='utf-8') as file1:
file1.write(cgi_file1 % self.pythonexe)
os.chmod(self.file1_path, 0o777)
self.file2_path = os.path.join(self.cgi_dir, 'file2.py')
with open(self.file2_path, 'w', encoding='utf-8') as file2:
file2.write(cgi_file2 % self.pythonexe)
os.chmod(self.file2_path, 0o777)
self.file3_path = os.path.join(self.cgi_child_dir, 'file3.py')
with open(self.file3_path, 'w', encoding='utf-8') as file3:
file3.write(cgi_file1 % self.pythonexe)
os.chmod(self.file3_path, 0o777)
self.file4_path = os.path.join(self.cgi_dir, 'file4.py')
with open(self.file4_path, 'w', encoding='utf-8') as file4:
file4.write(cgi_file4 % (self.pythonexe, 'QUERY_STRING'))
os.chmod(self.file4_path, 0o777)
self.file5_path = os.path.join(self.cgi_dir_in_sub_dir, 'file5.py')
with open(self.file5_path, 'w', encoding='utf-8') as file5:
file5.write(cgi_file1 % self.pythonexe)
os.chmod(self.file5_path, 0o777)
self.file6_path = os.path.join(self.cgi_dir, 'file6.py')
with open(self.file6_path, 'w', encoding='utf-8') as file6:
file6.write(cgi_file6 % self.pythonexe)
os.chmod(self.file6_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:
self._pythonexe_symlink.__exit__(None, None, None)
if self.nocgi_path:
os.remove(self.nocgi_path)
if self.file1_path:
os.remove(self.file1_path)
if self.file2_path:
os.remove(self.file2_path)
if self.file3_path:
os.remove(self.file3_path)
if self.file4_path:
os.remove(self.file4_path)
if self.file5_path:
os.remove(self.file5_path)
if self.file6_path:
os.remove(self.file6_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)
def test_url_collapse_path(self):
# verify tail is the last portion and head is the rest on proper urls
test_vectors = {
'': '//',
'..': IndexError,
'/.//..': IndexError,
'/': '//',
'//': '//',
'/\\': '//\\',
'/.//': '//',
'cgi-bin/file1.py': '/cgi-bin/file1.py',
'/cgi-bin/file1.py': '/cgi-bin/file1.py',
'a': '//a',
'/a': '//a',
'//a': '//a',
'./a': '//a',
'./C:/': '/C:/',
'/a/b': '/a/b',
'/a/b/': '/a/b/',
'/a/b/.': '/a/b/',
'/a/b/c/..': '/a/b/',
'/a/b/c/../d': '/a/b/d',
'/a/b/c/../d/e/../f': '/a/b/d/f',
'/a/b/c/../d/e/../../f': '/a/b/f',
'/a/b/c/../d/e/.././././..//f': '/a/b/f',
'../a/b/c/../d/e/.././././..//f': IndexError,
'/a/b/c/../d/e/../../../f': '/a/f',
'/a/b/c/../d/e/../../../../f': '//f',
'/a/b/c/../d/e/../../../../../f': IndexError,
'/a/b/c/../d/e/../../../../f/..': '//',
'/a/b/c/../d/e/../../../../f/../.': '//',
}
for path, expected in test_vectors.items():
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected,
server._url_collapse_path, path)
else:
actual = server._url_collapse_path(path)
self.assertEqual(expected, actual,
msg='path = %r\nGot: %r\nWanted: %r' %
(path, actual, expected))
def test_headers_and_content(self):
res = self.request('/cgi-bin/file1.py')
self.assertEqual(
(res.read(), res.getheader('Content-type'), res.status),
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK))
def test_issue19435(self):
res = self.request('///////////nocgi.py/../cgi-bin/nothere.sh')
self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
def test_post(self):
params = urllib.parse.urlencode(
{'spam' : 1, 'eggs' : 'python', 'bacon' : 123456})
headers = {'Content-type' : 'application/x-www-form-urlencoded'}
res = self.request('/cgi-bin/file2.py', 'POST', params, headers)
self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
def test_invaliduri(self):
res = self.request('/cgi-bin/invalid')
res.read()
self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
def test_authorization(self):
headers = {b'Authorization' : b'Basic ' +
base64.b64encode(b'username:pass')}
res = self.request('/cgi-bin/file1.py', 'GET', headers=headers)
self.assertEqual(
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
(res.read(), res.getheader('Content-type'), res.status))
def test_no_leading_slash(self):
# http://bugs.python.org/issue2254
res = self.request('cgi-bin/file1.py')
self.assertEqual(
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
(res.read(), res.getheader('Content-type'), res.status))
def test_os_environ_is_not_altered(self):
signature = "Test CGI Server"
os.environ['SERVER_SOFTWARE'] = signature
res = self.request('/cgi-bin/file1.py')
self.assertEqual(
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
(res.read(), res.getheader('Content-type'), res.status))
self.assertEqual(os.environ['SERVER_SOFTWARE'], signature)
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))
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))
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))
def test_query_with_continuous_slashes(self):
res = self.request('/cgi-bin/file4.py?k=aa%2F%2Fbb&//q//p//=//a//b//')
self.assertEqual(
(b'k=aa%2F%2Fbb&//q//p//=//a//b//' + self.linesep,
'text/html', HTTPStatus.OK),
(res.read(), res.getheader('Content-type'), res.status))
def test_cgi_path_in_sub_directories(self):
try:
CGIHTTPRequestHandler.cgi_directories.append('/sub/dir/cgi-bin')
res = self.request('/sub/dir/cgi-bin/file5.py')
self.assertEqual(
(b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
(res.read(), res.getheader('Content-type'), res.status))
finally:
CGIHTTPRequestHandler.cgi_directories.remove('/sub/dir/cgi-bin')
def test_accept(self):
browser_accept = \
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
tests = (
((('Accept', browser_accept),), browser_accept),
((), ''),
# Hack case to get two values for the one header
((('Accept', 'text/html'), ('ACCEPT', 'text/plain')),
'text/html,text/plain'),
)
for headers, expected in tests:
headers = OrderedDict(headers)
with self.subTest(headers):
res = self.request('/cgi-bin/file6.py', 'GET', headers=headers)
self.assertEqual(http.HTTPStatus.OK, res.status)
expected = f"HTTP_ACCEPT={expected}".encode('ascii')
self.assertIn(expected, res.read())
class SocketlessRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, directory=None):
request = mock.Mock()
@ -1162,6 +838,7 @@ class SocketlessRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass
class RejectingSocketlessRequestHandler(SocketlessRequestHandler):
def handle_expect_100(self):
self.send_error(HTTPStatus.EXPECTATION_FAILED)

View File

@ -69,7 +69,8 @@ def read_environ():
# Python 3's http.server.CGIHTTPRequestHandler decodes
# using the urllib.unquote default of UTF-8, amongst other
# issues.
# issues. While the CGI handler is removed in 3.15, this
# is kept for legacy reasons.
elif (
software.startswith('simplehttp/')
and 'python/3' in software

View File

@ -2294,7 +2294,7 @@ superclass. Patch by James Hilton-Balfe
.. nonce: VksX1D
.. section: Library
:class:`http.server.CGIHTTPRequestHandler` has been deprecated for removal
:class:`!http.server.CGIHTTPRequestHandler` has been deprecated for removal
in 3.15. Its design is old and the web world has long since moved beyond
CGI.

View File

@ -0,0 +1,3 @@
Remove :class:`!http.server.CGIHTTPRequestHandler` and ``--cgi`` flag from the
:program:`python -m http.server` command-line interface. They were
deprecated in Python 3.13. Patch by Bénédikt Tran.