From: Alan T. DeKok Date: Wed, 15 Sep 2021 18:18:01 +0000 (-0400) Subject: add crontab functionality. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c4ce47c26ac2f891a443bfe633b8d1d3537b64c2;p=thirdparty%2Ffreeradius-server.git add crontab functionality. 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" --- diff --git a/src/listen/cron/all.mk b/src/listen/cron/all.mk new file mode 100644 index 00000000000..113c2161153 --- /dev/null +++ b/src/listen/cron/all.mk @@ -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 index 00000000000..bda8317e2f4 --- /dev/null +++ b/src/listen/cron/proto_cron.c @@ -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 +#include +#include +#include +#include +#include +#include + +#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 index 00000000000..71137f672cf --- /dev/null +++ b/src/listen/cron/proto_cron.h @@ -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 +#include + +#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 + +#ifdef __cplusplus +} +#endif diff --git a/src/listen/cron/proto_cron.mk b/src/listen/cron/proto_cron.mk new file mode 100644 index 00000000000..10ebde9b4fd --- /dev/null +++ b/src/listen/cron/proto_cron.mk @@ -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 index 00000000000..6d7cae60898 --- /dev/null +++ b/src/listen/cron/proto_cron_crontab.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 index 00000000000..7dd34d54ad8 --- /dev/null +++ b/src/listen/cron/proto_cron_crontab.mk @@ -0,0 +1,9 @@ +TARGETNAME := proto_cron_crontab + +ifneq "$(TARGETNAME)" "" +TARGET := $(TARGETNAME).a +endif + +SOURCES := proto_cron_crontab.c + +TGT_PREREQS := libfreeradius-util.a