--- /dev/null
+/*
+ * 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,
+};
--- /dev/null
+/*
+ * 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,
+};