# Copyright (c) 2024, Felix Fontein # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations import datetime import re from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( OpenSSLObjectError, ) try: UTC = datetime.timezone.utc except AttributeError: _DURATION_ZERO = datetime.timedelta(0) class _UTCClass(datetime.tzinfo): def utcoffset(self, dt): return _DURATION_ZERO def dst(self, dt): return _DURATION_ZERO def tzname(self, dt): return "UTC" def fromutc(self, dt): return dt def __repr__(self): return "UTC" UTC = _UTCClass() def get_now_datetime(with_timezone): if with_timezone: return datetime.datetime.now(tz=UTC) return datetime.datetime.utcnow() def ensure_utc_timezone(timestamp): if timestamp.tzinfo is UTC: return timestamp if timestamp.tzinfo is None: # We assume that naive datetime objects use timezone UTC! return timestamp.replace(tzinfo=UTC) return timestamp.astimezone(UTC) def remove_timezone(timestamp): # Convert to native datetime object if timestamp.tzinfo is None: return timestamp if timestamp.tzinfo is not UTC: timestamp = timestamp.astimezone(UTC) return timestamp.replace(tzinfo=None) def add_or_remove_timezone(timestamp, with_timezone): return ( ensure_utc_timezone(timestamp) if with_timezone else remove_timezone(timestamp) ) def get_epoch_seconds(timestamp): if timestamp.tzinfo is None: # timestamp.timestamp() is offset by the local timezone if timestamp has no timezone timestamp = ensure_utc_timezone(timestamp) return timestamp.timestamp() def from_epoch_seconds(timestamp, with_timezone): if with_timezone: return datetime.datetime.fromtimestamp(timestamp, UTC) return datetime.datetime.utcfromtimestamp(timestamp) def convert_relative_to_datetime(relative_time_string, with_timezone=False, now=None): """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)""" parsed_result = re.match( r"^(?P[+-])((?P\d+)[wW])?((?P\d+)[dD])?((?P\d+)[hH])?((?P\d+)[mM])?((?P\d+)[sS]?)?$", relative_time_string, ) if parsed_result is None or len(relative_time_string) == 1: # not matched or only a single "+" or "-" return None offset = datetime.timedelta(0) if parsed_result.group("weeks") is not None: offset += datetime.timedelta(weeks=int(parsed_result.group("weeks"))) if parsed_result.group("days") is not None: offset += datetime.timedelta(days=int(parsed_result.group("days"))) if parsed_result.group("hours") is not None: offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) if parsed_result.group("minutes") is not None: offset += datetime.timedelta(minutes=int(parsed_result.group("minutes"))) if parsed_result.group("seconds") is not None: offset += datetime.timedelta(seconds=int(parsed_result.group("seconds"))) if now is None: now = get_now_datetime(with_timezone=with_timezone) else: now = add_or_remove_timezone(now, with_timezone=with_timezone) if parsed_result.group("prefix") == "+": return now + offset else: return now - offset def get_relative_time_option( input_string, input_name, backend="cryptography", with_timezone=False, now=None ): """ Return an absolute timespec if a relative timespec or an ASN1 formatted string is provided. The return value will be a datetime object for the cryptography backend. """ result = to_native(input_string) if result is None: raise OpenSSLObjectError( f'The timespec "{input_string}" for {input_name} is not valid' ) # Relative time if result.startswith("+") or result.startswith("-"): result_datetime = convert_relative_to_datetime( result, with_timezone=with_timezone, now=now ) if backend == "cryptography": return result_datetime # Absolute time if backend == "cryptography": for date_fmt, length in [ ( "%Y%m%d%H%M%SZ", 15, ), # this also parses '202401020304Z', but as datetime(2024, 1, 2, 3, 0, 4) ("%Y%m%d%H%MZ", 13), ( "%Y%m%d%H%M%S%z", 14 + 5, ), # this also parses '202401020304+0000', but as datetime(2024, 1, 2, 3, 0, 4, tzinfo=...) ("%Y%m%d%H%M%z", 12 + 5), ]: if len(result) != length: continue try: res = datetime.datetime.strptime(result, date_fmt) except ValueError: pass else: return add_or_remove_timezone(res, with_timezone=with_timezone) raise OpenSSLObjectError( f'The time spec "{input_string}" for {input_name} is invalid' )