]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3212] refactor time utils
authorPiotrek Zadroga <piotrek@isc.org>
Thu, 22 Feb 2024 12:30:15 +0000 (13:30 +0100)
committerPiotrek Zadroga <piotrek@isc.org>
Wed, 20 Mar 2024 15:17:29 +0000 (15:17 +0000)
Step 2: implement time utils in lib dns

src/lib/dhcp_ddns/ncr_msg.h
src/lib/dhcp_ddns/tests/ncr_unittests.cc
src/lib/dns/Makefile.am
src/lib/dns/tests/rdata_rrsig_unittest.cc
src/lib/dns/tests/rdata_tkey_unittest.cc
src/lib/dns/time_utils.cc [new file with mode: 0644]
src/lib/dns/time_utils.h [new file with mode: 0644]
src/lib/dns/tsig.cc

index 148f87a5107888d81bf983525cafaaae05a0b17d..6cb991f2b652b0c4a4c8c7e0754166e91329bc6c 100644 (file)
@@ -16,6 +16,7 @@
 #include <dhcp/duid.h>
 #include <dhcp/hwaddr.h>
 #include <dns/name.h>
+#include <dns/time_utils.h>
 #include <exceptions/exceptions.h>
 #include <util/buffer.h>
 
index ef77521d7575120d94f9395751da8913100db289..7dbac937e6e5c155a91838add05cc4e2ea4c8b5f 100644 (file)
@@ -275,7 +275,7 @@ TEST(NameChangeRequestTest, constructionTests) {
     EXPECT_TRUE(ncr);
 
     // Verify that full constructor works.
-    uint64_t expiry = isc::util::detail::gettimeWrapper();
+    uint64_t expiry = isc::util::detail::getTimeWrapper();
     D2Dhcid dhcid("010203040A7F8E3D");
 
     EXPECT_NO_THROW(ncr.reset(new NameChangeRequest(
index 9dd841065ab354177079509776455c994f70df8e..7dda8e4714c02771cd7ca09caa3f2f86175f3316 100644 (file)
@@ -38,6 +38,7 @@ libkea_dns___la_SOURCES += rrttl.h rrttl.cc
 libkea_dns___la_SOURCES += rrtype.cc
 libkea_dns___la_SOURCES += question.h question.cc
 libkea_dns___la_SOURCES += serial.h serial.cc
+libkea_dns___la_SOURCES += time_utils.h time_utils.cc
 libkea_dns___la_SOURCES += tsig.h tsig.cc
 libkea_dns___la_SOURCES += tsigerror.h tsigerror.cc
 libkea_dns___la_SOURCES += tsigkey.h tsigkey.cc
@@ -84,6 +85,7 @@ libdns___include_HEADERS = \
        rrttl.h \
        rrtype.h \
        serial.h \
+       time_utils.h \
        tsig.h \
        tsigerror.h \
        tsigkey.h \
index 2f69e035ba4baabe21cf3cfea9d5ab9682a3b978..9eeca2e69da4ca1f52f00a934855376cdbdac228 100644 (file)
@@ -14,6 +14,7 @@
 #include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrtype.h>
+#include <dns/time_utils.h>
 
 #include <gtest/gtest.h>
 
index 59d6b3bc2e3a62f38d693ee55358d2690db6183f..29e1c54d83c0c741a0a8cc4a63931708cfdb6b0f 100644 (file)
@@ -17,6 +17,7 @@
 #include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrtype.h>
+#include <dns/time_utils.h>
 #include <dns/tsigerror.h>
 
 #include <gtest/gtest.h>
diff --git a/src/lib/dns/time_utils.cc b/src/lib/dns/time_utils.cc
new file mode 100644 (file)
index 0000000..3af6149
--- /dev/null
@@ -0,0 +1,203 @@
+// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <stdint.h>
+
+#include <sys/time.h>
+
+#include <string>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+
+#include <stdio.h>
+#include <time.h>
+
+#include <exceptions/exceptions.h>
+
+#include <dns/time_utils.h>
+
+using namespace std;
+
+namespace {
+int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+
+inline bool
+isLeap(const int y) {
+    return ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0);
+}
+
+unsigned int
+yearSecs(const int year) {
+    return ((isLeap(year) ? 366 : 365 ) * 86400);
+}
+
+unsigned int
+monthSecs(const int month, const int year) {
+    return ((days[month] + ((month == 1 && isLeap(year)) ? 1 : 0 )) * 86400);
+}
+}
+
+namespace isc {
+namespace util {
+
+string
+timeToText64(uint64_t value) {
+    struct tm tm;
+    unsigned int secs;
+
+    // We cannot rely on gmtime() because time_t may not be of 64 bit
+    // integer.  The following conversion logic is borrowed from BIND 9.
+    tm.tm_year = 70;
+    while ((secs = yearSecs(tm.tm_year + 1900)) <= value) {
+        value -= secs;
+        ++tm.tm_year;
+        if (tm.tm_year + 1900 > 9999) {
+            isc_throw(InvalidTime,
+                      "Time value out of range (year > 9999): " <<
+                      tm.tm_year + 1900);
+        }
+    }
+    tm.tm_mon = 0;
+    while ((secs = monthSecs(tm.tm_mon, tm.tm_year + 1900)) <= value) {
+        value -= secs;
+        tm.tm_mon++;
+    }
+    tm.tm_mday = 1;
+    while (86400 <= value) {
+        value -= 86400;
+        ++tm.tm_mday;
+    }
+    tm.tm_hour = 0;
+    while (3600 <= value) {
+        value -= 3600;
+        ++tm.tm_hour;
+    }
+    tm.tm_min = 0;
+    while (60 <= value) {
+        value -= 60;
+        ++tm.tm_min;
+    }
+    tm.tm_sec = value;    // now t < 60, so this substitution is safe.
+
+    ostringstream oss;
+    oss << setfill('0')
+        << setw(4) << tm.tm_year + 1900
+        << setw(2) << tm.tm_mon + 1
+        << setw(2) << tm.tm_mday
+        << setw(2) << tm.tm_hour
+        << setw(2) << tm.tm_min
+        << setw(2) << tm.tm_sec;
+    return (oss.str());
+}
+
+// timeToText32() below uses the current system time.  To test it with
+// unusual current time values we introduce the following function pointer;
+// when it's non NULL, we call it to get the (normally faked) current time.
+// Otherwise we use the standard gettimeofday(2).  This hook is specifically
+// intended for testing purposes, so, even if it's visible outside of this
+// library, it's not even declared in a header file.
+namespace detail {
+int64_t (*getTimeFunction)() = NULL;
+
+int64_t
+getTimeWrapper() {
+    if (getTimeFunction != NULL) {
+        return (getTimeFunction());
+    }
+
+    struct timeval now;
+    gettimeofday(&now, NULL);
+
+    return (static_cast<int64_t>(now.tv_sec));
+}
+}
+
+string
+timeToText32(const uint32_t value) {
+    // We first adjust the time to the closest epoch based on the current time.
+    // Note that the following variables must be signed in order to handle
+    // time until year 2038 correctly.
+    const int64_t start = detail::getTimeWrapper() - 0x7fffffff;
+    int64_t base = 0;
+    int64_t t;
+    while ((t = (base + value)) < start) {
+        base += 0x100000000LL;
+    }
+
+    // Then convert it to text.
+    return (timeToText64(t));
+}
+
+namespace {
+const size_t DATE_LEN = 14;      // YYYYMMDDHHmmSS
+
+inline uint64_t ull(const int c) { return (static_cast<uint64_t>(c)); }
+
+inline void
+checkRange(const unsigned min, const unsigned max, const unsigned value,
+           const string& valname)
+{
+    if ((value >= min) && (value <= max)) {
+        return;
+    }
+    isc_throw(InvalidTime, "Invalid " << valname << " value: " << value);
+}
+}
+
+uint64_t
+timeFromText64(const string& time_txt) {
+    // Confirm the source only consists digits.  sscanf() allows some
+    // minor exceptions.
+    for (string::size_type i = 0; i < time_txt.length(); ++i) {
+        if (!isdigit(time_txt.at(i))) {
+            isc_throw(InvalidTime, "Couldn't convert non-numeric time value: "
+                      << time_txt);
+        }
+    }
+
+    unsigned year, month, day, hour, minute, second;
+    if (time_txt.length() != DATE_LEN ||
+        sscanf(time_txt.c_str(), "%4u%2u%2u%2u%2u%2u",
+               &year, &month, &day, &hour, &minute, &second) != 6)
+    {
+        isc_throw(InvalidTime, "Couldn't convert time value: " << time_txt);
+    }
+
+    checkRange(1970, 9999, year, "year");
+    checkRange(1, 12, month, "month");
+    checkRange(1, days[month - 1] + ((month == 2 && isLeap(year)) ? 1 : 0),
+            day, "day");
+    checkRange(0, 23, hour, "hour");
+    checkRange(0, 59, minute, "minute");
+    checkRange(0, 60, second, "second"); // 60 == leap second.
+
+    uint64_t timeval = second + (ull(60) * minute) + (ull(3600) * hour) +
+        ((day - 1) * ull(86400));
+    for (unsigned m = 0; m < (month - 1); ++m) {
+            timeval += days[m] * ull(86400);
+    }
+    if (isLeap(year) && month > 2) {
+            timeval += ull(86400);
+    }
+    for (unsigned y = 1970; y < year; ++y) {
+        timeval += ((isLeap(y) ? 366 : 365) * ull(86400));
+    }
+
+    return (timeval);
+}
+
+uint32_t
+timeFromText32(const string& time_txt) {
+    // The implicit conversion from uint64_t to uint32_t should just work here,
+    // because we only need to drop higher 32 bits.
+    return (timeFromText64(time_txt));
+}
+
+}
+}
diff --git a/src/lib/dns/time_utils.h b/src/lib/dns/time_utils.h
new file mode 100644 (file)
index 0000000..af61554
--- /dev/null
@@ -0,0 +1,166 @@
+// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef TIME_UTILS_H
+#define TIME_UTILS_H 1
+
+#include <exceptions/exceptions.h>
+
+#include <cstdint>
+#include <string>
+#include <sys/types.h>
+
+namespace isc {
+namespace util {
+
+/// @brief A standard DNS (or ISC) module exception that is thrown if
+/// a time conversion function encounters bad input.
+class InvalidTime : public Exception {
+public:
+    InvalidTime(const char* file, size_t line, const char* what)
+        : isc::Exception(file, line, what) {
+    }
+};
+
+namespace detail {
+
+/// @brief Return the current time in seconds.
+///
+/// This function returns the "current" time in seconds from epoch
+/// (00:00:00 January 1, 1970) as a 64-bit signed integer.  The return
+/// value can represent a point of time before epoch as a negative number.
+///
+/// This function is provided to help test time conscious implementations
+/// such as DNSSEC and TSIG signatures.  It is difficult to test them with
+/// an unusual or a specifically chosen "current" via system-provided
+/// library functions to get time.  This function acts as a straightforward
+/// wrapper of such a library function, but provides test code with a hook
+/// to return an arbitrary time value: if @c isc::util::detail::getTimeFunction
+/// is set to a pointer of function that returns 64-bit signed integer,
+/// @c getTimeWrapper() calls that function instead of the system library.
+///
+/// This hook variable is specifically intended for testing purposes, so,
+/// even if it's visible outside of this library, it's not even declared in a
+/// header file.
+///
+/// If the implementation doesn't need to be tested with faked current time,
+/// it should simply use the system supplied library function instead of
+/// this one.
+///
+/// @return current time in seconds
+int64_t
+getTimeWrapper();
+
+}  // namespace detail
+
+/// @name DNSSEC time conversion functions.
+///
+/// These functions convert between times represented in seconds (in integer)
+/// since epoch and those in the textual form used in the RRSIG records.
+/// For integers we provide both 32-bit and 64-bit versions.
+/// The RRSIG expiration and inception fields are both 32-bit unsigned
+/// integers, so 32-bit versions would be more useful for protocol operations.
+/// However, with 32-bit integers we need to take into account wrap-around
+/// points and compare values using the serial number arithmetic as specified
+/// in RFC4034, which would be more error prone.  We therefore provide 64-bit
+/// versions, too.
+///
+/// The timezone is always UTC for these functions.
+/// @{
+/// @brief Convert textual DNSSEC time to integer, 64-bit version.
+///
+/// The textual form must only consist of digits and be in the form of
+/// YYYYMMDDHHmmSS, where:
+/// - YYYY must be between 1970 and 9999
+/// - MM must be between 01 and 12
+/// - DD must be between 01 and 31 and must be a valid day for the month
+///   represented in 'MM'.  For example, if MM is 04, DD cannot be 31.
+///   DD can be 29 when MM is 02 only when YYYY is a leap year.
+/// - HH must be between 00 and 23
+/// - mm must be between 00 and 59
+/// - SS must be between 00 and 60
+///
+/// For all fields the range includes the begin and end values.  Note that
+/// 60 is allowed for 'SS', intending a leap second, although in real operation
+/// it's unlikely to be specified.
+///
+/// If the given text is valid, this function converts it to an unsigned
+/// 64-bit number of seconds since epoch (1 January 1970 00:00:00) and returns
+/// the converted value.  64 bits are sufficient to represent all possible
+/// values for the valid format uniquely, so there is no overflow.
+///
+/// @note RFC4034 also defines the textual form of an unsigned decimal integer
+/// for the corresponding time in seconds.  This function doesn't support
+/// this form, and if given it throws an exception of class @c InvalidTime.
+///
+/// @param time_txt Textual time in the form of YYYYMMDDHHmmSS
+///
+/// @return Seconds since epoch corresponding to @c time_txt
+///
+/// @throw InvalidTime The given textual representation is invalid.
+uint64_t
+timeFromText64(const std::string& time_txt);
+
+/// @brief Convert textual DNSSEC time to integer, 32-bit version.
+///
+/// This version is the same as @c timeFromText64() except that the return
+/// value is wrapped around to an unsigned 32-bit integer, simply dropping
+/// the upper 32 bits.
+///
+/// @param time_txt
+///
+/// @return
+uint32_t
+timeFromText32(const std::string& time_txt);
+
+/// @brief Convert integral DNSSEC time to textual form, 64-bit version.
+///
+/// This function takes an integer that would be seconds since epoch and
+/// converts it in the form of YYYYMMDDHHmmSS.  For example, if @c value is
+/// 0, it returns "19700101000000".  If the value corresponds to a point
+/// of time on and after year 10,000, which cannot be represented in the
+/// YYYY... form, an exception of class @c InvalidTime will be thrown.
+///
+/// @param value Seconds since epoch to be converted.
+/// @return Textual representation of @c value in the form of YYYYMMDDHHmmSS.
+///
+/// @throw InvalidTime The given time specifies on or after year 10,000.
+/// @throw Other A standard exception, if resource allocation for the
+/// returned text fails.
+std::string
+timeToText64(uint64_t value);
+
+/// @brief Convert integral DNSSEC time to textual form, 32-bit version.
+///
+/// This version is the same as @c timeToText64(), but the time value
+/// is expected to be the lower 32 bits of the full 64-bit value.
+/// These two will be different on and after a certain point of time
+/// in year 2106, so this function internally resolves the ambiguity
+/// using the current system time at the time of function call;
+/// it first identifies the range of [N*2^32 - 2^31, N*2^32 + 2^31)
+/// that contains the current time, and interprets @c value in the context
+/// of that range.  It then applies the same process as @c timeToText64().
+///
+/// There is one important exception in this processing, however.
+/// Until 19 Jan 2038 03:14:08 (2^31 seconds since epoch), this range
+/// would contain time before epoch.  In order to ensure the returned
+/// value is also a valid input to @c timeFromText, this function uses
+/// a special range [0, 2^32) until that time.  As a result, all upper
+/// half of the 32-bit values are treated as a future time.  For example,
+/// 2^32-1 (the highest value in 32-bit unsigned integers) will be converted
+/// to "21060207062815", instead of "19691231235959".
+///
+/// @param value
+///
+/// @return
+std::string
+timeToText32(uint32_t value);
+///@}
+
+}  // namespace util
+}  // namespace isc
+
+#endif  // TIME_UTILS_H
index b6659dd59609b953cdfb55ac6cb66147eab2432c..39b44e5a86a9441ce9d07c64a254143037ed7f03 100644 (file)
@@ -12,6 +12,7 @@
 #include <cryptolink/crypto_hmac.h>
 #include <dns/rdataclass.h>
 #include <dns/rrclass.h>
+#include <dns/time_utils.h>
 #include <dns/tsig.h>
 #include <dns/tsigerror.h>
 #include <dns/tsigkey.h>
@@ -37,14 +38,14 @@ namespace {
 typedef boost::shared_ptr<HMAC> HMACPtr;
 
 // TSIG uses 48-bit unsigned integer to represent time signed.
-// Since gettimeWrapper() returns a 64-bit *signed* integer, we
+// Since getTimeWrapper() returns a 64-bit *signed* integer, we
 // make sure it's stored in an unsigned 64-bit integer variable and
 // represents a value in the expected range.  (In reality, however,
-// gettimeWrapper() will return a positive integer that will fit
+// getTimeWrapper() will return a positive integer that will fit
 // in 48 bits)
 uint64_t
 getTSIGTime() {
-    return (detail::gettimeWrapper() & 0x0000ffffffffffffULL);
+    return (detail::getTimeWrapper() & 0x0000ffffffffffffULL);
 }
 }