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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/library/difflib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
*linejunk*: A function that accepts a single string argument, and returns
true if the string is junk, or false if not. The default is ``None``. There
is also a module-level function :func:`IS_LINE_JUNK`, which filters out lines
without visible characters, except for at most one pound character (``'#'``)
without visible characters, except for at most one hash character (``'#'``)
-- however the underlying :class:`SequenceMatcher` class does a dynamic
analysis of which lines are so frequent as to constitute noise, and this
usually works better than using this function.
Expand Down
1,242 changes: 618 additions & 624 deletions Doc/whatsnew/3.14.rst

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ def parse_request(self):
error response has already been sent back.

"""
is_http_0_9 = False
self.command = None # set in case of error on the first line
self.request_version = version = self.default_request_version
self.close_connection = True
Expand Down Expand Up @@ -359,6 +360,7 @@ def parse_request(self):
HTTPStatus.BAD_REQUEST,
"Bad HTTP/0.9 request type (%r)" % command)
return False
is_http_0_9 = True
self.command, self.path = command, path

# gh-87389: The purpose of replacing '//' with '/' is to protect
Expand All @@ -368,6 +370,11 @@ def parse_request(self):
if self.path.startswith('//'):
self.path = '/' + self.path.lstrip('/') # Reduce to a single /

# For HTTP/0.9, headers are not expected at all.
if is_http_0_9:
self.headers = {}
return True

# Examine the headers and look for a Connection directive.
try:
self.headers = http.client.parse_headers(self.rfile,
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,44 @@ def test_head_via_send_error(self):
self.assertEqual(b'', data)


class HTTP09ServerTestCase(BaseTestCase):

class request_handler(NoLogRequestHandler, BaseHTTPRequestHandler):
"""Request handler for HTTP/0.9 server."""

def do_GET(self):
self.wfile.write(f'OK: here is {self.path}\r\n'.encode())

def setUp(self):
super().setUp()
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock = self.enterContext(self.sock)
self.sock.connect((self.HOST, self.PORT))

def test_simple_get(self):
self.sock.send(b'GET /index.html\r\n')
res = self.sock.recv(1024)
self.assertEqual(res, b"OK: here is /index.html\r\n")

def test_invalid_request(self):
self.sock.send(b'POST /index.html\r\n')
res = self.sock.recv(1024)
self.assertIn(b"Bad HTTP/0.9 request type ('POST')", res)

def test_single_request(self):
self.sock.send(b'GET /foo.html\r\n')
res = self.sock.recv(1024)
self.assertEqual(res, b"OK: here is /foo.html\r\n")

# Ignore errors if the connection is already closed,
# as this is the expected behavior of HTTP/0.9.
with contextlib.suppress(OSError):
self.sock.send(b'GET /bar.html\r\n')
res = self.sock.recv(1024)
# The server should not process our request.
self.assertEqual(res, b'')


def certdata_file(*path):
return os.path.join(os.path.dirname(__file__), "certdata", *path)

Expand Down
36 changes: 36 additions & 0 deletions Lib/test/test_pyexpat.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,42 @@ def resolve_entity(context, base, system_id, public_id):
self.assertEqual(handler_call_args, [("bar", "baz")])


class ParentParserLifetimeTest(unittest.TestCase):
"""
Subparsers make use of their parent XML_Parser inside of Expat.
As a result, parent parsers need to outlive subparsers.

