/* * Copyright (c) 2006, Stefan Walter * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the * above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or * other materials provided with the distribution. * * The names of contributors to this software may not be * used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * * * CONTRIBUTORS * Stef Walter */ #include "usuals.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_PCRE #include #else #include #endif #include "regex_tree.h" #include "regex_oid.h" #define DEFAULT_CONFIG CONF_PREFIX "/rrdbot" #define DEFAULT_SOCKET "/var/run/snmp-regex.sock" #define DEFAULT_EXPIRES 0 /* our module handle */ static struct lmodule *module; /* OIDs */ static const struct asn_oid oid_regex = OIDX_regex; /* the Object Resource registration index */ static u_int reg_index = 0; /* Various match types */ enum { TYPE_UNKNOWN = 0, TYPE_COUNTER, TYPE_VALUE }; struct data_entry { uint32_t index; TAILQ_ENTRY(data_entry) link; int type; char *descr; uint64_t expires; #ifdef WITH_PCRE pcre *regex; pcre_extra *extra; #else regex_t regex; #endif char *result; uint64_t last_update; int64_t value_int; char *value_str; }; TAILQ_HEAD(data_entry_list, data_entry); /* list of regexes */ static struct data_entry_list entries = TAILQ_HEAD_INITIALIZER(entries); static uint32_t entry_count = 0; /* configuration */ static u_char *regex_config = NULL; static char *config_memory = NULL; static uint64_t option_expires = DEFAULT_EXPIRES; /* The socket to read from */ static u_char *regex_sock = NULL; static int sock_fd = -1; static void *sock_sel = NULL; #define LINE_LENGTH 1024 struct connection { int fd; void *sel; char line_buffer[LINE_LENGTH]; TAILQ_ENTRY(connection) link; }; TAILQ_HEAD(connection_list, connection); /* List of connections */ static struct connection_list connections = TAILQ_HEAD_INITIALIZER(connections); /* ----------------------------------------------------------------------------- * HELPERS */ static void emsg(const char *format, ...) { va_list va; va_start(va, format); vsyslog(LOG_ERR, format, va); va_end(va); } static void strcln (char* data, char ch) { char* p; for (p = data; *data; data++, p++) { while (*data == ch) data++; *p = *data; } /* null terminate */ *p = 0; } static void stretrim (char* data) { char* t = data + strlen (data); while (t > data && isspace (*(t - 1))) { t--; *t = 0; } } static char* strbtrim (const char* data) { while (*data && isspace (*data)) ++data; return (char*)data; } static char* strtrim (char* data) { data = (char*)strbtrim (data); stretrim (data); return data; } static uint64_t getcurrticks () { return get_ticks (); } #ifndef WITH_PCRE static const char* regex_msg (int code) { ASSERT (code != 0); switch (code) { case REG_NOMATCH: return "The regexec() function failed to match"; case REG_BADPAT: return "invalid regular expression"; case REG_ECOLLATE: return "invalid collating element"; case REG_ECTYPE: return "invalid character class"; case REG_EESCAPE: return "'\' applied to unescapable character"; case REG_ESUBREG: return "invalid backreference number"; case REG_EBRACK: return "brackets '[ ]' not balanced"; case REG_EPAREN: return "parentheses '( )' not balanced"; case REG_EBRACE: return "braces '{ }' not balanced"; case REG_BADBR: return "invalid repetition count(s) in '{ }'"; case REG_ERANGE: return "invalid character range in '[ ]'"; case REG_ESPACE: return "ran out of memory"; case REG_BADRPT: return "'?', '*', or '+' operand invalid"; case REG_EMPTY: return "empty (sub)expression"; case REG_ILLSEQ: return "illegal byte sequence (bad multibyte character)"; case REG_ASSERT: case REG_INVARG: default: return "internal or unknown error"; } } #endif /* WITH_PCRE */ /* ----------------------------------------------------------------------------- * MATCHING */ static int process_match (struct data_entry *data, char *result) { char *t; int64_t i; switch(data->type) { case TYPE_COUNTER: data->value_int++; if (result) free (result); break; case TYPE_VALUE: if (!result) { emsg ("match, but no result data for '%s'", data->descr); return -1; } /* Integer value */ i = strtoll (result, &t, 10); if (!*t) data->value_int = i; else data->value_int = 0; /* String value */ if (data->value_str) free (data->value_str); data->value_str = result; break; default: ASSERT(0); break; } data->last_update = getcurrticks (); /* fprintf(stderr, "updated '%s' to value '%d' at '%lld'\n", data->descr, data->value_str, data->last_update); */ return 0; } static char* process_result (struct data_entry *data, char *line, int len, #ifdef WITH_PCRE int *ovector, int substrings) #else regmatch_t *pm) #endif { char *result; char *p, *t; int rlen, l; int idx; /* Some nice little optimizations */ if (!data->result) return NULL; if (strchr (data->result, '\\') == NULL) return strdup (data->result); /* Figure out the string length */ rlen = strlen (data->result) + 1; for (p = data->result; *p; p++) { if(p[0] == '\\') { p++; if(p[0] == '\\' || !isdigit(p[0])) continue; idx = p[0] - '0'; ASSERT (idx >= 0 && idx <= 9); #ifdef WITH_PCRE if (idx < substrings) { ASSERT (ovector[(idx * 2) + 1] >= ovector[idx * 2]); ASSERT (ovector[(idx * 2) + 1] < len); ASSERT (ovector[idx * 2] < len); rlen += (ovector[(idx * 2) + 1]) - (ovector[idx * 2]); rlen += 1; } #else /* WITH_PCRE */ if (pm[idx].rm_so != -1 && pm[idx].rm_eo != -1) { ASSERT (pm[idx].rm_eo >= pm[idx].rm_so); ASSERT (pm[idx].rm_eo <= len); ASSERT (pm[idx].rm_so <= len); rlen += (pm[idx].rm_eo - pm[idx].rm_so); rlen += 1; } #endif /* WITH_PCRE */ } } result = (char*)calloc(rlen, 1); if (!result) { emsg ("out of memory"); return NULL; } for (p = data->result, t = result; *p; p++) { if (p[0] == '\\') { p++; if (isdigit(p[0])) { idx = p[0] - '0'; #ifdef WITH_PCRE if (idx < substrings) { l = (ovector[(idx * 2) + 1]) - (ovector[idx * 2]); memcpy (t, line + (ovector[idx * 2]), l); t += l; } #else /* WITH_PCRE */ if (pm[idx].rm_so != -1 && pm[idx].rm_eo != -1) { l = pm[idx].rm_eo - pm[idx].rm_so; memcpy (t, line + pm[idx].rm_so, l); t += l; } #endif /* WITH _PCRE */ continue; } } *t++ = *p; } *t = 0; return result; } static void process_log (char *line, int len) { struct data_entry *data; char *result; int r; #ifdef WITH_PCRE int ovector[30]; #else regmatch_t pm[10]; #endif for (data = TAILQ_FIRST (&entries); data; data = TAILQ_NEXT (data, link)) { result = NULL; #ifdef WITH_PCRE memset (ovector, 0, sizeof (ovector)); r = pcre_exec (data->regex, data->extra, line, len, 0, PCRE_NOTEMPTY | PCRE_NO_UTF8_CHECK, ovector, 30); if (r == PCRE_ERROR_NOMATCH) continue; else if (r == PCRE_ERROR_NOMEMORY) { emsg ("out of memory"); return; } else if (r < 0) { emsg ("internal error in matching code: %d", r); return; } result = process_result (data, line, len, ovector, r); #else r = regexec (&(data->regex), line, 10, pm, 0); if (r == REG_NOMATCH) continue; else if (r != 0) { emsg ("internal error in matching code: %d", r); return; } result = process_result (data, line, len, pm); #endif process_match (data, result); } } /* ----------------------------------------------------------------------------- * SOCKET HANDLING */ static void close_connection (struct connection *conn) { if (conn->sel) fd_deselect (conn->sel); conn->sel = NULL; if (conn->fd >= 0) close (conn->fd); conn->fd = -1; TAILQ_REMOVE (&connections, conn, link); free (conn); } static void io_data (int fd, void *user_data) { struct connection *conn = (struct connection*)user_data; char *line_buffer; char *t, *n; int len; int r, l; ASSERT (fd == conn->fd); line_buffer = conn->line_buffer; len = strlen (line_buffer); ASSERT (len < LINE_LENGTH); do { r = read (fd, line_buffer + len, (LINE_LENGTH - 1) - len); /* An error, close */ if (r < 0 && errno != EAGAIN) { emsg ("couldn't read from socket: %s: %s", regex_sock, strerror (errno)); close_connection (conn); return; /* Connection is finished, close */ } else if (r == 0) { close_connection (conn); return; /* Got data, null terminate */ } else if (r > 0) { len += r; ASSERT (len < LINE_LENGTH); line_buffer[len] = 0; } for (;;) { t = strchr (line_buffer, '\n'); if (t == NULL) { /* Wait for more data */ if (len < LINE_LENGTH - 1) break; /* Break really long lines */ n = t = line_buffer + len; } else { n = t + 1; } /* Break line (also DOS line) */ *t = 0; if (line_buffer != t && *(t - 1) == '\r') *(--t) = 0; l = t - line_buffer; /* Send it off */ process_log (line_buffer, l); /* Move data to front of buffer */ len -= (n - line_buffer); memmove (line_buffer, n, len + 1); } } while (r > 0); } static void io_accept (int fd, void *unused) { struct connection *conn; int nfd; nfd = accept (fd, NULL, NULL); if (nfd < 0) { if (errno != EAGAIN || errno != EINTR) emsg ("couldn't accept socket connection: %s: %s", regex_sock, strerror (errno)); return; } fcntl (nfd, F_SETFL, O_NONBLOCK | fcntl(nfd, F_GETFL, 0)); conn = calloc (1, sizeof (*conn)); if (!conn) { emsg ("out of memory"); close (nfd); return; } conn->fd = nfd; TAILQ_INSERT_HEAD (&connections, conn, link); conn->sel = fd_select (nfd, io_data, conn, module); if (!conn->sel) { emsg ("couldn't list on socket connection: %s: %s", regex_sock, strerror (errno)); close_connection (conn); } } static void close_sock () { struct connection *conn; if (sock_sel) fd_deselect (sock_sel); sock_sel = NULL; if (sock_fd != -1) close (sock_fd); sock_fd = -1; while ((conn = TAILQ_FIRST(&connections)) != NULL) close_connection (conn); } static int open_sock () { struct stat sb; struct sockaddr_un addr; int len; close_sock (); ASSERT (regex_sock && regex_sock[0]); if (stat (regex_sock, &sb) == 0) { /* Complain if it's not a sock */ if (!S_ISSOCK (sb.st_mode)) { emsg ("file or directory already exists: %s", regex_sock); return -1; } /* If it's a socket delete */ unlink (regex_sock); } else if (errno != ENOENT) { emsg ("couldn't access file: %s: %s", regex_sock, strerror (errno)); return -1; } /* Create socket and ... */ sock_fd = socket (AF_UNIX, SOCK_STREAM, 0); if (sock_fd < 0) { emsg ("couldn't create socket: %s", strerror (errno)); return -1; } memset (&addr, 0, sizeof (addr)); addr.sun_family = AF_UNIX; strlcpy (addr.sun_path, regex_sock, sizeof (addr.sun_path)); len = sizeof(addr) - (sizeof(addr.sun_path) - strlen (addr.sun_path)); /* ... bind and ... */ if (bind (sock_fd, (struct sockaddr*)&addr, len) != 0) { emsg ("couldn't bind to socket: %s: %s", regex_sock, strerror (errno)); return -1; } /* ... listen to it ... */ if (listen (sock_fd, 5) < 0) { emsg ("couldn't listen on socket: %s: %s", regex_sock, strerror (errno)); return -1; } /* ... watch for new connections ... */ sock_sel = fd_select (sock_fd, io_accept, NULL, module); if (!sock_sel) { emsg ("couldn't listen on socket: %s: %s", regex_sock, strerror (errno)); return -1; } return 0; } /* ----------------------------------------------------------------------------- * CONFIG PARSING */ static void config_free (struct data_entry *data) { #ifdef WITH_PCRE if (data->regex) free (data->regex); if (data->extra) free (data->extra); #else regfree (&(data->regex)); #endif if (data->value_str) free (data->value_str); free (data); } static void config_free_all () { struct data_entry *data; while ((data = TAILQ_FIRST(&entries)) != NULL) { TAILQ_REMOVE (&entries, data, link); config_free (data); } } static int config_entry (struct data_entry *data, char *name, char *regex, char *result, char *flags, int line) { #ifdef WITH_PCRE const char *errptr; int erroffset; int options = 0; #else int options = REG_EXTENDED; int r; #endif ASSERT (regex); ASSERT (flags); ASSERT (data); ASSERT (name); /* No result area means this is a counter */ data->type = result ? TYPE_VALUE : TYPE_COUNTER; /* Parse any options we have */ for (; *flags; flags++) { switch (flags[0]) { case 'i': #ifdef WITH_PCRE options |= PCRE_CASELESS; #else options |= REG_ICASE; #endif break; default: emsg ("[line %d] invalid flag: %c", line, flags[0]); return -1; } } /* The name */ data->descr = name; /* Parse the regex */ #ifdef WITH_PCRE data->regex = pcre_compile (regex, options, &errptr, &erroffset, NULL); if (!data->regex) { emsg ("[line %d] invalid regular expression: %s", line, errptr); return -1; } /* Optimize the regex, ignore errors */ data->extra = pcre_study (data->regex, 0, &errptr); #else r = regcomp (&(data->regex), regex, options); if (r != 0) { emsg ("[line %d] invalid regular expression: %s", line, regex_msg (r)); return -1; } #endif /* Replacement data */ data->result = result; /* Options */ data->expires = option_expires; return 0; } static int config_line (char *name, char *value, int line) { struct data_entry *data; char *regex = NULL; char *result = NULL; char *flags = NULL; char delimiter; char *t; int r; /* config_parse trims this for us */ ASSERT (!isspace(value[0])); /* First thing is the delimiter */ if (!*value) return -2; delimiter = value[0]; ++value; /* Look for the next delimiter */ t = strchr (value, delimiter); if (!t) return -2; *t = 0; regex = value; value = t + 1; /* And the result */ t = strchr (value, delimiter); if (t) { *t = 0; t++; result = value; } /* The flags */ flags = t ? t : value; /* Now populate an entry */ data = (struct data_entry*)calloc(1, sizeof (*data)); if (!data) { emsg ("out of memory"); return -1; } /* Now make an entry out of it all */ r = config_entry (data, name, regex, result, flags, line); if (r < 0) { free (data); return r; } /* Put it in our table */ data->index = entry_count++; INSERT_OBJECT_INT (data, &entries); return 0; } static void config_var (char *name, char *value, int line) { char *t2; int i; /* The expires variable */ if (strcmp (name, "expire") == 0 || strcmp (name, "expires") == 0) { i = strtol (value, &t2, 10); if (i <= 0 || *t2) emsg ("invalid value for '%s' variable. ignoring: %s", name, value); else option_expires = i * 100; return; } emsg ("unknown or invalid variable. ignoring: %s", name); } static int config_read () { FILE *f = NULL; long len; ASSERT (regex_config && regex_config[0]); f = fopen (regex_config, "r"); if (f == NULL) { emsg ("couldn't open config file: %s", regex_config); return -1; } /* Figure out size */ if (fseek (f, 0, SEEK_END) == -1 || (len = ftell (f)) == -1 || fseek (f, 0, SEEK_SET) == -1) { emsg ("couldn't seek config file: %s", regex_config); return -1; } ASSERT (!config_memory); if ((config_memory = (char*)malloc (len + 2)) == NULL) { emsg ("out of memory"); return -1; } /* And read in one block */ if(fread(config_memory, 1, len, f) != len) { emsg ("couldn't read config file: %s", regex_config); free (config_memory); config_memory = NULL; return -1; } fclose (f); /* Null terminate the data */ config_memory[len] = '\n'; config_memory[len + 1] = 0; return 0; } static int config_parse () { char* next; char* p; char* t; int line = 0; int var = 0; int r; config_free_all (); /* Reads raw config file into config_memory */ if (config_read () < 0) return -1; ASSERT (config_memory); /* Remove nasty dos line endings */ strcln(config_memory, '\r'); next = config_memory; while ((t = strchr (next, '\n')) != NULL) { *t = 0; p = next; next = t + 1; t = strbtrim (p); line++; /* blank lines, comments */ if (!*t || *t == '#') continue; t = t + strcspn (t, ":="); if (!*t) { emsg ("invalid config line: %s", p); return -1; } /* Equal sign denotes a variable */ var = (*t == '='); *t = 0; t++; /* Pass variables to the appropriate place */ if (var) { config_var (strtrim (p), strtrim (t), line); /* And config lines to the appropriate place */ } else { r = config_line (strtrim (p), strtrim (t), line); if (r < 0) { /* If -2 was returned, no error message was printed */ if (r == -2) emsg ("[line %d] invalid configuration file", line); return -1; } } } return 0; } /* ----------------------------------------------------------------------------- * CALLBACKS */ int op_regexconfig (struct snmp_context *ctx, struct snmp_value *value, u_int sub, u_int iidx, enum snmp_op op) { asn_subid_t which = value->var.subs[sub - 1]; int r = SNMP_ERR_NOERROR; switch (which) { case LEAF_regexConfig: if (op == SNMP_OP_GET) return string_get (value, regex_config, -1); /* Remainder only at initialization */ if (community != COMM_INITIALIZE) return SNMP_ERR_NOT_WRITEABLE; switch (op) { case SNMP_OP_SET: if ((r = string_save (value, ctx, -1, ®ex_config)) == SNMP_ERR_NOERROR) { if (!regex_config[0]) r = SNMP_ERR_WRONG_VALUE; else if (config_parse () < 0) r = SNMP_ERR_GENERR; } if (r != SNMP_ERR_NOERROR) string_rollback (ctx, ®ex_config); break; case SNMP_OP_COMMIT: string_commit (ctx); break; case SNMP_OP_ROLLBACK: string_rollback (ctx, ®ex_config); break; default: ASSERT(0); break; } return r; case LEAF_regexSocket: if (op == SNMP_OP_GET) return string_get (value, regex_sock, -1); /* Remainder only at initialization */ if (community != COMM_INITIALIZE) return SNMP_ERR_NOT_WRITEABLE; switch (op) { case SNMP_OP_SET: if ((r = string_save (value, ctx, -1, ®ex_sock)) == SNMP_ERR_NOERROR) { if (!regex_sock[0]) r = SNMP_ERR_WRONG_VALUE; else if (open_sock () < 0) r = SNMP_ERR_GENERR; } if (r != SNMP_ERR_NOERROR) string_rollback (ctx, ®ex_config); break; case SNMP_OP_COMMIT: string_commit (ctx); break; case SNMP_OP_ROLLBACK: string_rollback (ctx, ®ex_sock); break; default: ASSERT(0); break; } return r; } ASSERT(0); return -1; } int op_regex (struct snmp_context *ctx, struct snmp_value *value, u_int sub, u_int iidx, enum snmp_op op) { asn_subid_t which = value->var.subs[sub - 1]; switch (op) { case SNMP_OP_GET: break; case SNMP_OP_SET: return SNMP_ERR_NOT_WRITEABLE; case SNMP_OP_ROLLBACK: case SNMP_OP_COMMIT: return SNMP_ERR_NOERROR; default: ASSERT(0); break; } switch (which) { case LEAF_regexCount: value->v.integer = entry_count; break; default: ASSERT(0); break; } return SNMP_ERR_NOERROR; } int op_regexentry (struct snmp_context *ctx, struct snmp_value *value, u_int sub, u_int iidx, enum snmp_op op) { asn_subid_t which = value->var.subs[sub - 1]; struct data_entry *data; uint64_t ticks; int expired = 0; switch (op) { case SNMP_OP_GETNEXT: data = NEXT_OBJECT_INT (&entries, &value->var, sub); if (data == NULL) return SNMP_ERR_NOSUCHNAME; value->var.len = sub + 1; value->var.subs[sub] = data->index; break; case SNMP_OP_GET: data = FIND_OBJECT_INT (&entries, &value->var, sub); if (data == NULL) return SNMP_ERR_NOSUCHNAME; break; case SNMP_OP_SET: if (index_decode (&value->var, sub, iidx, &data)) return SNMP_ERR_NO_CREATION; data = FIND_OBJECT_INT (&entries, &value->var, sub); if (data != NULL) return SNMP_ERR_NOT_WRITEABLE; return SNMP_ERR_NO_CREATION; default: ASSERT(0); break; } /* Figure out if we're expired or not */ ticks = getcurrticks (); if (ticks == 0) return SNMP_ERR_GENERR; if (data->expires && data->last_update) { if (ticks > (data->last_update + data->expires)) expired = 1; } switch (which) { case LEAF_regexIndex: value->v.integer = data->index; break; case LEAF_regexDescr: return (string_get (value, data->descr, -1)); case LEAF_regexLast: if (data->last_update && !expired) value->v.uint32 = (ticks - data->last_update); else value->v.uint32 = 0; break; case LEAF_regexInteger: value->v.uint32 = expired ? 0 : data->value_int; break; case LEAF_regexValue: return (string_get (value, (data->value_str && !expired) ? data->value_str : "", -1)); case LEAF_regexCounter: value->v.counter64 = (expired && data->type != TYPE_COUNTER) ? 0 : data->value_int; break; default: ASSERT(0); break; }; return SNMP_ERR_NOERROR; } /* ----------------------------------------------------------------------------- * MODULE */ /* the initialisation function */ static int module_init (struct lmodule *mod, int argc, char *argv[]) { module = mod; if (argc != 0) { syslog(LOG_ERR, "bad number of arguments for %s", __func__); return EINVAL; } regex_config = strdup (DEFAULT_CONFIG); if (!regex_config) return ENOMEM; regex_sock = strdup (DEFAULT_SOCKET); if (!regex_sock) { free (regex_config); return ENOMEM; } return 0; } /* Module is started */ static void module_start (void) { reg_index = or_register (&oid_regex, "The MIB for regex data.", module); } /* Called, when the module is to be unloaded after it was successfully loaded */ static int module_fini (void) { if (reg_index) or_unregister (reg_index); ASSERT (regex_config); free (regex_config); ASSERT (regex_sock); free (regex_sock); close_sock (); config_free_all (); return 0; } const struct snmp_module config = { .comment = "This module implements SNMP listing of data from regular expressions", .init = module_init, .start = module_start, .fini = module_fini, .tree = regex_ctree, .tree_size = regex_CTREE_SIZE, };