]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add "time_since" xlat to rlm_date
authorMatthew Newton <matthew-git@newtoncomputing.co.uk>
Tue, 26 Sep 2023 13:31:43 +0000 (14:31 +0100)
committerMatthew Newton <matthew-git@newtoncomputing.co.uk>
Tue, 26 Sep 2023 21:57:41 +0000 (22:57 +0100)
Makes it much easier to calculate latencies in seconds,
milliseconds or microseconds, as well as getting time since
epoch in each of those bases as well.

doc/ChangeLog
raddb/mods-available/date
src/modules/rlm_date/rlm_date.c
src/tests/modules/date/all.mk [new file with mode: 0644]
src/tests/modules/date/date_xlat.attrs [new file with mode: 0644]
src/tests/modules/date/date_xlat.unlang [new file with mode: 0644]
src/tests/modules/date/module.conf [new file with mode: 0644]

index 71fc81a0d16cc9e6ee5d8065e9850d55f4bee0a4..cbecee08e13f20871b0c4e48cb522065c213f3d3 100644 (file)
@@ -16,6 +16,8 @@ FreeRADIUS 3.2.4 Fri 26 May 2023 12:00:00 EDT urgency=low
        * Update dictionary.aruba
        * Added "radsecret" program which generates strong secrets.  See the
          top of the "clients.conf" file for more information.
+       * Add "time_since" xlat to calculate elapsed time in seconds, milliseconds
+         and microseconds.
 
        Bug fixes
        * Fix corner case with empty defaults in rlm_files.  Fixes #5035
index 25a64da17d4953bddea2416f30498161b484d744..69d8f6ab9013df3ea0fb8a163d59aae14891dd59 100644 (file)
@@ -33,3 +33,48 @@ date wispr2date {
        #  default = no
 #      utc = yes
 }
+
+#
+#  The date module also provides the %{time_since:} xlat, which
+#  makes it possible to both:
+#    - get the time since the epoch in seconds, milliseconds or
+#      microseconds; and
+#    - calculate the time elapsed since a given time.
+#
+#  Syntax is: %{time_since:BASE[:(number|&attribute)]}
+#  where "BASE" is "s", "ms" or "us".
+#
+#  Examples:
+#    %{time_since:s}
+#        - time in seconds since the epoch, same as %c
+#
+#    %{time_since:s:1695753388}
+#        - time in seconds since Tue 26 Sep 19:36:28 BST 2023
+#          (which is 1695753388 in UNIX time)
+#
+#    %{time_since:s:&Tmp-Integer-0}
+#        - Time since the number of seconds in Tmp-Integer-0
+#
+#    %{time_since:ms}
+#        - Milliseconds since the epoch
+#
+#    %{time_since:us}
+#        - Microseconds since the epoch
+#
+#  The provided attribute should be an Integer (or Integer64 for
+#  ms or us bases). However, other attributes will be converted if
+#  possible, with a warning given. The only one that might make
+#  sense is a Date attribute (which will be scaled appropriately
+#  according to the base, as Date is always in seconds).
+#
+#  Primary usage would be for taking latenct measurements, for
+#  example to calculate the number of microseconds an LDAP call
+#  took:
+#
+#    update request {
+#      &Tmp-Integer64-0 := %{time_since:us}"
+#    }
+#    ldap
+#    update request {
+#      &Tmp-Integer64-1 := %{time_since:us:&Tmp-Integer64-0}"
+#    }
index 79191e73d87d17d3f30cabac27b2ca69ba5d5c0f..b9f42a5dfa7da2c82b9525ff8587368064140c7b 100644 (file)
  * @brief Translates timestrings between formats.
  *
  * @author Artur Malinowski <artur@wow.com>
+ * @author Matthew Newton
  *
  * @copyright 2013 Artur Malinowski <artur@wow.com>
- * @copyright 1999-2013 The FreeRADIUS Server Project.
+ * @copyright 1999-2023 The FreeRADIUS Server Project.
  */
 
 #include <freeradius-devel/radiusd.h>
