mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
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
11 changed files with 28 additions and 755 deletions
|
@ -20,7 +20,7 @@ Pending removal in Python 3.15
|
||||||
|
|
||||||
* :mod:`http.server`:
|
* :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.
|
has been deprecated since Python 3.13.
|
||||||
No direct replacement exists.
|
No direct replacement exists.
|
||||||
*Anything* is better than CGI to interface
|
*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`.
|
: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:
|
.. _http-server-cli:
|
||||||
|
|
||||||
Command-line interface
|
Command-line interface
|
||||||
|
@ -563,24 +514,6 @@ The following options are accepted:
|
||||||
|
|
||||||
.. versionadded:: 3.11
|
.. 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
|
.. option:: --tls-cert
|
||||||
|
|
||||||
Specifies a TLS certificate chain for HTTPS connections::
|
Specifies a TLS certificate chain for HTTPS connections::
|
||||||
|
|
|
@ -1871,7 +1871,7 @@ New Deprecations
|
||||||
|
|
||||||
* :mod:`http.server`:
|
* :mod:`http.server`:
|
||||||
|
|
||||||
* Deprecate :class:`~http.server.CGIHTTPRequestHandler`,
|
* Deprecate :class:`!CGIHTTPRequestHandler`,
|
||||||
to be removed in Python 3.15.
|
to be removed in Python 3.15.
|
||||||
Process-based CGI HTTP servers have been out of favor for a very long time.
|
Process-based CGI HTTP servers have been out of favor for a very long time.
|
||||||
This code was outdated, unmaintained, and rarely used.
|
This code was outdated, unmaintained, and rarely used.
|
||||||
|
|
|
@ -121,6 +121,15 @@ Deprecated
|
||||||
Removed
|
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
|
platform
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,6 @@ IMPORT_MAPPING.update({
|
||||||
'SimpleDialog': 'tkinter.simpledialog',
|
'SimpleDialog': 'tkinter.simpledialog',
|
||||||
'DocXMLRPCServer': 'xmlrpc.server',
|
'DocXMLRPCServer': 'xmlrpc.server',
|
||||||
'SimpleHTTPServer': 'http.server',
|
'SimpleHTTPServer': 'http.server',
|
||||||
'CGIHTTPServer': 'http.server',
|
|
||||||
# For compatibility with broken pickles saved in old Python 3 versions
|
# For compatibility with broken pickles saved in old Python 3 versions
|
||||||
'UserDict': 'collections',
|
'UserDict': 'collections',
|
||||||
'UserList': 'collections',
|
'UserList': 'collections',
|
||||||
|
@ -217,8 +216,6 @@ REVERSE_NAME_MAPPING.update({
|
||||||
('DocXMLRPCServer', 'DocCGIXMLRPCRequestHandler'),
|
('DocXMLRPCServer', 'DocCGIXMLRPCRequestHandler'),
|
||||||
('http.server', 'SimpleHTTPRequestHandler'):
|
('http.server', 'SimpleHTTPRequestHandler'):
|
||||||
('SimpleHTTPServer', 'SimpleHTTPRequestHandler'),
|
('SimpleHTTPServer', 'SimpleHTTPRequestHandler'),
|
||||||
('http.server', 'CGIHTTPRequestHandler'):
|
|
||||||
('CGIHTTPServer', 'CGIHTTPRequestHandler'),
|
|
||||||
('_socket', 'socket'): ('socket', '_socketobject'),
|
('_socket', 'socket'): ('socket', '_socketobject'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -181,11 +181,10 @@ def _strip_ipv6_iface(enc_name: bytes) -> bytes:
|
||||||
return enc_name
|
return enc_name
|
||||||
|
|
||||||
class HTTPMessage(email.message.Message):
|
class HTTPMessage(email.message.Message):
|
||||||
# XXX The only usage of this method is in
|
|
||||||
# http.server.CGIHTTPRequestHandler. Maybe move the code there so
|
# The getallmatchingheaders() method was only used by the CGI handler
|
||||||
# that it doesn't need to be part of the public API. The API has
|
# that was removed in Python 3.15. However, since the public API was not
|
||||||
# never been defined so this could cause backwards compatibility
|
# properly defined, it will be kept for backwards compatibility reasons.
|
||||||
# issues.
|
|
||||||
|
|
||||||
def getallmatchingheaders(self, name):
|
def getallmatchingheaders(self, name):
|
||||||
"""Find all header lines matching a given header name.
|
"""Find all header lines matching a given header name.
|
||||||
|
|
|
@ -1,29 +1,10 @@
|
||||||
"""HTTP server classes.
|
"""HTTP server classes.
|
||||||
|
|
||||||
Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
|
Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
|
||||||
SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST,
|
SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST.
|
||||||
and (deprecated) CGIHTTPRequestHandler for CGI scripts.
|
|
||||||
|
|
||||||
It does, however, optionally implement HTTP/1.1 persistent connections.
|
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:
|
XXX To do:
|
||||||
|
|
||||||
- log requests even later (to capture byte count)
|
- log requests even later (to capture byte count)
|
||||||
|
@ -86,10 +67,8 @@ __all__ = [
|
||||||
"HTTPServer", "ThreadingHTTPServer",
|
"HTTPServer", "ThreadingHTTPServer",
|
||||||
"HTTPSServer", "ThreadingHTTPSServer",
|
"HTTPSServer", "ThreadingHTTPSServer",
|
||||||
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
|
"BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
|
||||||
"CGIHTTPRequestHandler",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import email.utils
|
import email.utils
|
||||||
import html
|
import html
|
||||||
|
@ -99,7 +78,6 @@ import itertools
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import select
|
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import socketserver
|
import socketserver
|
||||||
|
@ -953,56 +931,6 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
|
||||||
return 'application/octet-stream'
|
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
|
nobody = None
|
||||||
|
|
||||||
def nobody_uid():
|
def nobody_uid():
|
||||||
|
@ -1026,274 +954,6 @@ def executable(path):
|
||||||
return os.access(path, os.X_OK)
|
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):
|
def _get_best_family(*address):
|
||||||
infos = socket.getaddrinfo(
|
infos = socket.getaddrinfo(
|
||||||
*address,
|
*address,
|
||||||
|
@ -1336,13 +996,12 @@ def test(HandlerClass=BaseHTTPRequestHandler,
|
||||||
print("\nKeyboard interrupt received, exiting.")
|
print("\nKeyboard interrupt received, exiting.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import argparse
|
import argparse
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(color=True)
|
parser = argparse.ArgumentParser(color=True)
|
||||||
parser.add_argument('--cgi', action='store_true',
|
|
||||||
help='run as CGI server')
|
|
||||||
parser.add_argument('-b', '--bind', metavar='ADDRESS',
|
parser.add_argument('-b', '--bind', metavar='ADDRESS',
|
||||||
help='bind to this address '
|
help='bind to this address '
|
||||||
'(default: all interfaces)')
|
'(default: all interfaces)')
|
||||||
|
@ -1378,11 +1037,6 @@ if __name__ == '__main__':
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
parser.error(f"Failed to read TLS password file: {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
|
# ensure dual-stack is not disabled; ref #38907
|
||||||
class DualStackServer(ThreadingHTTPServer):
|
class DualStackServer(ThreadingHTTPServer):
|
||||||
|
|
||||||
|
@ -1398,7 +1052,7 @@ if __name__ == '__main__':
|
||||||
directory=args.directory)
|
directory=args.directory)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
HandlerClass=handler_class,
|
HandlerClass=SimpleHTTPRequestHandler,
|
||||||
ServerClass=DualStackServer,
|
ServerClass=DualStackServer,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
bind=args.bind,
|
bind=args.bind,
|
||||||
|
|
|
@ -3,16 +3,15 @@
|
||||||
Written by Cody A.W. Somerville <cody-somerville@ubuntu.com>,
|
Written by Cody A.W. Somerville <cody-somerville@ubuntu.com>,
|
||||||
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
|
Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
|
||||||
"""
|
"""
|
||||||
from collections import OrderedDict
|
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
|
from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
|
||||||
SimpleHTTPRequestHandler, CGIHTTPRequestHandler
|
SimpleHTTPRequestHandler
|
||||||
from http import server, HTTPStatus
|
from http import server, HTTPStatus
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import base64
|
|
||||||
import ntpath
|
import ntpath
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -31,7 +30,7 @@ from io import BytesIO, StringIO
|
||||||
import unittest
|
import unittest
|
||||||
from test import support
|
from test import support
|
||||||
from test.support import (
|
from test.support import (
|
||||||
is_apple, import_helper, os_helper, requires_subprocess, threading_helper
|
is_apple, import_helper, os_helper, threading_helper
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -820,329 +819,6 @@ class SimpleHTTPServerTestCase(BaseTestCase):
|
||||||
self.tempdir_name + "/?hi=1")
|
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):
|
class SocketlessRequestHandler(SimpleHTTPRequestHandler):
|
||||||
def __init__(self, directory=None):
|
def __init__(self, directory=None):
|
||||||
request = mock.Mock()
|
request = mock.Mock()
|
||||||
|
@ -1162,6 +838,7 @@ class SocketlessRequestHandler(SimpleHTTPRequestHandler):
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RejectingSocketlessRequestHandler(SocketlessRequestHandler):
|
class RejectingSocketlessRequestHandler(SocketlessRequestHandler):
|
||||||
def handle_expect_100(self):
|
def handle_expect_100(self):
|
||||||
self.send_error(HTTPStatus.EXPECTATION_FAILED)
|
self.send_error(HTTPStatus.EXPECTATION_FAILED)
|
||||||
|
|
|
@ -69,7 +69,8 @@ def read_environ():
|
||||||
|
|
||||||
# Python 3's http.server.CGIHTTPRequestHandler decodes
|
# Python 3's http.server.CGIHTTPRequestHandler decodes
|
||||||
# using the urllib.unquote default of UTF-8, amongst other
|
# 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 (
|
elif (
|
||||||
software.startswith('simplehttp/')
|
software.startswith('simplehttp/')
|
||||||
and 'python/3' in software
|
and 'python/3' in software
|
||||||
|
|
|
@ -2294,7 +2294,7 @@ superclass. Patch by James Hilton-Balfe
|
||||||
.. nonce: VksX1D
|
.. nonce: VksX1D
|
||||||
.. section: Library
|
.. 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
|
in 3.15. Its design is old and the web world has long since moved beyond
|
||||||
CGI.
|
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…
Add table
Add a link
Reference in a new issue