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:
parent
2f1ecb3bc4
commit
faac627e47
@ -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
|
||||
|
@ -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::
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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'),
|
||||
})
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user