From 1b4f1d0a622384495ba5529e60822b02b9f2303a Mon Sep 17 00:00:00 2001 From: Matthew Newton Date: Tue, 26 Sep 2023 14:31:43 +0100 Subject: [PATCH] add "time_since" xlat to rlm_date 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 | 2 + raddb/mods-available/date | 45 ++++++ src/modules/rlm_date/rlm_date.c | 159 +++++++++++++++++- src/tests/modules/date/all.mk | 3 + src/tests/modules/date/date_xlat.attrs | 13 ++ src/tests/modules/date/date_xlat.unlang | 207 ++++++++++++++++++++++++ src/tests/modules/date/module.conf | 3 + 7 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 src/tests/modules/date/all.mk create mode 100644 src/tests/modules/date/date_xlat.attrs create mode 100644 src/tests/modules/date/date_xlat.unlang create mode 100644 src/tests/modules/date/module.conf diff --git a/doc/ChangeLog b/doc/ChangeLog index 71fc81a0d1..cbecee08e1 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -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 diff --git a/raddb/mods-available/date b/raddb/mods-available/date index 25a64da17d..69d8f6ab90 100644 --- a/raddb/mods-available/date +++ b/raddb/mods-available/date @@ -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}" +# } diff --git a/src/modules/rlm_date/rlm_date.c b/src/modules/rlm_date/rlm_date.c index 79191e73d8..b9f42a5dfa 100644 --- a/src/modules/rlm_date/rlm_date.c +++ b/src/modules/rlm_date/rlm_date.c @@ -19,9 +19,10 @@ * @brief Translates timestrings between formats. * * @author Artur Malinowski + * @author Matthew Newton * * @copyright 2013 Artur Malinowski - * @copyright 1999-2013 The FreeRADIUS Server Project. + * @copyright 1999-2023 The FreeRADIUS Server Project. */ #include @@ -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 index 0000000000..90966df2a8 --- /dev/null +++ b/src/tests/modules/date/all.mk @@ -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 index 0000000000..a75e2b84fa --- /dev/null +++ b/src/tests/modules/date/date_xlat.attrs @@ -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 index 0000000000..8f09a72cda --- /dev/null +++ b/src/tests/modules/date/date_xlat.unlang @@ -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 index 0000000000..cb7ef07d86 --- /dev/null +++ b/src/tests/modules/date/module.conf @@ -0,0 +1,3 @@ +#date unit test config +date { +} -- 2.47.3