See https://github.com/python/cpython/issues/139400.
"""

def test_parent_parser_outlives_its_subparsers__single(self):
parser = expat.ParserCreate()
subparser = parser.ExternalEntityParserCreate(None)

# Now try to cause garbage collection of the parent parser
# while it's still being referenced by a related subparser.
del parser

def test_parent_parser_outlives_its_subparsers__multiple(self):
parser = expat.ParserCreate()
subparser_one = parser.ExternalEntityParserCreate(None)
subparser_two = parser.ExternalEntityParserCreate(None)

# Now try to cause garbage collection of the parent parser
# while it's still being referenced by a related subparser.
del parser

def test_parent_parser_outlives_its_subparsers__chain(self):
parser = expat.ParserCreate()
subparser = parser.ExternalEntityParserCreate(None)
subsubparser = subparser.ExternalEntityParserCreate(None)

# Now try to cause garbage collection of the parent parsers
# while they are still being referenced by a related subparser.
del parser
del subparser


class ReparseDeferralTest(unittest.TestCase):
def test_getter_setter_round_trip(self):
parser = expat.ParserCreate()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`xml.parsers.expat`: Make sure that parent Expat parsers are only
garbage-collected once they are no longer referenced by subparsers created
by :meth:`~xml.parsers.expat.xmlparser.ExternalEntityParserCreate`.
Patch by Sebastian Pipping.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:mod:`http.server`: fix default handling of HTTP/0.9 requests in
:class:`~http.server.BaseHTTPRequestHandler`. Previously,
:meth:`!BaseHTTPRequestHandler.parse_request`` incorrectly
waited for headers in the request although those are not
supported in HTTP/0.9. Patch by Bénédikt Tran.
25 changes: 25 additions & 0 deletions Modules/pyexpat.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ typedef struct {
PyObject_HEAD

XML_Parser itself;
/*
* Strong reference to a parent `xmlparseobject` if this parser
* is a child parser. Set to NULL if this parser is a root parser.
* This is needed to keep the parent parser alive as long as it has
* at least one child parser.
*
* See https://github.com/python/cpython/issues/139400 for details.
*/
PyObject *parent;
int ordered_attributes; /* Return attributes as a list. */
int specified_attributes; /* Report only specified attributes. */
int in_callback; /* Is a callback active? */
Expand Down Expand Up @@ -1067,6 +1076,11 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self,
return NULL;
}

// The new subparser will make use of the parent XML_Parser inside of Expat.
// So we need to take subparsers into account with the reference counting
// of their parent parser.
Py_INCREF(self);

new_parser->buffer_size = self->buffer_size;
new_parser->buffer_used = 0;
new_parser->buffer = NULL;
Expand All @@ -1076,18 +1090,21 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self,
new_parser->ns_prefixes = self->ns_prefixes;
new_parser->itself = XML_ExternalEntityParserCreate(self->itself, context,
encoding);
new_parser->parent = (PyObject *)self;
new_parser->handlers = 0;
new_parser->intern = Py_XNewRef(self->intern);

if (self->buffer != NULL) {
new_parser->buffer = PyMem_Malloc(new_parser->buffer_size);
if (new_parser->buffer == NULL) {
Py_DECREF(new_parser);
Py_DECREF(self);
return PyErr_NoMemory();
}
}
if (!new_parser->itself) {
Py_DECREF(new_parser);
Py_DECREF(self);
return PyErr_NoMemory();
}

Expand All @@ -1101,6 +1118,7 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self,
new_parser->handlers = PyMem_New(PyObject *, i);
if (!new_parser->handlers) {
Py_DECREF(new_parser);
Py_DECREF(self);
return PyErr_NoMemory();
}
clear_handlers(new_parser, 1);
Expand Down Expand Up @@ -1481,6 +1499,7 @@ newxmlparseobject(pyexpat_state *state, const char *encoding,
/* namespace_separator is either NULL or contains one char + \0 */
self->itself = XML_ParserCreate_MM(encoding, &ExpatMemoryHandler,
namespace_separator);
self->parent = NULL;
if (self->itself == NULL) {
PyErr_SetString(PyExc_RuntimeError,
"XML_ParserCreate failed");
Expand Down Expand Up @@ -1517,6 +1536,7 @@ xmlparse_traverse(PyObject *op, visitproc visit, void *arg)
for (size_t i = 0; handler_info[i].name != NULL; i++) {
Py_VISIT(self->handlers[i]);
}
Py_VISIT(self->parent);
Py_VISIT(Py_TYPE(op));
return 0;
}
Expand All @@ -1527,6 +1547,10 @@ xmlparse_clear(PyObject *op)
xmlparseobject *self = xmlparseobject_CAST(op);
clear_handlers(self, 0);
Py_CLEAR(self->intern);
// NOTE: We cannot call Py_CLEAR(self->parent) prior to calling
// XML_ParserFree(self->itself), or a subparser could lose its parent
// XML_Parser while still making use of it internally.
// https://github.com/python/cpython/issues/139400
return 0;
}

Expand All @@ -1540,6 +1564,7 @@ xmlparse_dealloc(PyObject *op)
XML_ParserFree(self->itself);
}
self->itself = NULL;
Py_CLEAR(self->parent);

if (self->handlers != NULL) {
PyMem_Free(self->handlers);
Expand Down
Loading