]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
add crontab functionality.
authorAlan T. DeKok <aland@freeradius.org>
Wed, 15 Sep 2021 18:18:01 +0000 (14:18 -0400)
committerAlan T. DeKok <aland@freeradius.org>
Wed, 15 Sep 2021 18:18:52 +0000 (14:18 -0400)
much of proto_cron.c is just a copy of proto_load.c
So we might want to have a generic "timer" front end, and then
have it do timer "load" or timer "crontab"

src/listen/cron/all.mk [new file with mode: 0644]
src/listen/cron/proto_cron.c [new file with mode: 0644]
src/listen/cron/proto_cron.h [new file with mode: 0644]
src/listen/cron/proto_cron.mk [new file with mode: 0644]
src/listen/cron/proto_cron_crontab.c [new file with mode: 0644]
src/listen/cron/proto_cron_crontab.mk [new file with mode: 0644]

diff --git a/src/listen/cron/all.mk b/src/listen/cron/all.mk
new file mode 100644 (file)
index 0000000..113c216
--- /dev/null
@@ -0,0 +1,3 @@
+SUBMAKEFILES := \
+       proto_cron.mk \
+       proto_cron_crontab.mk \
diff --git a/src/listen/cron/proto_cron.c b/src/listen/cron/proto_cron.c
new file mode 100644 (file)
index 0000000..bda8317
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file proto_cron.c
+ * @brief Load master protocol handler.
+ *
+ * @copyright 2017 Arran Cudbard-Bell (a.cudbardb@freeradius.org)
+ * @copyright 2016 Alan DeKok (aland@freeradius.org)
+ */
+#include <freeradius-devel/io/application.h>
+#include <freeradius-devel/io/listen.h>
+#include <freeradius-devel/io/schedule.h>
+#include <freeradius-devel/radius/radius.h>
+#include <freeradius-devel/server/base.h>
+#include <freeradius-devel/server/virtual_servers.h>
+#include <freeradius-devel/util/debug.h>
+
+#include "proto_cron.h"
+
+extern fr_app_t proto_cron;
+static int type_parse(TALLOC_CTX *ctx, void *out, UNUSED void *parent, CONF_ITEM *ci, CONF_PARSER const *rule);
+static int transport_parse(TALLOC_CTX *ctx, void *out, UNUSED void *parent, CONF_ITEM *ci, CONF_PARSER const *rule);
+
+/** How to parse a Load listen section
+ *
+ */
+static CONF_PARSER const proto_cron_config[] = {
+       { FR_CONF_OFFSET("type", FR_TYPE_VOID | FR_TYPE_NOT_EMPTY | FR_TYPE_REQUIRED, proto_cron_t,
+                         type), .func = type_parse },
+       { FR_CONF_OFFSET("transport", FR_TYPE_VOID, proto_cron_t, io.submodule),
+         .func = transport_parse, .dflt = "crontab" },
+
+       /*
+        *      Add this as a synonym so normal humans can understand it.
+        */
+       { FR_CONF_OFFSET("max_entry_size", FR_TYPE_UINT32, proto_cron_t, max_packet_size) } ,
+
+       /*
+        *      For performance tweaking.  NOT for normal humans.
+        */
+       { FR_CONF_OFFSET("max_packet_size", FR_TYPE_UINT32, proto_cron_t, max_packet_size) } ,
+       { FR_CONF_OFFSET("num_messages", FR_TYPE_UINT32, proto_cron_t, num_messages) } ,
+
+       { FR_CONF_OFFSET("priority", FR_TYPE_UINT32, proto_cron_t, priority) },
+
+       CONF_PARSER_TERMINATOR
+};
+
+static fr_dict_t const *dict_freeradius;
+
+extern fr_dict_autoload_t proto_cron_dict[];
+fr_dict_autoload_t proto_cron_dict[] = {
+       { .out = &dict_freeradius, .proto = "freeradius" },
+
+       { NULL }
+};
+
+static fr_dict_attr_t const *attr_packet_dst_ip_address;
+static fr_dict_attr_t const *attr_packet_dst_ipv6_address;
+static fr_dict_attr_t const *attr_packet_dst_port;
+static fr_dict_attr_t const *attr_packet_original_timestamp;
+static fr_dict_attr_t const *attr_packet_src_ip_address;
+static fr_dict_attr_t const *attr_packet_src_ipv6_address;
+static fr_dict_attr_t const *attr_packet_src_port;
+static fr_dict_attr_t const *attr_protocol;
+
+extern fr_dict_attr_autoload_t proto_cron_dict_attr[];
+fr_dict_attr_autoload_t proto_cron_dict_attr[] = {
+       { .out = &attr_packet_dst_ip_address, .name = "Packet-Dst-IP-Address", .type = FR_TYPE_IPV4_ADDR, .dict = &dict_freeradius },
+       { .out = &attr_packet_dst_ipv6_address, .name = "Packet-Dst-IPv6-Address", .type = FR_TYPE_IPV6_ADDR, .dict = &dict_freeradius },
+       { .out = &attr_packet_dst_port, .name = "Packet-Dst-Port", .type = FR_TYPE_UINT16, .dict = &dict_freeradius },
+       { .out = &attr_packet_original_timestamp, .name = "Packet-Original-Timestamp", .type = FR_TYPE_DATE, .dict = &dict_freeradius },
+       { .out = &attr_packet_src_ip_address, .name = "Packet-Src-IP-Address", .type = FR_TYPE_IPV4_ADDR, .dict = &dict_freeradius },
+       { .out = &attr_packet_src_ipv6_address, .name = "Packet-Src-IPv6-Address", .type = FR_TYPE_IPV6_ADDR, .dict = &dict_freeradius },
+       { .out = &attr_packet_src_port, .name = "Packet-Src-Port", .type = FR_TYPE_UINT16, .dict = &dict_freeradius },
+       { .out = &attr_protocol, .name = "Protocol", .type = FR_TYPE_UINT32, .dict = &dict_freeradius },
+
+       { NULL }
+};
+
+/** Wrapper around dl_instance which translates the packet-type into a submodule name
+ *
+ * @param[in] ctx      to allocate data in (instance of proto_cron).
+ * @param[out] out     Where to write a dl_module_inst_t containing the module handle and instance.
+ * @param[in] parent   Base structure address.
+ * @param[in] ci       #CONF_PAIR specifying the name of the type module.
+ * @param[in] rule     unused.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ */
+static int type_parse(UNUSED TALLOC_CTX *ctx, void *out, void *parent, CONF_ITEM *ci, UNUSED CONF_PARSER const *rule)
+{
+       proto_cron_t            *inst = talloc_get_type_abort(parent, proto_cron_t);
+       fr_dict_enum_t const    *type_enum;
+       CONF_PAIR               *cp = cf_item_to_pair(ci);
+       char const              *value = cf_pair_value(cp);
+
+       *((char const **) out) = value;
+
+       inst->dict = virtual_server_namespace_by_ci(ci);
+       if (!inst->dict) {
+               cf_log_err(ci, "Please define 'namespace' in this virtual server");
+               return -1;
+       }
+
+       inst->attr_packet_type = fr_dict_attr_by_name(NULL, fr_dict_root(inst->dict), "Packet-Type");
+       if (!inst->attr_packet_type) {
+               cf_log_err(ci, "Failed to find 'Packet-Type' attribute");
+               return -1;
+       }
+
+       if (!value) {
+               cf_log_err(ci, "No value given for 'type'");
+               return -1;
+       }
+
+       type_enum = fr_dict_enum_by_name(inst->attr_packet_type, value, -1);
+       if (!type_enum) {
+               cf_log_err(ci, "Invalid type \"%s\"", value);
+               return -1;
+       }
+
+       inst->code = type_enum->value->vb_uint32;
+       return 0;
+}
+
+/** Wrapper around dl_instance
+ *
+ * @param[in] ctx      to allocate data in (instance of proto_cron).
+ * @param[out] out     Where to write a dl_module_inst_t containing the module handle and instance.
+ * @param[in] parent   Base structure address.
+ * @param[in] ci       #CONF_PAIR specifying the name of the type module.
+ * @param[in] rule     unused.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ */
+static int transport_parse(TALLOC_CTX *ctx, void *out, UNUSED void *parent,
+                          CONF_ITEM *ci, UNUSED CONF_PARSER const *rule)
+{
+       char const      *name = cf_pair_value(cf_item_to_pair(ci));
+       dl_module_inst_t        *parent_inst;
+       CONF_SECTION    *listen_cs = cf_item_to_section(cf_parent(ci));
+       CONF_SECTION    *transport_cs;
+
+       transport_cs = cf_section_find(listen_cs, name, NULL);
+
+       /*
+        *      Allocate an empty section if one doesn't exist
+        *      this is so defaults get parsed.
+        */
+       if (!transport_cs) transport_cs = cf_section_alloc(listen_cs, listen_cs, name, NULL);
+
+       parent_inst = cf_data_value(cf_data_find(listen_cs, dl_module_inst_t, "proto_cron"));
+       fr_assert(parent_inst);
+
+       return dl_module_instance(ctx, out, transport_cs, parent_inst, name, DL_MODULE_TYPE_SUBMODULE);
+}
+
+/** Decode the packet, and set the request->process function
+ *
+ */
+static int mod_decode(void const *instance, request_t *request, uint8_t *const data, size_t data_len)
+{
+       proto_cron_t const      *inst = talloc_get_type_abort_const(instance, proto_cron_t);
+
+       request->dict = inst->dict;
+       request->packet->code = inst->code;
+
+       /*
+        *      Set default addresses
+        */
+       request->packet->socket.fd = -1;
+       request->packet->socket.inet.src_ipaddr.af = AF_INET;
+       request->packet->socket.inet.src_ipaddr.addr.v4.s_addr = htonl(INADDR_NONE);
+       request->packet->socket.inet.dst_ipaddr = request->packet->socket.inet.src_ipaddr;
+
+       request->reply->socket.inet.src_ipaddr = request->packet->socket.inet.src_ipaddr;
+       request->reply->socket.inet.dst_ipaddr = request->packet->socket.inet.src_ipaddr;
+
+       /*
+        *      The app_io is responsible for decoding all of the data.
+        */
+       return inst->io.app_io->decode(inst->io.app_io_instance, request, data, data_len);
+}
+
+/*
+ *     We don't need to encode any of the replies.  We just go "yeah, it's fine".
+ */
+static ssize_t mod_encode(UNUSED void const *instance, request_t *request, uint8_t *buffer, size_t buffer_len)
+{
+       if (buffer_len < 1) return -1;
+
+       *buffer = request->reply->code;
+       return 1;
+}
+
+/** Open listen sockets/connect to external event source
+ *
+ * @param[in] instance Ctx data for this application.
+ * @param[in] sc       to add our file descriptor to.
+ * @param[in] conf     Listen section parsed to give us isntance.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ */
+static int mod_open(void *instance, fr_schedule_t *sc, UNUSED CONF_SECTION *conf)
+{
+       proto_cron_t    *inst = talloc_get_type_abort(instance, proto_cron_t);
+
+       inst->io.app = &proto_cron;
+       inst->io.app_instance = instance;
+
+       /*
+        *      io.app_io should already be set
+        */
+       return fr_master_io_listen(inst, &inst->io, sc,
+                                  inst->max_packet_size, inst->num_messages);
+}
+
+
+/** Instantiate the application
+ *
+ * Instantiate I/O and type submodules.
+ *
+ * @param[in] instance Ctx data for this application.
+ * @param[in] conf     Listen section parsed to give us isntance.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ */
+static int mod_instantiate(void *instance, CONF_SECTION *conf)
+{
+       proto_cron_t            *inst = talloc_get_type_abort(instance, proto_cron_t);
+
+       fr_assert(inst->io.submodule);
+
+       /*
+        *      These configuration items are not printed by default,
+        *      because normal people shouldn't be touching them.
+        */
+       if (!inst->max_packet_size && inst->io.app_io) inst->max_packet_size = inst->io.app_io->default_message_size;
+
+       if (!inst->num_messages) inst->num_messages = 256;
+
+       FR_INTEGER_BOUND_CHECK("num_messages", inst->num_messages, >=, 32);
+       FR_INTEGER_BOUND_CHECK("num_messages", inst->num_messages, <=, 65535);
+
+       FR_INTEGER_BOUND_CHECK("max_packet_size", inst->max_packet_size, >=, 1024);
+       FR_INTEGER_BOUND_CHECK("max_packet_size", inst->max_packet_size, <=, 65535);
+
+       /*
+        *      Instantiate the master io submodule
+        */
+       return fr_master_app_io.instantiate(&inst->io, conf);
+}
+
+/** Bootstrap the application
+ *
+ * Bootstrap I/O and type submodules.
+ *
+ * @param[in] instance Ctx data for this application.
+ * @param[in] conf     Listen section parsed to give us instance.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ */
+static int mod_bootstrap(void *instance, CONF_SECTION *conf)
+{
+       proto_cron_t            *inst = talloc_get_type_abort(instance, proto_cron_t);
+
+       /*
+        *      Ensure that the server CONF_SECTION is always set.
+        */
+       inst->io.server_cs = cf_item_to_section(cf_parent(conf));
+
+       /*
+        *      No IO module, it's an empty listener.
+        */
+       if (!inst->io.submodule) {
+               cf_log_err(conf, "The load generator MUST have a 'transport = ...' set");
+               return -1;
+       }
+
+       /*
+        *      Tell the master handler about the main protocol instance.
+        */
+       inst->io.app = &proto_cron;
+       inst->io.app_instance = inst;
+
+       /*
+        *      The listener is inside of a virtual server.
+        */
+       inst->server_cs = cf_item_to_section(cf_parent(conf));
+       inst->cs = conf;
+       inst->self = &proto_cron;
+
+       /*
+        *      We will need this for dynamic clients and connected sockets.
+        */
+       inst->io.dl_inst = dl_module_instance_by_data(inst);
+       fr_assert(inst != NULL);
+
+       /*
+        *      Bootstrap the master IO handler.
+        */
+       return fr_master_app_io.bootstrap(&inst->io, conf);
+}
+
+
+fr_app_t proto_cron = {
+       .magic                  = RLM_MODULE_INIT,
+       .name                   = "cron",
+       .config                 = proto_cron_config,
+       .inst_size              = sizeof(proto_cron_t),
+
+       .bootstrap              = mod_bootstrap,
+       .instantiate            = mod_instantiate,
+       .open                   = mod_open,
+       .decode                 = mod_decode,
+       .encode                 = mod_encode,
+};
diff --git a/src/listen/cron/proto_cron.h b/src/listen/cron/proto_cron.h
new file mode 100644 (file)
index 0000000..71137f6
--- /dev/null
@@ -0,0 +1,56 @@
+#pragma once
+/*
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more loads.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file proto_cron.h
+ * @brief Cron master protocol handler.
+ *
+ * @copyright 2021 Network RADIUS SAS (legal@networkradius.com)
+ */
+RCSIDH(proto_cron_h, "$Id$")
+
+#include <freeradius-devel/server/module.h>
+#include <freeradius-devel/io/master.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+       fr_io_instance_t                io;                             //!< wrapper for IO abstraction
+
+       CONF_SECTION                    *server_cs;                     //!< server CS for this listener
+       CONF_SECTION                    *cs;                            //!< my configuration
+       fr_app_t                        *self;                          //!< child / parent linking issues
+       char const                      *type;                          //!< packet type name
+
+       fr_dict_t const                 *dict;                          //!< root dictionary
+       fr_dict_attr_t const            *attr_packet_type;
+
+       uint32_t                        code;                           //!< packet code to use for incoming packets
+       uint32_t                        max_packet_size;                //!< for message ring buffer
+       uint32_t                        num_messages;                   //!< for message ring buffer
+       uint32_t                        priority;                       //!< for packet processing, larger == higher
+} proto_cron_t;
+
+#include <pthread.h>
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/listen/cron/proto_cron.mk b/src/listen/cron/proto_cron.mk
new file mode 100644 (file)
index 0000000..10ebde9
--- /dev/null
@@ -0,0 +1,9 @@
+TARGETNAME     := proto_cron
+
+ifneq "$(TARGETNAME)" ""
+TARGET         := $(TARGETNAME).a
+endif
+
+SOURCES                := proto_cron.c
+
+TGT_PREREQS    := $(LIBFREERADIUS_SERVER) libfreeradius-io.a
diff --git a/src/listen/cron/proto_cron_crontab.c b/src/listen/cron/proto_cron_crontab.c
new file mode 100644 (file)
index 0000000..6d7cae6
--- /dev/null
@@ -0,0 +1,706 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file proto_cron_crontab.c
+ * @brief Generate crontab events.
+ *
+ * @copyright 2021 Network RADIUS SAS (legal@networkradius.com)
+ */
+#include <netdb.h>
+#include <fcntl.h>
+#include <freeradius-devel/server/base.h>
+#include <freeradius-devel/server/protocol.h>
+#include <freeradius-devel/io/base.h>
+#include <freeradius-devel/io/application.h>
+#include <freeradius-devel/io/listen.h>
+#include <freeradius-devel/io/schedule.h>
+#include <freeradius-devel/util/debug.h>
+
+#include "proto_cron.h"
+
+extern fr_app_io_t proto_cron_crontab;
+
+typedef struct proto_cron_tab_s proto_cron_crontab_t;
+
+typedef struct {
+       fr_event_list_t                 *el;                    //!< event list
+       fr_network_t                    *nr;                    //!< network handler
+
+       char const                      *name;                  //!< socket name
+
+       proto_cron_crontab_t const      *inst;
+
+       fr_event_timer_t const          *ev;                    //!< for writing statistics
+
+       fr_listen_t                     *parent;                //!< master IO handler
+
+       fr_time_t                       recv_time;              //!< when the timer hit.
+
+       bool                            suspended;              //!< we suspend reading from the FD.
+       bool                            bootstrap;              //!< get it started
+} proto_cron_crontab_thread_t;
+
+typedef struct {
+       unsigned int    min;
+       unsigned int    max;
+
+       bool            wildcard;
+       size_t          offset;
+
+       uint64_t        fields;
+} cron_tab_t;
+
+struct proto_cron_tab_s {
+       proto_cron_t                    *parent;
+
+       CONF_SECTION                    *cs;                    //!< our configuration
+
+       char const                      *filename;              //!< where to read input packet from
+       fr_pair_list_t                  pair_list;              //!< for input packet
+
+       int                             code;
+       char const                      *spec;                  //!< crontab spec
+
+       cron_tab_t                      tab[5];
+
+       RADCLIENT                       *client;                //!< static client
+};
+
+
+static int time_parse(TALLOC_CTX *ctx, void *out, UNUSED void *parent, CONF_ITEM *ci, CONF_PARSER const *rule);
+
+static const CONF_PARSER crontab_listen_config[] = {
+       { FR_CONF_OFFSET("filename", FR_TYPE_FILE_INPUT | FR_TYPE_REQUIRED | FR_TYPE_NOT_EMPTY, proto_cron_crontab_t, filename) },
+
+       { FR_CONF_OFFSET("when", FR_TYPE_STRING | FR_TYPE_NOT_EMPTY | FR_TYPE_REQUIRED, proto_cron_crontab_t, spec),
+                       .func = time_parse },
+       
+       CONF_PARSER_TERMINATOR
+};
+
+/*
+ *     Parse a basic field with sanity checks.
+ */
+static int parse_field(CONF_ITEM *ci, char const **start, char const *name,
+                      cron_tab_t *tab, unsigned int min, unsigned int max, size_t offset)
+{
+       char const *p;
+       char *end = NULL;
+       unsigned int num, next, step, last = 0;
+       bool last_is_set = false;
+       bool wildcard = false;
+       unsigned int i;
+       uint64_t fields = 0;
+
+       p = *start;
+       fr_skip_whitespace(p);
+
+       if (!*p) {
+               cf_log_err(ci, "Missing field for %s", name);
+               return -1;
+       }
+
+       tab->min = min;
+       tab->max = max;
+       tab->offset = offset;
+       tab->fields = 0;
+
+       /*
+        *      See 'man 5 crontab' for the format.
+        */
+       while (p) {
+               /*
+                *      Allow wildcards, but only once.
+                */
+               if (*p == '*') {
+                       if (wildcard) {
+                               cf_log_err(ci, "Cannot use two wildcards for %s at %s", name, p);
+                               return -1;
+                       }
+
+                       end = UNCONST(char *, p) + 1;
+                       wildcard = true;
+                       num = min;
+                       next = max;
+                       goto check_step;
+               }
+
+               /*
+                *      If there's already a "*", we can't have another one.
+                */
+               if (wildcard) {
+                       cf_log_err(ci, "Cannot use wildcard and numbers for %s at %s", name, p);
+                       return -1;
+               }
+
+               /*
+                *      If it's not a wildcard, it MUST be a number,
+                *      which is between min and max.
+                */
+               num = strtoul(p, &end, 10);
+               if ((num < min) || (num > max)) {
+                       cf_log_err(ci, "Number is invalid or out of bounds (%d..%d) for %s at %s",
+                                  min, max, name, p);
+                       return -1;
+               }
+
+               /*
+                *      Don't allow the same number to be specified
+                *      multiple times.
+                */
+               if (!last_is_set) {
+                       last_is_set = true;
+
+               } else if (num <= last) {
+                               cf_log_err(ci, "Number overlaps with previous value of %u, for %s at %s",
+                                          last, name, p);
+                               return -1;
+               }
+               last = num;
+
+               /*
+                *      Ranges are allowed, with potential steps
+                */
+               if (*end == '-') {
+                       p = end + 1;
+                       next = strtoul(p, &end, 10);
+                       if (next <= num) {
+                               cf_log_err(ci, "End of range number overlaps with previous value of %u, for %s at %s",
+                                          num, name, p);
+                               return -1;
+                       }
+
+                       if (next > max) {
+                               cf_log_err(ci, "End of range number is invalid or out of bounds (%d..%d) for %s at %s",
+                                          min, max, name, p);
+                               return -1;
+                       }
+
+               check_step:
+                       last = next;
+
+                       /*
+                        *      Allow /N
+                        */
+                       if (*end == '/') {
+                               p = end + 1;
+
+                               step = strtoul(p, &end, 10);
+                               if (step >= max) {
+                                       cf_log_err(ci, "Step value is invalid or out of bounds for %s at %s", name, p);
+                                       return -1;
+                               }
+                       } else {
+                               step = 1;
+                       }
+
+                       /*
+                        *      Set the necessary bits.
+                        */
+                       for (i = num; i <= next; i += step) {
+                               fields |= ((uint64_t) 1) << i;
+                       }
+               } /* end of range specifier */
+
+               /*
+                *      We can specify multiple fields, separated by a comma.
+                */
+               if (*end == ',') {
+                       fields |= ((uint64_t) 1) << num;
+                       p = end + 1;
+                       continue;
+               }
+
+               /*
+                *      EOS or space is end of field.
+                */
+               if (!(!*end || isspace((int) *end))) {
+                       cf_log_err(ci, "Unexpected text for %s at %s", name, end);
+                       return -1;
+               }
+
+               /*
+                *      We're at the end of the field, stop.
+                */
+               break;
+       }
+
+       /*
+        *      Set a wildcard, so we can skip a lot of the later
+        *      logic.
+        */
+       tab->wildcard = true;
+       for (i = min; i <= max; i++) {
+               if ((fields & (((uint64_t) 1) << i)) == 0) {
+                       tab->wildcard = false;
+                       break;
+               }
+       }
+
+       tab->fields = fields;
+       *start = end;
+       return 0;
+}
+
+/*
+ *     Special names, including our own extensions.
+ */
+static fr_table_ptr_sorted_t time_names[] = {
+       { L("annually"),        "0 0 1 1 *" },
+       { L("daily"),           "0 0 * * *" },
+       { L("hourly"),          "0 * * * *" },
+       { L("midnight"),        "0 0 * * *" },
+       { L("monthly"),         "0 0 1 * *" },
+//     { L("reboot"),          "+" },
+//     { L("shutdown"),        "-" },
+//     { L("startup"),         "+" },
+       { L("weekly"),          "0 0 * * 0" },
+       { L("yearly"),          "0 0 1 1 *" },
+};
+static size_t time_names_len = NUM_ELEMENTS(time_names);
+
+/** Wrapper around dl_instance which checks the syntax of a cron job
+ *
+ * @param[in] ctx      to allocate data in (instance of proto_cron).
+ * @param[out] out     Where to write a dl_module_inst_t containing the module handle and instance.
+ * @param[in] parent   Base structure address.
+ * @param[in] ci       #CONF_PAIR specifying the name of the type module.
+ * @param[in] rule     unused.
+ * @return
+ *     - 0 on success.
+ *     - -1 on failure.
+ *
+ *     https://github.com/staticlibs/ccronexpr/blob/master/ccronexpr.c
+ */
+static int time_parse(UNUSED TALLOC_CTX *ctx, void *out, void *parent, CONF_ITEM *ci, UNUSED CONF_PARSER const *rule)
+{
+       proto_cron_crontab_t            *inst = talloc_get_type_abort(parent, proto_cron_crontab_t);
+       CONF_PAIR               *cp = cf_item_to_pair(ci);
+       char const              *value = cf_pair_value(cp);
+       char const              *p;
+
+       p = value;
+
+       /*
+        *      Check for special names.
+        */
+       if (*p == '@') {
+               p = fr_table_value_by_str(time_names, p + 1, NULL);
+               if (!p) {
+                       cf_log_err(ci, "Invalid time name '%s'", value);
+                       return -1;
+               }
+
+               /*
+                *      Over-write the special names with standard
+                *      ones, so that the rest of the parser is simpler.
+                */
+               *((char const **) out) = p;
+               return 0;
+       }
+
+       *((char const **) out) = value;
+
+       if (parse_field(ci, &p, "minute",       &inst->tab[0], 0, 59, offsetof(struct tm, tm_min)) < 0) return -1;
+       if (parse_field(ci, &p, "hour",         &inst->tab[1], 0, 59, offsetof(struct tm, tm_hour)) < 0) return -1;
+       if (parse_field(ci, &p, "day of month", &inst->tab[2], 1, 31, offsetof(struct tm, tm_mday)) < 0) return -1;
+       if (parse_field(ci, &p, "month",        &inst->tab[3], 1,12, offsetof(struct tm, tm_mon)) < 0) return -1;
+       if (parse_field(ci, &p, "day of week",  &inst->tab[4], 0, 6, offsetof(struct tm, tm_wday)) < 0) return -1;
+       
+       fr_skip_whitespace(p);
+
+       if (*p) {
+               cf_log_err(ci, "Unexpected text after cron time specification");
+               return -1;
+       }
+
+       return 0;
+}
+
+static ssize_t mod_read(fr_listen_t *li, void **packet_ctx, fr_time_t *recv_time_p, uint8_t *buffer, size_t buffer_len, size_t *leftover, UNUSED uint32_t *priority, UNUSED bool *is_dup)
+{
+       proto_cron_crontab_t const      *inst = talloc_get_type_abort_const(li->app_io_instance, proto_cron_crontab_t);
+       proto_cron_crontab_thread_t     *thread = talloc_get_type_abort(li->thread_instance, proto_cron_crontab_thread_t);
+       fr_io_address_t                 *address, **address_p;
+
+       *leftover = 0;
+
+       /*
+        *      Suspend all activity on the FD, because we let the
+        *      timers do their work.
+        */
+       if (!thread->suspended) {
+               static fr_event_update_t pause[] = {
+                       FR_EVENT_SUSPEND(fr_event_io_func_t, read),
+                       FR_EVENT_SUSPEND(fr_event_io_func_t, write),
+                       { 0 }
+               };
+
+               if (fr_event_filter_update(thread->el, li->fd, FR_EVENT_FILTER_IO, pause) < 0) {
+                       fr_assert(0);
+               }
+
+               /*
+                *      Don't read from it the first time.
+                */
+               thread->suspended = true;
+               return 0;
+       }
+
+       /*
+        *      Where the addresses should go.  This is a special case
+        *      for proto_radius.
+        */
+       address_p = (fr_io_address_t **) packet_ctx;
+       address = *address_p;
+
+       memset(address, 0, sizeof(*address));
+       address->socket.inet.src_ipaddr.af = AF_INET;
+       address->socket.inet.dst_ipaddr.af = AF_INET;
+       address->radclient = inst->client;
+
+       *recv_time_p = thread->recv_time;
+
+       if (buffer_len < 1) {
+               DEBUG2("proto_cron_tab read buffer is too small for input packet");
+               return 0;
+       }
+
+       buffer[0] = 0;
+
+       /*
+        *      Print out what we received.
+        */
+       DEBUG2("proto_cron_crontab - reading packet for %s",
+              thread->name);
+
+       return 1;
+}
+
+
+static ssize_t mod_write(UNUSED fr_listen_t *li, UNUSED void *packet_ctx, UNUSED fr_time_t request_time,
+                        UNUSED uint8_t *buffer, UNUSED size_t buffer_len, UNUSED size_t written)
+{
+       return buffer_len;
+}
+
+
+/** Open a crontab listener
+ *
+ */
+static int mod_open(fr_listen_t *li)
+{
+       proto_cron_crontab_t const              *inst = talloc_get_type_abort_const(li->app_io_instance, proto_cron_crontab_t);
+       proto_cron_crontab_thread_t     *thread = talloc_get_type_abort(li->thread_instance, proto_cron_crontab_thread_t);
+
+       fr_ipaddr_t                     ipaddr;
+
+       /*
+        *      We never read or write to this file, but we need a
+        *      readable FD in order to bootstrap the process.
+        */
+       li->fd = open(inst->filename, O_RDONLY);
+
+       memset(&ipaddr, 0, sizeof(ipaddr));
+       ipaddr.af = AF_INET;
+       li->app_io_addr = fr_socket_addr_alloc_inet_src(li, IPPROTO_UDP, 0, &ipaddr, 0);
+
+       fr_assert((cf_parent(inst->cs) != NULL) && (cf_parent(cf_parent(inst->cs)) != NULL));   /* listen { ... } */
+
+       thread->name = talloc_typed_asprintf(thread, "cron_crontab from filename %s", inst->filename ? inst->filename : "none");
+       thread->parent = talloc_parent(li);
+
+       return 0;
+}
+
+
+/** Decode the packet
+ *
+ */
+static int mod_decode(void const *instance, request_t *request, UNUSED uint8_t *const data, UNUSED size_t data_len)
+{
+       proto_cron_crontab_t const      *inst = talloc_get_type_abort_const(instance, proto_cron_crontab_t);
+       fr_io_track_t const     *track = talloc_get_type_abort_const(request->async->packet_ctx, fr_io_track_t);
+       fr_io_address_t const   *address = track->address;
+
+       /*
+        *      Set the request dictionary so that we can do
+        *      generic->protocol attribute conversions as
+        *      the request runs through the server.
+        */
+       request->dict = inst->parent->dict;
+
+       /*
+        *      Hacks for now until we have a lower-level decode routine.
+        */
+       if (inst->code) request->packet->code = inst->code;
+       request->packet->id = fr_rand() & 0xff;
+       request->reply->id = request->packet->id;
+       memset(request->packet->vector, 0, sizeof(request->packet->vector));
+
+       request->packet->data = talloc_zero_array(request->packet, uint8_t, 1);
+       request->packet->data_len = 1;
+
+       /*
+        *      Note that we don't set a limit on max_attributes here.
+        *      That MUST be set and checked in the underlying
+        *      transport, via a call to fr_radius_ok().
+        */
+       (void) fr_pair_list_copy(request->request_ctx, &request->request_pairs, &inst->pair_list);
+
+       /*
+        *      Set the rest of the fields.
+        */
+       request->client = UNCONST(RADCLIENT *, address->radclient);
+
+       request->packet->socket = address->socket;
+       fr_socket_addr_swap(&request->reply->socket, &address->socket);
+
+       REQUEST_VERIFY(request);
+
+       return 0;
+}
+
+/*
+ *     Get the next time interval.
+ *
+ *     Set the relevant "struct tm" field to its next value, and
+ *     return "true"
+ *
+ *     Set the relevant "struct tm" field to its minimum value, and
+ *     return "false".
+ */
+static bool get_next(struct tm *tm, cron_tab_t const *tab)
+{
+       unsigned int i, num = *(int *) (((uint8_t *) tm) + tab->offset);
+
+       num++;
+
+       /*
+        *      Simplified process for "do each thing".
+        */
+       if (tab->wildcard) {
+               if (num < tab->max) goto done;
+               goto next;
+       }
+
+       /*
+        *      See when the next time interval is.
+        */
+       for (i = num; i <= tab->max; i++) {
+               if ((tab->fields & (((uint64_t) 1) << i)) != 0) {
+                       num = i;
+                       break;
+               }
+       }
+
+       /*
+        *      We ran out of time intervals.  Reset this field to the
+        *      minimum, and ask the caller to go to the next
+        *      interval.
+        */
+       if (i > tab->max) {
+       next:
+               *(int *) (((uint8_t *) tm) + tab->offset) = tab->min;
+               return false;
+       }
+
+done:  
+       *(int *) (((uint8_t *) tm) + tab->offset) = num;
+       return true;
+}
+
+/*
+ *     Called when tm.tm_sec == 0.  If it isn't zero, then it means
+ *     that the timer is late, and we treat it as if tm.tm_sec == 0.
+ */
+static void do_cron(fr_event_list_t *el, fr_time_t now, void *uctx)
+{
+       proto_cron_crontab_thread_t     *thread = uctx;
+       struct tm tm;
+       time_t start = time(NULL), end;
+
+       thread->recv_time = now;
+
+       localtime_r(&start, &tm);
+
+       /*
+        *      For now, ignore "day of week".  If the "day of week"
+        *      is a wildcard, then ignore it.  Otherwise, calculate
+        *      next based on "day of month" and also "day of week",
+        *      and then return the time which is closer.
+        */
+       tm.tm_sec = 0;
+       if (get_next(&tm, &thread->inst->tab[0])) goto set; /* minutes */
+       if (get_next(&tm, &thread->inst->tab[1])) goto set; /* hours */
+       if (get_next(&tm, &thread->inst->tab[2])) goto set; /* days */
+       if (get_next(&tm, &thread->inst->tab[3])) goto set; /* month */
+
+       /*
+        *      We ran out of months, so we have to go to the next year.
+        */
+       tm.tm_year++;
+
+set:
+       end = mktime(&tm);
+       fr_assert(end >= start);
+
+       if (DEBUG_ENABLED2) {
+               char buffer[256];
+
+               ctime_r(&end, buffer);
+               DEBUG("TIMER - virtual server %s next cron is at %s, in %ld seconds",
+                     cf_section_name2(thread->inst->parent->server_cs), buffer, end - start);
+       }
+
+       if (fr_event_timer_at(thread, el, &thread->ev, now + fr_time_delta_from_sec(end - start), do_cron, thread) < 0) {
+               fr_assert(0);
+       }
+
+       /*
+        *      Don't run the event the first time.
+        */
+       if (thread->bootstrap) {
+               thread->bootstrap = false;
+               return;
+       }
+
+       /*
+        *      Now that we've set the timer, tell the network side to
+        *      call our read routine.
+        */
+//     fr_network_listen_read(thread->nr, thread->parent);
+}
+
+/** Set the event list for a new socket
+ *
+ * @param[in] li the listener
+ * @param[in] el the event list
+ * @param[in] nr context from the network side
+ */
+static void mod_event_list_set(fr_listen_t *li, fr_event_list_t *el, void *nr)
+{
+       proto_cron_crontab_t const       *inst = talloc_get_type_abort_const(li->app_io_instance, proto_cron_crontab_t);
+       proto_cron_crontab_thread_t     *thread = talloc_get_type_abort(li->thread_instance, proto_cron_crontab_thread_t);
+
+       thread->el = el;
+       thread->nr = nr;
+       thread->inst = inst;
+       thread->bootstrap = true;
+
+       do_cron(el, fr_time(), thread);
+}
+
+static char const *mod_name(fr_listen_t *li)
+{
+       proto_cron_crontab_thread_t     *thread = talloc_get_type_abort(li->thread_instance, proto_cron_crontab_thread_t);
+
+       return thread->name;
+}
+
+
+static int mod_bootstrap(void *instance, CONF_SECTION *cs)
+{
+       proto_cron_crontab_t    *inst = talloc_get_type_abort(instance, proto_cron_crontab_t);
+       dl_module_inst_t const  *dl_inst;
+
+       /*
+        *      Find the dl_module_inst_t holding our instance data
+        *      so we can find out what the parent of our instance
+        *      was.
+        */
+       dl_inst = dl_module_instance_by_data(instance);
+       fr_assert(dl_inst);
+
+       inst->parent = talloc_get_type_abort(dl_inst->parent->data, proto_cron_t);
+
+       inst->cs = cs;
+
+       return 0;
+}
+
+static RADCLIENT *mod_client_find(fr_listen_t *li, UNUSED fr_ipaddr_t const *ipaddr, UNUSED int ipproto)
+{
+       proto_cron_crontab_t const       *inst = talloc_get_type_abort_const(li->app_io_instance, proto_cron_crontab_t);
+
+       return inst->client;
+}
+
+
+static int mod_instantiate(void *instance, CONF_SECTION *cs)
+{
+       proto_cron_crontab_t    *inst = talloc_get_type_abort(instance, proto_cron_crontab_t);
+       RADCLIENT               *client;
+       fr_pair_t               *vp;
+       FILE                    *fp;
+       bool                    done = false;
+
+       fr_pair_list_init(&inst->pair_list);
+       inst->client = client = talloc_zero(inst, RADCLIENT);
+       if (!inst->client) return 0;
+
+       client->ipaddr.af = AF_INET;
+       client->src_ipaddr = client->ipaddr;
+
+       client->longname = client->shortname = inst->filename;
+       client->secret = talloc_strdup(client, "testing123");
+       client->nas_type = talloc_strdup(client, "load");
+       client->use_connected = false;
+
+       fp = fopen(inst->filename, "r");
+       if (!fp) {
+               cf_log_err(cs, "Failed opening %s - %s",
+                          inst->filename, fr_syserror(errno));
+               return -1;
+       }
+
+       if (fr_pair_list_afrom_file(inst, inst->parent->dict, &inst->pair_list, fp, &done) < 0) {
+               cf_log_perr(cs, "Failed reading %s", inst->filename);
+               fclose(fp);
+               return -1;
+       }
+
+       fclose(fp);
+
+       vp = fr_pair_find_by_da(&inst->pair_list, inst->parent->attr_packet_type, 0);
+       if (vp) inst->code = vp->vp_uint32;
+
+       return 0;
+}
+
+fr_app_io_t proto_cron_crontab = {
+       .magic                  = RLM_MODULE_INIT,
+       .name                   = "cron_crontab",
+       .config                 = crontab_listen_config,
+       .inst_size              = sizeof(proto_cron_crontab_t),
+       .thread_inst_size       = sizeof(proto_cron_crontab_thread_t),
+       .bootstrap              = mod_bootstrap,
+       .instantiate            = mod_instantiate,
+
+       .default_message_size   = 4096,
+       .track_duplicates       = false,
+
+       .open                   = mod_open,
+       .read                   = mod_read,
+       .write                  = mod_write,
+       .event_list_set         = mod_event_list_set,
+       .client_find            = mod_client_find,
+       .get_name               = mod_name,
+
+       .decode                 = mod_decode,
+};
diff --git a/src/listen/cron/proto_cron_crontab.mk b/src/listen/cron/proto_cron_crontab.mk
new file mode 100644 (file)
index 0000000..7dd34d5
--- /dev/null
@@ -0,0 +1,9 @@
+TARGETNAME     := proto_cron_crontab
+
+ifneq "$(TARGETNAME)" ""
+TARGET         := $(TARGETNAME).a
+endif
+
+SOURCES                := proto_cron_crontab.c
+
+TGT_PREREQS    := libfreeradius-util.a