@@ -116,6 +117,161 @@ static ssize_t xlat_date_convert(void *instance, REQUEST *request, char const *f
 }
 DIAG_ON(format-nonliteral)
 
+
+/** Get time in ms since either epoch or another value
+ *
+ *  %{time_since:s} - return seconds since epoch
+ *  %{time_since:ms:0} - return milliseconds since epoch
+ *  %{time_since:us:1695745763034443} - return microseconds since Tue 26 Sep 17:29:23.034443 BST 2023
+ *  %{time_since:us:&Tmp-Integer64-1} - return microseconds since value in &Tmp-Integer64-1
+ */
+
+static ssize_t xlat_time_since(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen)
+{
+       uint64_t time_now = 0;
+       uint64_t time_delta = 0;
+       uint64_t time_since = 0;
+       struct timeval tv;
+
+       enum timebase {
+               S = 1,
+               MS = 1000,
+               US = 1000000,
+       };
+       enum timebase time_base;
+
+       while (isspace((uint8_t) *fmt)) fmt++;
+
+       /*
+        *  Work out what time base we are using, s, ms or us.
+        */
+       if (fmt[0] == 'm' && fmt[1] == 's') {
+               time_base = MS;
+               fmt += 2;
+       } else if (fmt[0] == 'u' && fmt[1] == 's') {
+               time_base = US;
+               fmt += 2;
+       } else if (fmt[0] == 's') {
+               time_base = S;
+               fmt++;
+       } else {
+               REDEBUG("Time base (ms, us, s) missing in time_since xlat");
+       error:
+               *out = '\0';
+               return -1;
+       }
+
+       if (fmt[0] != '\0' && fmt[0] != ':') {
+               REDEBUG("Invalid arguments passed to time_since xlat");
+               goto error;
+       }
+
+       if (fmt[0] == ':') fmt++;
+
+       /*
+        *  Handle the different formats that we can be passed
+        */
+       if (fmt[0] == '\0') {
+               /*
+                *  %{time_since:[mu]?s:} - epoch
+                */
+               time_since = 0;
+
+       } else if (fmt[0] == '&') {
+               /*
+                *  We were provided with an attribute
+                *
+                *  %{time_since:[mu]?s:&Attr-Name}
+                */
+               value_data_t outnum;
+               VALUE_PAIR *vp;
+
+               if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) {
+                       REDEBUG("Unable to parse attribute in time_ms_since xlat");
+                       goto error;
+               }
+
+               if (vp->da->type == PW_TYPE_INTEGER64) {
+                       /*
+                        *  Int64 is easy
+                        */
+                       time_since = vp->vp_integer64;
+               } else {
+                       /*
+                        *  ...but not others - try and convert, but warn it's likely nonsensical.
+                        */
+                       if (value_data_cast(request, &outnum,
+                                       PW_TYPE_INTEGER64, NULL, vp->da->type, NULL,
+                                       &vp->data, vp->vp_length) < 0) {
+                               REDEBUG("Unable to convert %s to integer", fmt);
+                               goto error;
+                       }
+                       if (vp->da->type == PW_TYPE_DATE) {
+                               /*
+                                *  Special case a Date - we know it's seconds
+                                */
+                               RDEBUG3("Attribute \"%s\" is a date; multiplying seconds by %d", fmt, time_base);
+                               time_since = outnum.integer64 * time_base;
+                       } else {
+                               RWDEBUG("Attribute \"%s\" is not integer, conversion may not make sense", fmt);
+                               time_since = outnum.integer64;
+                       }
+               }
+
+       } else if (fmt[0] == '-') {
+               REDEBUG("time_since xlat only accepts positive integers");
+               goto error;
+
+       } else {
+               /*
+                *  Otherwise we hope we were provided with an integer value
+                *
+                *  %{time_since:[mu]?s:12345}
+                */
+               if (sscanf(fmt, "%" PRIu64, &time_since) != 1) {
+                       REDEBUG("Failed parsing \"%s\" as integer", fmt);
+                       goto error;
+               }
+       }
+
+       /*
+        *  Get current time and add milli/micro component if needed
+        */
+       gettimeofday(&tv, NULL);
+
+       time_now = (uint64_t)tv.tv_sec * time_base;
+
+       if (time_base == MS) {
+               time_now += (uint64_t)tv.tv_usec / 1000;
+       } else if (time_base == US) {
+               time_now += (uint64_t)tv.tv_usec;
+       }
+
+       /*
+        *  time_since needs to be in the past
+        */
+       if (time_since > time_now) {
+               REDEBUG("time provided to time_since needs to be in the past");
+               goto error;
+       }
+
+       /*
+        *  Calculate time since provided value
+        */
+       time_delta = time_now - time_since;
+
+       /*
+        *  Write out and return
+        */
+       if ((size_t)snprintf(out, outlen, "%" PRIu64, time_delta) >= outlen) {
+               REDEBUG("Insufficient space to write 64-bit time value");
+               goto error;
+       }
+
+       return 0;
+}
+
+
 static int mod_bootstrap(CONF_SECTION *conf, void *instance)
 {
        rlm_date_t *inst = instance;
@@ -126,6 +282,7 @@ static int mod_bootstrap(CONF_SECTION *conf, void *instance)
        }
 
        xlat_register(inst->xlat_name, xlat_date_convert, NULL, inst);
