Skip to content

gh-101035: Check fs.st_mtime before use. #101036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 8 additions & 5 deletions Doc/library/http.server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,14 @@ provides three different variants:

If the request was mapped to a file, it is opened. Any :exc:`OSError`
exception in opening the requested file is mapped to a ``404``,
``'File not found'`` error. If there was a ``'If-Modified-Since'``
header in the request, and the file was not modified after this time,
a ``304``, ``'Not Modified'`` response is sent. Otherwise, the content
type is guessed by calling the :meth:`guess_type` method, which in turn
uses the *extensions_map* variable, and the file contents are returned.
``'File not found'`` error. If the file's modified timestamp is less then
``-43200``, ``0`` will be used as the modified timestamp to avoid
:exc:`OSError` exception on ``'win32'`` platform. If there was a
``'If-Modified-Since'`` header in the request, and the file was not
modified after this time, a ``304``, ``'Not Modified'`` response is sent.
Otherwise, the content type is guessed by calling the :meth:`guess_type`
method, which in turn uses the *extensions_map* variable, and the file
contents are returned.

A ``'Content-type:'`` header with the guessed content type is output,
followed by a ``'Content-Length:'`` header with the file's size and a
Expand Down
12 changes: 10 additions & 2 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,14 @@ def send_head(self):

try:
fs = os.fstat(f.fileno())
# On Windows, _PyTime_gmtime() only allows timestamps larger or
# equal to -43200 (1969-12-31 12:00:00). But os.fstat() will
# happily return stat_result with st_mtime less then -43200. See
# issue #101035. For files with st_mtime less then -43200, clients
# might as well get the "updated" version.
mtime = fs.st_mtime
if sys.platform == 'win32' and mtime < -43200:
mtime = 0
# Use browser cache if possible
if ("If-Modified-Since" in self.headers
and "If-None-Match" not in self.headers):
Expand All @@ -749,7 +757,7 @@ def send_head(self):
if ims.tzinfo is datetime.timezone.utc:
# compare to UTC datetime of last modification
last_modif = datetime.datetime.fromtimestamp(
fs.st_mtime, datetime.timezone.utc)
mtime, datetime.timezone.utc)
# remove microseconds, like in If-Modified-Since
last_modif = last_modif.replace(microsecond=0)

Expand All @@ -763,7 +771,7 @@ def send_head(self):
self.send_header("Content-type", ctype)
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified",
self.date_time_string(fs.st_mtime))
self.date_time_string(mtime))
self.end_headers()
return f
except:
Expand Down
23 changes: 20 additions & 3 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,11 @@ def setUp(self):
self.tempdir = tempfile.mkdtemp(dir=basetempdir)
self.tempdir_name = os.path.basename(self.tempdir)
self.base_url = '/' + self.tempdir_name
tempname = os.path.join(self.tempdir, 'test')
with open(tempname, 'wb') as temp:
self.tempname = os.path.join(self.tempdir, 'test')
with open(self.tempname, 'wb') as temp:
temp.write(self.data)
temp.flush()
mtime = os.stat(tempname).st_mtime
mtime = os.stat(self.tempname).st_mtime
# compute last modification datetime for browser cache tests
last_modif = datetime.datetime.fromtimestamp(mtime,
datetime.timezone.utc)
Expand Down Expand Up @@ -574,6 +574,23 @@ def test_last_modified(self):
last_modif_header = response.headers['Last-modified']
self.assertEqual(last_modif_header, self.last_modif_header)

@unittest.skipUnless(sys.platform == 'win32',
'win32 specific OSError')
def test_last_modified_file_with_mtime_12_hour_before_epoch(self):
"""Check that SimpleHTTPServer can function correctly when file's
mtime is less then -43200 (before 1969-12-31 12:00:00+00:00).
"""
stat = os.stat(self.tempname)
atime_bak, mtime_bak = stat.st_atime, stat.st_mtime

os.utime(self.tempname, (-43201., -43201.))
try:
self.request(self.base_url + '/test')
except http.client.RemoteDisconnected as err:
raise err
finally:
os.utime(self.tempname, (atime_bak, mtime_bak))

def test_path_without_leading_slash(self):
response = self.request(self.tempdir_name + '/test')
self.check_status_and_reason(response, HTTPStatus.OK, data=self.data)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Check if the timestamp from :func:`os.fstat()` is valid for :func:`datetime.datetime.fromtimestamp()`.
If the timestamp is not valid, use ``0`` instead to avoid :exc:`OSError`.