handle Y2038 in timestamp to datetime conversions

According to datetime.utcfromtimestamp() method documentation[1],
this and similar methods fail for dates past 2038 and can be replaced by
the following expression on the POSIX compliant systems:

  datetime(1970, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=timestamp)

Make sure to use a method that at least allows to import the timestamps
properly to datetime objects on 32-bit platforms.

[1] https://docs.python.org/3/library/datetime.html#datetime.datetime.utcfromtimestamp

Fixes: https://pagure.io/freeipa/issue/8378

Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
Alexander Bokovoy 2020-06-20 12:03:07 +03:00
parent a3c648bd92
commit 1f6ca418ee
6 changed files with 61 additions and 21 deletions

View File

@ -22,6 +22,7 @@ import datetime
import email.utils
from calendar import timegm
from urllib.parse import urlparse
from ipapython.ipautil import datetime_from_utctimestamp
'''
@ -184,7 +185,7 @@ class Cookie:
# use the RFC 1123 parsing function which uses only English
try:
dt = datetime.datetime(*email.utils.parsedate(s)[0:6])
dt = email.utils.parsedate_to_datetime(s)
except Exception as e:
raise ValueError("unable to parse expires datetime '%s': %s" % (s, e))
@ -390,7 +391,7 @@ class Cookie:
elif isinstance(value, datetime.datetime):
self._timestamp = value
elif isinstance(value, (int, float)):
self._timestamp = datetime.datetime.utcfromtimestamp(value)
self._timestamp = datetime_from_utctimestamp(value, units=1)
elif isinstance(value, str):
self._timestamp = Cookie.parse_datetime(value)
else:
@ -415,8 +416,10 @@ class Cookie:
self._expires = None
elif isinstance(value, datetime.datetime):
self._expires = value
if self._expires.tzinfo is None:
self._expires.replace(tzinfo=datetime.timezone.utc)
elif isinstance(value, (int, float)):
self._expires = datetime.datetime.utcfromtimestamp(value)
self._expires = datetime_from_utctimestamp(value, units=1)
elif isinstance(value, str):
self._expires = Cookie.parse_datetime(value)
else:

View File

@ -1647,3 +1647,27 @@ def rmtree(path):
shutil.rmtree(path)
except Exception as e:
logger.error('Error removing %s: %s', path, str(e))
def datetime_from_utctimestamp(t, units=1):
"""
Convert a timestamp or a time.struct_time to a datetime.datetime
object down to seconds, with UTC timezone
The conversion is safe for year 2038 problem
:param t: int or float timestamp in (milli)seconds since UNIX epoch
or time.struct_time
:param units: normalizing factor for the timestamp
(1 for seconds, 1000 for milliseconds)
defaults to 1
:return: datetime.datetime object in UTC timezone
"""
if isinstance(t, time.struct_time):
v = int(time.mktime(t))
elif isinstance(t, (float, int)):
v = int(t)
else:
raise TypeError(t)
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
return epoch + datetime.timedelta(seconds=v // units)

View File

@ -2,7 +2,6 @@
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
from datetime import datetime
import os
from ipaserver.dnssec._odsbase import AbstractODSDBConnection
@ -39,7 +38,10 @@ class ODSDBConnection(AbstractODSDBConnection):
for row in cur:
key = dict()
key['HSMkey_id'] = row['locator']
key['generate'] = str(datetime.fromtimestamp(row['inception']))
key['generate'] = ipautil.datetime_from_utctimestamp(
row['inception'],
units=1).replace(tzinfo=None).isoformat(
sep=' ', timespec='seconds')
key['algorithm'] = row['algorithm']
key['publish'] = key['generate']
key['active'] = None

View File

@ -52,6 +52,7 @@ from ipalib.request import context
from ipalib import output
from ipapython import dnsutil, kerberos
from ipapython.dn import DN
from ipapython.ipautil import datetime_from_utctimestamp
from ipaserver.plugins.service import normalize_principal, validate_realm
from ipaserver.masters import (
ENABLED_SERVICE, CONFIGURED_SERVICE, is_service_enabled
@ -254,8 +255,9 @@ def normalize_pkidate(value):
def convert_pkidatetime(value):
value = datetime.datetime.fromtimestamp(int(value) // 1000)
return x509.format_datetime(value)
if isinstance(value, str):
value = int(value)
return x509.format_datetime(datetime_from_utctimestamp(value, units=1000))
def normalize_serial_number(num):

View File

@ -241,7 +241,6 @@ digits and nothing else follows.
from __future__ import absolute_import
import datetime
import json
import logging
@ -651,12 +650,14 @@ def parse_check_request_result_xml(doc):
updated_on = doc.xpath('//xml/header/updatedOn[1]')
if len(updated_on) == 1:
updated_on = datetime.datetime.utcfromtimestamp(int(updated_on[0].text))
updated_on = ipautil.datetime_from_utctimestamp(
int(updated_on[0].text), units=1)
response['updated_on'] = updated_on
created_on = doc.xpath('//xml/header/createdOn[1]')
if len(created_on) == 1:
created_on = datetime.datetime.utcfromtimestamp(int(created_on[0].text))
created_on = ipautil.datetime_from_utctimestamp(
int(created_on[0].text), units=1)
response['created_on'] = created_on
request_notes = doc.xpath('//xml/header/requestNotes[1]')

View File

@ -19,9 +19,8 @@
import datetime
import email.utils
import calendar
from ipapython.cookie import Cookie
from ipapython.ipautil import datetime_from_utctimestamp
import pytest
pytestmark = pytest.mark.tier0
@ -148,17 +147,21 @@ class TestExpires:
@pytest.fixture(autouse=True)
def expires_setup(self):
# Force microseconds to zero because cookie timestamps only have second resolution
self.now = datetime.datetime.utcnow().replace(microsecond=0)
self.now_timestamp = calendar.timegm(self.now.utctimetuple())
self.now = datetime.datetime.now(
tz=datetime.timezone.utc).replace(microsecond=0)
self.now_timestamp = datetime_from_utctimestamp(
self.now.utctimetuple(), units=1).timestamp()
self.now_string = email.utils.formatdate(self.now_timestamp, usegmt=True)
self.max_age = 3600 # 1 hour
self.age_expiration = self.now + datetime.timedelta(seconds=self.max_age)
self.age_timestamp = calendar.timegm(self.age_expiration.utctimetuple())
self.age_timestamp = datetime_from_utctimestamp(
self.age_expiration.utctimetuple(), units=1).timestamp()
self.age_string = email.utils.formatdate(self.age_timestamp, usegmt=True)
self.expires = self.now + datetime.timedelta(days=1) # 1 day
self.expires_timestamp = calendar.timegm(self.expires.utctimetuple())
self.expires_timestamp = datetime_from_utctimestamp(
self.expires.utctimetuple(), units=1).timestamp()
self.expires_string = email.utils.formatdate(self.expires_timestamp, usegmt=True)
def test_expires(self):
@ -327,7 +330,8 @@ class TestAttributes:
assert cookie.max_age is None
cookie.expires = 'Sun, 06 Nov 1994 08:49:37 GMT'
assert cookie.expires == datetime.datetime(1994, 11, 6, 8, 49, 37)
assert cookie.expires == datetime.datetime(
1994, 11, 6, 8, 49, 37, tzinfo=datetime.timezone.utc)
cookie.expires = None
assert cookie.expires is None
@ -433,17 +437,21 @@ class TestNormalization:
@pytest.fixture(autouse=True)
def normalization_setup(self):
# Force microseconds to zero because cookie timestamps only have second resolution
self.now = datetime.datetime.utcnow().replace(microsecond=0)
self.now_timestamp = calendar.timegm(self.now.utctimetuple())
self.now = datetime.datetime.now(
tz=datetime.timezone.utc).replace(microsecond=0)
self.now_timestamp = datetime_from_utctimestamp(
self.now.utctimetuple(), units=1).timestamp()
self.now_string = email.utils.formatdate(self.now_timestamp, usegmt=True)
self.max_age = 3600 # 1 hour
self.age_expiration = self.now + datetime.timedelta(seconds=self.max_age)
self.age_timestamp = calendar.timegm(self.age_expiration.utctimetuple())
self.age_timestamp = datetime_from_utctimestamp(
self.age_expiration.utctimetuple(), units=1).timestamp()
self.age_string = email.utils.formatdate(self.age_timestamp, usegmt=True)
self.expires = self.now + datetime.timedelta(days=1) # 1 day
self.expires_timestamp = calendar.timegm(self.expires.utctimetuple())
self.expires_timestamp = datetime_from_utctimestamp(
self.expires.utctimetuple(), units=1).timestamp()
self.expires_string = email.utils.formatdate(self.expires_timestamp, usegmt=True)
def test_path_normalization(self):