+       xlat_register("time_since", xlat_time_since, NULL, inst);
 
        return 0;
 }
diff --git a/src/tests/modules/date/all.mk b/src/tests/modules/date/all.mk
new file mode 100644 (file)
index 0000000..90966df
--- /dev/null
@@ -0,0 +1,3 @@
+#
+# Test the "date" module
+#
diff --git a/src/tests/modules/date/date_xlat.attrs b/src/tests/modules/date/date_xlat.attrs
new file mode 100644 (file)
index 0000000..a75e2b8
--- /dev/null
@@ -0,0 +1,13 @@
+#
+#  Input packet
+#
+Packet-Type = Access-Request
+User-Name = 'Bob'
+User-Password = 'Alice'
+Tmp-Integer-1 = "12345"
+
+#
+#  Expected answer
+#
+Response-Packet-Type == Access-Accept
+
diff --git a/src/tests/modules/date/date_xlat.unlang b/src/tests/modules/date/date_xlat.unlang
new file mode 100644 (file)
index 0000000..8f09a72
--- /dev/null
@@ -0,0 +1,207 @@
+#
+#  Selection of tests for the %{time_since:} xlat
+#
+#  Somewhat limited in what we can do here, as it bases its
+#  responses off the current system time. So we need to do some
+#  comparisons rather than actual value checks.
+#
+
+#
+#  %c and %{time_since:s:0} should match
+#
+update {
+       &Tmp-Integer-9 := 0
+}
+
+update {
+       &Tmp-Integer-0 := "%c"
+       &Tmp-Integer-1 := "%{time_since:s:0}"
+       &Tmp-Integer-2 := "%{time_since:s:&Tmp-Integer-9}"
+}
+
+if (&Tmp-Integer-0 != &Tmp-Integer-1) {
+       if (&Tmp-Integer-0 != "%{expr:&Tmp-Integer-1 - 1}") {
+       # at a push, %{time_since:s:0} might be one second later,
+       # depending on when the test ran
+               test_fail
+       }
+}
+
+if (&Tmp-Integer-1 != &Tmp-Integer-2) {
+       if (&Tmp-Integer-1 != "%{expr:&Tmp-Integer-2 - 1}") {
+               test_fail
+       }
+}
+
+#
+#  If we run time_since 3 times, they should be the same or increasing
+#
+update {
+       &Tmp-Integer64-0 := "%{time_since:s:0}"
+}
+
+update {
+       &Tmp-Integer64-1 := "%{time_since:s:}"
+}
+
+update {
+       &Tmp-Integer64-2 := "%{time_since:s}"
+}
+
+if (&Tmp-Integer64-0 > &Tmp-Integer64-1 || \
+       &Tmp-Integer64-1 > &Tmp-Integer64-2 || \
+       &Tmp-Integer64-0 > &Tmp-Integer64-2) {
+       test_fail
+}
+
+#
+#  It's way past the year 2020, so this should only fail if the
+#  computer's clock is very wrong...
+#
+if (&Tmp-Integer64-0 < 1600000000) {
+       test_fail
+}
+
+
+#
+#  Similar for milliseconds
+#
+update {
+       &Tmp-Integer64-3 := "%{time_since:ms:0}"
+}
+
+update {
+       &Tmp-Integer64-4 := "%{time_since:ms:}"
+}
+
+update {
+       &Tmp-Integer64-5 := "%{time_since:ms}"
+}
+
+if (&Tmp-Integer64-3 > &Tmp-Integer64-4 || \
+       &Tmp-Integer64-4 > &Tmp-Integer64-5 || \
+       &Tmp-Integer64-3 > &Tmp-Integer64-5) {
+       test_fail
+}
+
+
+#
+#  ...and microseconds
+#
+update {
+       &Tmp-Integer64-6 := "%{time_since:us:0}"
+}
+
+update {
+       &Tmp-Integer64-7 := "%{time_since:us:}"
+}
+
+update {
+       &Tmp-Integer64-8 := "%{time_since:us}"
+}
+
+if (&Tmp-Integer64-6 > &Tmp-Integer64-7 || \
+       &Tmp-Integer64-7 > &Tmp-Integer64-8 || \
+       &Tmp-Integer64-6 > &Tmp-Integer64-8) {
+       test_fail
+}
+
+if ("%{expr:&Tmp-Integer64-7 - &Tmp-Integer64-6}" > 250) {
+       # you have a really slow computer if the time between
+       # getting these took more than 250us
+       test_fail
+}
+
+
+#
+#  Seconds component * 1000 must always be same or less than
+#  milliseconds, and microseconds.
+#
+if ("%{expr:%{time_since:s:0} * 1000}" > "%{time_since:ms:0}") {
+       test_fail
+}
+
+if ("%{expr:%{time_since:ms:0} * 1000}" > "%{time_since:us:0}") {
+       test_fail
+}
+
+if ("%{expr:%{time_since:s:0} * 1000000}" > "%{time_since:us:0}") {
+       test_fail
+}
+
+
+#
+#  Test for some errors
+#
+
+#  missing time base
+update {
+       &Tmp-Integer-0 := "%{time_since:}"
+}
+
+if (!(&Module-Failure-Message[*] == 'Time base (ms, us, s) missing in time_since xlat')) {
+       test_fail
+}
+
+update {
+       &Module-Failure-Message !* ANY
+}
+
+
+#  invalid time base
+update {
+       &Tmp-Integer-0 := "%{time_since:bob}"
+}
+
+if (!(&Module-Failure-Message[*] == 'Time base (ms, us, s) missing in time_since xlat')) {
+       test_fail
+}
+
+update {
+       &Module-Failure-Message !* ANY
+}
+
+
+# negative values
+update {
+       &Tmp-Integer-0 := "%{time_since:ms:-1234}"
+}
+
+if (!(&Module-Failure-Message[*] == 'time_since xlat only accepts positive integers')) {
+       test_fail
+}
+
+update {
+       &Module-Failure-Message !* ANY
+}
+
+
+#  invalid attribute
+update {
+       &Tmp-Integer-0 := "%{time_since:us:&Test-Non-Existant-Attr}"
+}
+
+if (!(&Module-Failure-Message[*] == 'Unable to parse attribute in time_ms_since xlat')) {
+       test_fail
+}
+
+update {
+       &Module-Failure-Message !* ANY
+}
+
+
+#  silly text
+update {
+       &Tmp-Integer-0 := "%{time_since:us:test random text}"
+}
+
+if (!(&Module-Failure-Message[*] == 'Failed parsing "test random text" as integer')) {
+       test_fail
+}
+
+update {
+       &Module-Failure-Message !* ANY
+}
+
+
+test_pass
diff --git a/src/tests/modules/date/module.conf b/src/tests/modules/date/module.conf
new file mode 100644 (file)
index 0000000..cb7ef07
--- /dev/null
@@ -0,0 +1,3 @@
+#date unit test config
+date {
+}