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

View File

@@ -1647,3 +1647,27 @@ def rmtree(path):
shutil.rmtree(path) shutil.rmtree(path)
except Exception as e: except Exception as e:
logger.error('Error removing %s: %s', path, str(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 # Copyright (C) 2020 FreeIPA Contributors see COPYING for license
# #
from datetime import datetime
import os import os
from ipaserver.dnssec._odsbase import AbstractODSDBConnection from ipaserver.dnssec._odsbase import AbstractODSDBConnection
@@ -39,7 +38,10 @@ class ODSDBConnection(AbstractODSDBConnection):
for row in cur: for row in cur:
key = dict() key = dict()
key['HSMkey_id'] = row['locator'] 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['algorithm'] = row['algorithm']
key['publish'] = key['generate'] key['publish'] = key['generate']
key['active'] = None key['active'] = None

View File

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

View File

@@ -241,7 +241,6 @@ digits and nothing else follows.
from __future__ import absolute_import from __future__ import absolute_import
import datetime
import json import json
import logging import logging
@@ -651,12 +650,14 @@ def parse_check_request_result_xml(doc):
updated_on = doc.xpath('//xml/header/updatedOn[1]') updated_on = doc.xpath('//xml/header/updatedOn[1]')
if len(updated_on) == 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 response['updated_on'] = updated_on
created_on = doc.xpath('//xml/header/createdOn[1]') created_on = doc.xpath('//xml/header/createdOn[1]')
if len(created_on) == 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 response['created_on'] = created_on
request_notes = doc.xpath('//xml/header/requestNotes[1]') request_notes = doc.xpath('//xml/header/requestNotes[1]')

View File

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