filewatcherd

FreeBSD daemon that watches files and runs commands when they change
git clone https://git.instinctive.eu/filewatcherd.git
Log | Files | Refs | README | LICENSE

commit 9fcdef164f2fae4b5e96d77a3ff56ea427288240
Author: Natasha Kerensikova <natacha@instinctive.eu>
Date:   Tue, 30 Jul 2013 23:38:09 +0200

Initial commit

Diffstat:
ALICENSE | 13+++++++++++++
AMakefile | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATODO | 6++++++
Afilewatcherd.c | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog.c | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alog.h | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arun.c | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arun.h | 30++++++++++++++++++++++++++++++
Awatchtab.c | 666+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awatchtab.h | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1831 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013, Natacha Porté + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,63 @@ +# Makefile + +# Copyright (c) 2009-2013, Natacha Porté +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +DEPDIR=depends +ALLDEPS=$(DEPDIR)/all +CFLAGS=-g -O3 -Wall -Wextra -Werror +LDFLAGS=-g -O3 -Wall -Wextra -Werror +CC=gcc + +all: filewatcherd + +.PHONY: all clean + + +# executables + +filewatcherd: filewatcherd.o log.o run.o watchtab.o + $(CC) $(LDFLAGS) $(.ALLSRC) -o $(.TARGET) + + +# Housekeeping + +clean: + rm -f *.o + rm -f filewatcherd + rm -rf $(DEPDIR) + + +# dependencies + +.sinclude "$(ALLDEPS)" + + +# generic object compilations + +.c.o: + @mkdir -p $(DEPDIR) + @touch $(ALLDEPS) + @$(CC) -MM $(.IMPSRC) > $(DEPDIR)/$(.PREFIX).d + @grep -q "$(.PREFIX).d" $(ALLDEPS) \ + || echo ".include \"$(.PREFIX).d\"" >> $(ALLDEPS) + $(CC) -c $(CFLAGS) -o $(.TARGET) $(.IMPSRC) + +.m.o: + @mkdir -p $(DEPDIR) + @touch $(ALLDEPS) + @$(CC) -MM $(.IMPSRC) > depends/$(.PREFIX).d + @grep -q "$(.PREFIX).d" $(ALLDEPS) \ + || echo ".include \"$(.PREFIX).d\"" >> $(ALLDEPS) + $(CC) -c $(CFLAGS) -o $(.TARGET) $(.IMPSRC) diff --git a/README.md b/README.md @@ -0,0 +1,73 @@ +# Overview + +`filewatcherd` is a daemon inspired by cron, that run commands based on +file changes instead of time. + +In principle it is similar to `incron`, but it's simpler, more limited, +and does not depend on anything outside of FreeBSD base. + +# Watchtab + +Usage of `filewatcherd` is quite straightforward: the daemon has a few +basic command-line options, and takes a _watchtab_ file as main input. + +The watchtab is heavily inspired from `crontab`. Blank lines are ignored, +leading and trailing blanks in line are ignored, line starting with a +hash sign (`#`) are ignored as comments. + +Environment lines are defined as having an equal sign (`=`) before any +backslash (`\\`) or tabulation character. They represent environment +variables available for commands, and only affect the entries below them. + +Entry lines consist of 3 to 6 tabulation-separated fields. A complete line +contains the following fields in respective order: + +1. Path of the file to watch +2. Event set to consider +3. Delay between the first triggering event and command run +4. User, and optionally group, to set for the command +5. `chroot` to set for the command +6. The command itself + +When only 5 fields are present, `chroot` is skipped. When there are only +4 fields, user is also skipped. When there are only 3 field, delay is +considered 0. + +In path, `chroot` and command fields, backslashes (`\\`) act as an escape +character, allowing to embed tabulations, backslashes and/or equal signs +in those fields without misinterpretation. + +The event set can be a single star sign (`*`) to mean all available event, +or a list of any number of event names separated by a single non-letter +byte. The available events are `delete`, `write`, `extend`, `attrib`, +`link`, `rename` and `revoke`, with semantics matching those of +similar-named `fflags` for vnode filter. + +The delay is given in seconds and can be fractional, up to the nanosecond +(though most system probably do not have such a resolution in +`nanosleep(3)`). + +The user can be a login string or a numeric id, and is optionally followed +by a group string or numeric id after a colon (`:`). When specified, those +must exist and have a matching `passwd` or `group` entry. + +The command is run in a clean environment, containing only variables +explicitly declared in the watchtab file, and `SHELL`, `PATH`, `HOME`, +`TRIGGER`, `USER`, `LOGNAME`. + + * `SHELL` and `PATH` default respectively to `/bin/sh` and +`/usr/bin:/bin`, but they can be overwritten in the watchtab. + * `HOME` default to the home directory of the user running the command, +but can be overwritten in the watchtab. + * `USER` and `LOGNAME` are both forced to the login name of the user +running the command, and values provided in the watchtab are ignored. + * `TRIGGER` is forced to the path of the file triggering the event +(seen from outside the `chroot`), ignoring any value provided in the +watchtab. + +The watchtab is automatically watched by `filewatcherd` itself, and is +automatically reloaded when it changes. + +# Internals + +Coming soon. diff --git a/TODO b/TODO @@ -0,0 +1,6 @@ + * document the internals + * fix implicit HOME in first entry considered as explicit in following entries + * deal with command output + * check how signals interfere with current code + * think about how to handle multiple watchtabs + * support locking watchtabs to specific users, to make a safe multiuser system daemon diff --git a/filewatcherd.c b/filewatcherd.c @@ -0,0 +1,322 @@ +/* filewatcherd.c - main function for file watcher daemon */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <fcntl.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <syslog.h> + +#include <sys/types.h> +#include <sys/event.h> +#include <sys/time.h> + +#include "log.h" +#include "run.h" +#include "watchtab.h" + +/* insert_entry - wait for an event described by the given watchtab entry */ +static int +insert_entry(int kq, struct watch_entry *wentry) { + struct kevent event; + wentry->fd = open(wentry->path, O_RDONLY | O_CLOEXEC); + if (wentry->fd < 0) { + log_open_entry(wentry->path); + wentry->fd = -1; + return -1; + } + EV_SET(&event, wentry->fd, + EVFILT_VNODE, + EV_ADD | EV_ONESHOT, + wentry->events, + 0, + wentry); + if (kevent(kq, &event, 1, 0, 0, 0) < 0) { + log_kevent_entry(wentry->path); + close(wentry->fd); + wentry->fd = -1; + return -1; + } + + log_entry_wait(wentry); + return 0; +} + + + +int +main(int argc, char **argv) { + int kq; /* file descriptor for the kernel queue */ + int argerr = 0; /* whether arguments are invalid */ + int help = 0; /* whether help text should be displayed */ + int daemonize = 1; /* whether fork to background and use syslog */ + const char *tabpath = 0;/* path to the watchtab file */ + int tab_fd; /* file descriptor of watchtab */ + FILE *tab_f; /* file stream of watchtab */ + struct watchtab wtab; /* current watchtab data */ + intptr_t delay = 100; /* delay in ms before reloading watchtab */ + int wtab_error = 0; /* whether watchtab can't be opened */ + + struct option longopts[] = { + { "foreground", no_argument, 0, 'd' }, + { "help", no_argument, 0, 'h' }, + { "wait", required_argument, 0, 'w' }, + { 0, 0, 0, 0 } + }; + + /* Temporary variables */ + struct kevent event; + struct watch_entry *wentry; + pid_t pid; + char c; + char *s; + + + /*************************** + * COMMAND LINE PROCESSING * + ***************************/ + + /* Process options */ + while (!argerr + && (c = getopt_long(argc, argv, "dhw:", longopts, 0)) != -1) { + switch (c) { + case 'd': + daemonize = 0; + break; + case 'h': + help = 1; + break; + case 'w': + delay = strtol(optarg, &s, 10); + if (!s[0]) { + log_bad_delay(optarg); + argerr = 1; + } + break; + default: + argerr = 1; + } + } + + /* Use the first argument as watchtab, discard the reset */ + if (optind < argc) + tabpath = argv[optind]; + else + argerr = 1; + + /* Display help text and terminate */ + if (argerr || help) { + fprintf(argerr ? stderr : stdout, + "Usage: %s [-dh] [-f delay_ms] watchtab\n\n" + "\t-d, --foreground\n" + "\t\tDon't fork to background and log to stderr\n" + "\t-h, --help\n" + "\t\tDisplay this help text\n" + "\t-w, --wait delay_ms\n" + "\t\tWait that number of milliseconds after watchtab\n" + "\t\tchanges before reloading it\n", + argv[0]); + return argerr ? EXIT_FAILURE : EXIT_SUCCESS; + } + + + /****************** + * INITIALIZATION * + ******************/ + + /* Try to open and read the watchtab */ + tab_fd = open(tabpath, O_RDONLY | O_CLOEXEC); + if (tab_fd < 0) { + log_open_watchtab(tabpath); + return EXIT_FAILURE; + } + tab_f = fdopen(tab_fd, "r"); + if (!tab_f) { + log_open_watchtab(tabpath); + return EXIT_FAILURE; + } + SLIST_INIT(&wtab); + if (wtab_readfile(&wtab, tab_f, tabpath) < 0) + return EXIT_FAILURE; + log_watchtab_loaded(tabpath); + + /* Create a kernel queue */ + kq = kqueue(); + if (kq == -1) { + log_kqueue(); + return EXIT_FAILURE; + } + + /* Insert config file watcher */ + EV_SET(&event, tab_fd, + EVFILT_VNODE, + EV_ADD | EV_ONESHOT, + NOTE_DELETE | NOTE_WRITE | NOTE_RENAME | NOTE_REVOKE, + 0, 0); + if (kevent(kq, &event, 1, 0, 0, 0) < 0) { + log_kevent_watchtab(tabpath); + return EXIT_FAILURE; + } + + /* Fork to background */ + if (daemonize) { + daemon(0, 0); + set_report(&syslog); + } + + /* Insert initial watchers */ + SLIST_FOREACH(wentry, &wtab, next) { + insert_entry(kq, wentry); + } + + + /************* + * MAIN LOOP * + *************/ + + while (1) { + /* Wait for a single event */ + if (kevent(kq, 0, 0, &event, 1, 0) < 0) { + log_kevent_wait(); + break; + } + + switch (event.filter) { + case EVFILT_VNODE: + if (!event.udata) { + /* + * Something happened on the watchtab: + * close everything and start the timer before + * reloading it. + */ + fclose(tab_f); /* also closes tab_fd */ + EV_SET(&event, 42, + EVFILT_TIMER, + EV_ADD, + 0, + delay, /* ms */ + 0); + if (kevent(kq, &event, 1, 0, 0, 0) < 0) { + log_kevent_timer(); + exit(EXIT_FAILURE); + } + break; + } + + /* A watchtab entry has been triggered */ + wentry = event.udata; + if (wentry->fd < 0 + || (uintptr_t)wentry->fd != event.ident) { + LOG_ASSERT("wentry->fd"); + exit(EXIT_FAILURE); + } + close(wentry->fd); + wentry->fd = -1; + pid = run_entry(wentry); + if (!pid) break; + + /* Wait for the command to finish */ + EV_SET(&event, pid, + EVFILT_PROC, + EV_ADD | EV_ONESHOT, + NOTE_EXIT, + 0, + wentry); + if (kevent(kq, &event, 1, 0, 0, 0) < 0) + log_kevent_proc(wentry, pid); + break; + + case EVFILT_PROC: + /* + * The command has finished, re-insert the path to + * watch it. + */ + insert_entry(kq, event.udata); + break; + + case EVFILT_TIMER: + /* + * Timer for watchtab reload has expired, try to + * reopen and reload it. + * When open fails, keep the timer around to try + * again after delay (suppressing errors). + * When loading fails, keep the old watchtab but add + * the event filter anyway to try again on next update. + */ + + /* Try opening the watchtab file */ + tab_fd = open(tabpath, O_RDONLY | O_CLOEXEC); + if (tab_fd < 0) { + if (!wtab_error) + log_open_watchtab(tabpath); + wtab_error = 1; + break; + } + tab_f = fdopen(tab_fd, "r"); + if (!tab_f) { + if (!wtab_error) + log_open_watchtab(tabpath); + wtab_error = 1; + close(tab_fd); + break; + } + + /* Delete the timer */ + event.flags = EV_DELETE; + if (kevent(kq, &event, 1, 0, 0, 0) < 0) { + log_kevent_timer_off(); + /* timer is still around, close files */ + fclose(tab_f); + break; + } + + /* Watch the file for changes */ + EV_SET(&event, tab_fd, + EVFILT_VNODE, + EV_ADD | EV_ONESHOT, + NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE + | NOTE_WRITE, + 0, 0); + if (kevent(kq, &event, 1, 0, 0, 0) < 0) + log_kevent_watchtab(tabpath); + + /* Load watchtab contents on a temporary variable */ + /* local */{ + struct watchtab new_wtab + = SLIST_HEAD_INITIALIZER(new_wtab); + + if (wtab_readfile(&new_wtab, + tab_f, tabpath) < 0) { + wtab_release(&new_wtab); + break; + } + + wtab_release(&wtab); + wtab = new_wtab; + SLIST_FOREACH(wentry, &wtab, next) { + insert_entry(kq, wentry); + } + } + + log_watchtab_loaded(tabpath); + break; + } + } + + return EXIT_SUCCESS; +} diff --git a/log.c b/log.c @@ -0,0 +1,285 @@ +/* log.c - report errors to the outside world */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <errno.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <syslog.h> + +#include "log.h" + +/************* + * REPORTING * + *************/ + +/* report - callback actually used by formatting functions */ +static report_fn report = &report_to_stderr; + + +/* set_report - use the given callback for error reporting */ +void +set_report(report_fn callback) { + report = callback; +} + + +/* report_to_stderr - wrapper to send the message to standard error output */ +void +report_to_stderr(int priority, const char *message, ...) { + va_list ap; + (void)priority; + + va_start(ap, message); + vfprintf(stderr, message, ap); + va_end(ap); + fputc('\n', stderr); +} + + + +/******************* + * ERROR FORMATING * + *******************/ + +/* log_alloc - memory allocation failure */ +void +log_alloc(const char *subsystem) { + if (subsystem) + report(LOG_ERR, "Unable to allocate memory for %s", subsystem); + else + report(LOG_ERR, "Unable to allocate memory"); +} + + +/* log_assert - internal inconsistency */ +void +log_assert(const char *reason, const char *source, unsigned line) { + if (reason) + report(LOG_ERR, "Internal inconsistency at %s:%u (%s)", + source, line, reason); + else + report(LOG_ERR, "Internal inconsistency at %s:%u", + source, line); +} + + +/* log_bad_delay - invalid string provided for delay value */ +void +log_bad_delay(const char *opt) { + report(LOG_ERR, "Bad value \"%s\" for delay", opt); +} + + +/* log_chdir - chdir("/") failed after successful chroot() */ +void +log_chdir(const char *newroot) { + report(LOG_ERR, "chdir(\"/\") error after chroot to %s: %s", + newroot, strerror(errno)); +} + + +/* log_chroot - chroot() failed */ +void +log_chroot(const char *newroot) { + report(LOG_ERR, "Unable to chroot to %s: %s", + newroot, strerror(errno)); +} + + +/* log_entry_wait - watchtab entry successfully inserted in the queue */ +void +log_entry_wait(struct watch_entry *wentry) { + report(LOG_INFO, "Waiting for events on \"%s\"", wentry->path); +} + +/* log_exec - execve() failed */ +void +log_exec(struct watch_entry *wentry) { + report(LOG_ERR, "Unable to execute \"%s\": %s", + wentry->command, strerror(errno)); +} + + +/* log_fork - fork() failed */ +void +log_fork(void) { + report(LOG_ERR, "Error in fork(): %s", strerror(errno)); +} + + +/* log_kevent_entry - kevent() failed when adding an event for a file entry */ +void +log_kevent_entry(const char *path) { + report(LOG_ERR, "Unable to queue filter for file \"%s\": %s", + path, strerror(errno)); +} + + +/* log_kevent_proc - kevent() failed when adding a command watcher */ +void +log_kevent_proc(struct watch_entry *wentry, pid_t pid) { + report(LOG_ERR, "Unable to watch command pid %d (\"%s\"): %s", + (int)pid, wentry->command, strerror(errno)); +} + + +/* log_kevent_timer - kevent() failed when adding a timer */ +void +log_kevent_timer(void) { + report(LOG_ERR, "Unable to queue timer for watchtab: %s", + strerror(errno)); +} + + +/* log_kevent_timer_off - kevent() failed when removing a timer */ +void +log_kevent_timer_off(void) { + report(LOG_ERR, "Unable to delete timer for watchtab: %s", + strerror(errno)); +} + + +/* log_kevent_wait - kevent() failed while waiting for an event */ +void +log_kevent_wait(void) { + report(LOG_ERR, "Error while waiting for a kevent: %s", + strerror(errno)); +} + + +/* log_kevent_watchtab - kevent() failed when adding a watchtab event */ +void +log_kevent_watchtab(const char *path) { + report(LOG_ERR, "Unable to queue filter for watchtab \"%s\": %s", + path, strerror(errno)); +} + + +/* log_kqueue - report failure in kqueue() call */ +void +log_kqueue(void) { + report(LOG_ERR, "Error in kqueue(): %s", strerror(errno)); +} + + +/* log_lookup_group - getgrnam() failed */ +void +log_lookup_group(const char *group) { + if (errno) + report(LOG_ERR, "Error while lookup group \"%s\": %s", + group, strerror(errno)); + else + report(LOG_ERR, "Unable to find group \"%s\"", group); +} + +/* log_lookup_pw - getpwnam() failed */ +void +log_lookup_pw(const char *login) { + if (errno) + report(LOG_ERR, "Error while lookup user \"%s\": %s", + login, strerror(errno)); + else + report(LOG_ERR, "Unable to find user \"%s\"", login); +} + +/* log_lookup_self - getlogin() or getpwnam() failed */ +void +log_lookup_self(void) { + report(LOG_ERR, "Error while trying to lookup current user login"); +} + + +/* log_open_entry - open() failed on watchtab entry file */ +void +log_open_entry(const char *path) { + report(LOG_ERR, "Unable to open watched file \"%s\": %s", + path, strerror(errno)); +} + + +/* log_open_watchtab - watchtab file open() failed */ +void +log_open_watchtab(const char *path) { + report(LOG_ERR, "Unable to open watchtab \"%s\": %s", + path, strerror(errno)); +} + + +/* log_running - a watchtab entry has been triggered */ +void +log_running(struct watch_entry *wentry) { + report(LOG_INFO, "Running \"%s\", triggered by \"%s\"", + wentry->command, wentry->path); +} + + +/* log_setgid - setgid() failed */ +void +log_setgid(gid_t gid) { + report(LOG_INFO, "Unable to set gID to %d: %s", + (int)gid, strerror(errno)); +} + + +/* log_setuid - setuid() failed */ +void +log_setuid(uid_t uid) { + report(LOG_INFO, "Unable to set uID to %d: %s", + (int)uid, strerror(errno)); +} + + +/* log_watchtab_invalid_action - invalid action line in watchtab */ +void +log_watchtab_invalid_action(const char *filename, unsigned line_no) { + report(LOG_ERR, "Invalid action line at line %s:%u", + filename, line_no); +} + + +/* log_watchtab_invalid_delay - invalid delay field in watchtab entry */ +void +log_watchtab_invalid_delay(const char *filename, unsigned line_no, + const char *field) { + report(LOG_ERR, "Invalid delay field \"%s\" at %s:%u", + field, filename, line_no); +} + + +/* log_watchtab_invalid_events - parse error in watchtab event set */ +void +log_watchtab_invalid_events(const char *filename, unsigned line_no, + const char *field, size_t len) { + report(LOG_ERR, "Invalid event set \"%.*s\" at %s:%u", + (int)len, field, filename, line_no); +} + + +/* log_watchtab_loaded - watchtab has been successfully loaded */ +void +log_watchtab_loaded(const char *path) { + report(LOG_NOTICE, "Watchtab \"%s\" loaded successfully", path); +} + + +/* log_watchtab_read - read error on watchtab */ +void +log_watchtab_read(void) { + report(LOG_ERR, "Error while reading from watchtab"); +} diff --git a/log.h b/log.h @@ -0,0 +1,168 @@ +/* log.h - report errors to the outside world */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This module gather all error-formating functions, so that all user-facing + * strings are gathered in one place. + */ + +#ifndef FILEWATCHER_LOG_H +#define FILEWATCHER_LOG_H + +#include "watchtab.h" + + +/************* + * REPORTING * + *************/ + +/* report_fn - report callback, same semantics as syslog() */ +typedef void (*report_fn)(int priority, const char *message, ...) + __attribute__((format (printf, 2, 3))); + +/* report_to_stderr - wrapper to send the message to standard error output */ +void +report_to_stderr(int priority, const char *message, ...); + +/* set_report - use the given callback for error reporting */ +void +set_report(report_fn callback); + +/******************* + * ERROR FORMATING * + *******************/ + +/* log_alloc - memory allocation failure */ +void +log_alloc(const char *subsystem); + +/* log_assert - internal inconsistency */ +void +log_assert(const char *reason, const char *source, unsigned line); +#define LOG_ASSERT(m) log_assert((m), __FILE__, __LINE__) + +/* log_bad_delay - invalid string provided for delay value */ +void +log_bad_delay(const char *opt); + +/* log_chdir - chdir("/") failed after successful chroot() */ +void +log_chdir(const char *newroot); + +/* log_chroot - chroot() failed */ +void +log_chroot(const char *newroot); + +/* log_entry_wait - watchtab entry successfully inserted in the queue */ +void +log_entry_wait(struct watch_entry *wentry); + +/* log_exec - execve() failed */ +void +log_exec(struct watch_entry *wentry); + +/* log_fork - fork() failed */ +void +log_fork(void); + +/* log_kevent_entry - kevent() failed when adding an event for a file entry */ +void +log_kevent_entry(const char *path); + +/* log_kevent_proc - kevent() failed when adding a command watcher */ +void +log_kevent_proc(struct watch_entry *wentry, pid_t pid); + +/* log_kevent_timer - kevent() failed when adding a timer */ +void +log_kevent_timer(void); + +/* log_kevent_timer_off - kevent() failed when removing a timer */ +void +log_kevent_timer_off(void); + +/* log_kevent_wait - kevent() failed while waiting for an event */ +void +log_kevent_wait(void); + +/* log_kevent_watchtab - kevent() failed when adding a watchtab event */ +void +log_kevent_watchtab(const char *path); + +/* log_kqueue - kqueue() failed */ +void +log_kqueue(void); + +/* log_lookup_group - getgrnam() failed */ +/* WARNING: errno must explicitly be zeroed before calling getgrnam() */ +void +log_lookup_group(const char *group); + +/* log_lookup_pw - getpwnam() failed */ +/* WARNING: errno must explicitly be zeroed before calling getpwnam() */ +void +log_lookup_pw(const char *login); + +/* log_lookup_self - getlogin() or getpwnam() failed */ +/* WARNING: errno must explicitly be zeroed before calling getpwnam() */ +void +log_lookup_self(void); + +/* log_open_entry - open() failed on watchtab entry file */ +void +log_open_entry(const char *path); + +/* log_open_watchtab - watchtab file open() failed */ +void +log_open_watchtab(const char *path); + +/* log_running - a watchtab entry has been triggered */ +void +log_running(struct watch_entry *wentry); + +/* log_setgid - setgid() failed */ +void +log_setgid(gid_t gid); + +/* log_setuid - setuid() failed */ +void +log_setuid(uid_t uid); + +/* log_watchtab_invalid_action - invalid action line in watchtab */ +void +log_watchtab_invalid_action(const char *filename, unsigned line_no); + +/* log_watchtab_invalid_delay - invalid delay field in watchtab entry */ +void +log_watchtab_invalid_delay(const char *filename, unsigned line_no, + const char *field); + +/* log_watchtab_invalid_events - parse error in watchtab event set */ +void +log_watchtab_invalid_events(const char *filename, unsigned line_no, + const char *field, size_t len); + +/* log_watchtab_loaded - watchtab has been successfully loaded */ +void +log_watchtab_loaded(const char *path); + +/* log_watchtab_read - read error on watchtab */ +void +log_watchtab_read(void); + +#endif /* ndef FILEWATCHER_LOG_H */ diff --git a/run.c b/run.c @@ -0,0 +1,92 @@ +/* run.c - command execution */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <spawn.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "log.h" +#include "run.h" + +/* run_entry - start the command associated with the given entry */ +pid_t +run_entry(struct watch_entry *wentry) { + char *argv[4]; + size_t i = 0; + pid_t result; + int has_delay = (wentry->delay.tv_sec || wentry->delay.tv_nsec); + + /* Create a child process and hand control back to parent */ + result = has_delay ? fork() : vfork(); + if (result == -1) { + log_fork(); + return 0; + } else if (result != 0) { + return result; + } + + /* chroot if requested */ + if (wentry->chroot) { + if (chroot(wentry->chroot) < 0) { + log_chroot(wentry->chroot); + _exit(EXIT_FAILURE); + } + if (chdir("/") < 0) { + log_chdir(wentry->chroot); + _exit(EXIT_FAILURE); + } + } + + /* Set gid and uid if requested */ + if (wentry->gid && setgid(wentry->gid) < 0) { + log_setgid(wentry->gid); + _exit(EXIT_FAILURE); + } + if (wentry->uid && setuid(wentry->uid) < 0) { + log_setuid(wentry->uid); + _exit(EXIT_FAILURE); + } + + /* Wait for some time if requested */ + if (has_delay) + nanosleep(&wentry->delay, 0); + + /* Lookup SHELL environment variable */ + argv[0] = 0; + for (i = 0; wentry->envp[i]; i++) { + if (strncmp(wentry->envp[i], "SHELL=", 6) == 0) { + argv[0] = wentry->envp[i] + 6; + break; + } + } + + /* Build argument list */ + if (!argv[0]) argv[0] = "/bin/sh"; + argv[1] = "-c"; + argv[2] = (char *)wentry->command; + argv[3] = 0; + + /* Handover control to the command */ + execve(argv[0], argv, wentry->envp); + + /* Report error */ + log_exec(wentry); + _exit(EXIT_FAILURE); +} diff --git a/run.h b/run.h @@ -0,0 +1,30 @@ +/* run.h - command execution */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef FILEWATCHER_RUN_H +#define FILEWATCHER_RUN_H + +#include <sys/types.h> + +#include "watchtab.h" + +/* run_entry - start the command associated with the given entry */ +pid_t +run_entry(struct watch_entry *wentry); + +#endif /* ndef FILEWATCHER_RUN_H */ diff --git a/watchtab.c b/watchtab.c @@ -0,0 +1,666 @@ +/* watchtab.c - configuration tables for file watches */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <stdlib.h> +#include <string.h> + +#include <sys/types.h> +#include <sys/event.h> + +#include "log.h" +#include "watchtab.h" + +/* number of pointers allocated at once in watch_env */ +#define WENV_ALLOC_UNIT 16; + +/********************* + * LOCAL SUBPROGRAMS * + *********************/ + +/* parse_events - process a configuration string into fflags vnode events */ +static u_int +parse_events(const char *line, size_t len) { + u_int result = 0; + size_t i = 0; + + /* Check wildcard */ + if (len == 1 && line[0] == '*') + return NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB + | NOTE_LINK | NOTE_RENAME | NOTE_REVOKE; + + /* + * Parse entry as a list of token separated by a single + * non-letter byte. + */ + while (i < len) { + if (strncmp(line + i, "delete", 6) == 0 + || strncmp(line + i, "DELETE", 6) == 0) { + result |= NOTE_DELETE; + i += 6; + } + else if (strncmp(line + i, "write", 5) == 0 + || strncmp(line + i, "WRITE", 5) == 0) { + result |= NOTE_WRITE; + i += 5; + } + else if (strncmp(line + i, "extend", 6) == 0 + || strncmp(line + i, "EXTEND", 6) == 0) { + result |= NOTE_EXTEND; + i += 6; + } + else if (strncmp(line + i, "attrib", 6) == 0 + || strncmp(line + i, "ATTRIB", 6) == 0) { + result |= NOTE_ATTRIB; + i += 6; + } + else if (strncmp(line + i, "link", 4) == 0 + || strncmp(line + i, "LINK", 4) == 0) { + result |= NOTE_LINK; + i += 4; + } + else if (strncmp(line + i, "rename", 6) == 0 + || strncmp(line + i, "RENAME", 6) == 0) { + result |= NOTE_RENAME; + i += 6; + } + else if (strncmp(line + i, "revoke", 6) == 0 + || strncmp(line + i, "REVOKE", 6) == 0) { + result |= NOTE_REVOKE; + i += 6; + } + else return 0; + + if (i < len && ((line[i] >= 'a' && line[i] <= 'z') + || (line[i] >= 'A' && line[i] <= 'Z'))) + return 0; + else + i++; + } + + return result; +} + + +/* strdupesc - duplicate and unescape an input string */ +static char * +strdupesc(const char *src, size_t len) { + size_t s = 0, d = 0; + char *dest = malloc(len + 1); + + if (!dest) { + log_alloc("watchtab entry internal string"); + return 0; + } + + while (src[s] && s < len) { + if (src[s] != '\\' || (s > 0 && src[s-1] == '\\')) + dest[d++] = src[s]; + s++; + } + dest[d] = 0; + return dest; +} + +/* wenv_resize - preallocate enough storage for new_size pointers */ +static int +wenv_resize(struct watch_env *wenv, size_t new_size) { + size_t new_cap, i; + const char **new_env; + + /* Initialize if needed */ + if (!wenv->environ && wenv_init(wenv) < 0) + return -1; + + /* Don't realloc when enough capacity is available */ + if (new_size <= wenv->capacity) return 0; + + /* Actually resize */ + new_cap = wenv->capacity; + while (new_cap < new_size) new_cap += WENV_ALLOC_UNIT; + new_env = realloc(wenv->environ, sizeof *wenv->environ * new_cap); + if (!new_env) { + log_alloc("environment variables"); + return -1; + } + + /* Clear new pointers */ + for (i = wenv->capacity; i < new_cap; i++) + new_env[i] = 0; + + /* Update watch_env */ + wenv->capacity = new_cap; + wenv->environ = new_env; + return 0; +} + + + +/******************** + * PUBLIC INTERFACE * + ********************/ + +/* wentry_init - initialize a watch_entry with null values */ +void +wentry_init(struct watch_entry *wentry) { + if (!wentry) return; + + wentry->path = 0; + wentry->events = 0; + wentry->delay.tv_sec = 0; + wentry->delay.tv_nsec = 0; + wentry->uid = 0; + wentry->gid = 0; + wentry->chroot = 0; + wentry->command = 0; + wentry->envp = 0; + wentry->fd = -1; +} + + +/* wentry_release - free internal objects from a watch_entry */ +void +wentry_release(struct watch_entry *wentry) { + if (!wentry) return; + + free((void *)(wentry->path)); + free((void *)(wentry->chroot)); + free((void *)(wentry->command)); + wentry->path = 0; + wentry->chroot = 0; + wentry->command = 0; + + if (wentry->envp) { + size_t i = 0; + while (wentry->envp[i]) + free(wentry->envp[i++]); + free(wentry->envp); + } + wentry->envp = 0; + + if (wentry->fd != -1) + close(wentry->fd); + wentry->fd = -1; +} + + +/* wentry_free - free a watch_entry and the string it contains */ +void +wentry_free(struct watch_entry *wentry) { + if (!wentry) return; + + wentry_release(wentry); + free(wentry); +} + + +/* wentry_readline - parse a config file line and fill a struct watch_entry */ +/* Return 0 on success or -1 on failure. */ +int +wentry_readline(struct watch_entry *dest, char *line, + struct watch_env *base_env, const char *filename, unsigned line_no) { + size_t path_len = 0; + size_t event_first = 0, event_len = 0; + size_t delay_first = 0, delay_len = 0; + size_t user_first = 0, user_len = 0; + size_t chroot_first = 0, chroot_len = 0; + size_t cmd_first = 0, cmd_len = 0; + struct passwd *pw = 0; + struct group *grp = 0; + size_t i = 1; + + /* Sanity checks */ + if (!line || line[0] == 0 || line[0] == '\t') { + LOG_ASSERT(0); + return -1; + } + + /* Look for fields boundaries */ + while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++; + path_len = i; + while (line[i] == '\t') i++; + event_first = i; + while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++; + event_len = i - event_first; + while (line[i] == '\t') i++; + delay_first = i; + while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++; + delay_len = i - delay_first; + while (line[i] == '\t') i++; + user_first = i; + while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++; + user_len = i - user_first; + while (line[i] == '\t') i++; + chroot_first = i; + while (line[i] != 0 && (line[i] != '\t' || line[i-1] == '\\')) i++; + chroot_len = i - chroot_first; + while (line[i] == '\t') i++; + cmd_first = i; + while (line[i] != 0) i++; + cmd_len = i - cmd_first; + + /* Less than 3 fields found is a parse error */ + if (line[delay_first] == 0) { + log_watchtab_invalid_action(filename, line_no); + return -1; + } + + /* Adjust offsets depending on which fields are omitted */ + if (line[user_first] == 0) { + /* 3-field line: path, events, command */ + cmd_first = delay_first; + cmd_len = delay_len; + delay_first = delay_len = 0; + user_first = user_len = 0; + chroot_first = chroot_len = 0; + } + else if (line[chroot_first] == 0) { + /* 4-field line: path, events, delay, command */ + cmd_first = user_first; + cmd_len = user_len; + user_first = user_len = chroot_first = chroot_len = 0; + } + else if (line[cmd_first] == 0) { + /* 5-field line: path, events, delay, user, command */ + cmd_first = chroot_first; + cmd_len = chroot_len; + chroot_first = chroot_len = 0; + } + + /* Parse event set */ + dest->events = parse_events(line + event_first, event_len); + if (dest->events == 0) { + log_watchtab_invalid_events(filename, line_no, + line + event_first, event_len); + return -1; + } + + /* Parse delay */ + dest->delay.tv_sec = 0; + dest->delay.tv_nsec = 0; + if (delay_len > 0 + && !(delay_len == 1 && line[delay_first] == '*')) { + char *s; + + /* Decode integer part */ + dest->delay.tv_sec = strtol(line + delay_first, &s, 10); + + /* Decode fractional part if any */ + if (*s == '.') { + char *ns; + dest->delay.tv_nsec = strtol(s + 1, &ns, 10); + while (ns - s <= 9) { + dest->delay.tv_nsec *= 10; + s--; + } + s = ns; + } + + /* Check trailing non-digits */ + if (s < line + delay_first + delay_len) { + line[delay_first + delay_len] = 0; + log_watchtab_invalid_delay(filename, line_no, + line + delay_first); + return -1; + } + } + + /* Process user name and optional group name */ + if (user_len > 0) { + char *login = line + user_first; + char *group = 0; + + line[user_len] = 0; + + /* Process group */ + group = strchr(login, ':'); + if (group) { + *group = 0; + group++; + for (i = 0; group[i] >= '0' && group[i] <= '9'; i++); + errno = 0; + grp = group[i] + ? getgrnam(group) + : getgrgid(strtol(group, 0, 10)); + if (!grp) { + log_lookup_group(group); + return -1; + } + } + + /* Lookup user name */ + for (i = 0; login[i] >= '0' && login[i] <= '9'; i++); + errno = 0; + pw = login[i] + ? getpwnam(login) + : getpwuid(strtol(login, 0, 10)); + if (!pw) { + log_lookup_pw(login); + return -1; + } + } + + /* Store numeric ids */ + dest->uid = pw ? pw->pw_uid : 0; + dest->gid = grp ? grp->gr_gid : (pw ? pw->pw_gid : 0); + + /* Lookup self name if not overridden */ + if (!pw) { + char *login; + errno = 0; + login = getlogin(); + pw = login ? getpwnam(login) : 0; + if (!pw) { + log_lookup_self(); + return -1; + } + } + + /* At this point, no parse error can occur, filling in data */ + + /* Clean up destination */ + wentry_release(dest); + + /* Copy string parameters */ + dest->path = strdupesc(line, path_len); + dest->command = strdupesc(line + cmd_first, cmd_len); + + if (chroot_len > 0) + dest->chroot = strdupesc(line + chroot_first, chroot_len); + else + dest->chroot = 0; + + /* Setup environment */ + wenv_set(base_env, "LOGNAME", pw->pw_name, 1); + wenv_set(base_env, "USER", pw->pw_name, 1); + wenv_set(base_env, "HOME", pw->pw_dir, 0); + wenv_set(base_env, "TRIGGER", dest->path, 1); + dest->envp = wenv_dup(base_env); + + return 0; +} + + + +/*********************** + * WATCH_ENV INTERFACE * + ***********************/ + +/* wenv_init - create an empty environment list */ +int +wenv_init(struct watch_env *wenv) { + if (!wenv) { + LOG_ASSERT(0); + return -1; + } + + wenv->capacity = WENV_ALLOC_UNIT; + wenv->size = 0; + wenv->environ = malloc(sizeof *wenv->environ * wenv->capacity); + if (!wenv->environ) { + log_alloc("initial environment variables"); + return -1; + } + + return 0; +} + + +/* wenv_release - free string memory in a struct watch_env but not the struct*/ +void +wenv_release(struct watch_env *wenv) { + free(wenv->environ); + wenv->size = 0; + wenv->environ = 0; +} + + +/* wenv_add - append a string to an existing struct watch_env */ +int +wenv_add(struct watch_env *wenv, const char *env_str) { + if (!wenv || !env_str) { + LOG_ASSERT(0); + return -1; + } + + /* Increase array size if needed */ + if (wenv_resize(wenv, wenv->size + 2) < 0) return -1; + + /* Store a copy of the provided string */ + wenv->environ[wenv->size] = strdup(env_str); + wenv->size++; + return 0; +} + +/* wenv_set - insert or reset an environment variable */ +int +wenv_set(struct watch_env *wenv, const char *name, const char *value, + int overwrite) { + size_t namelen, linelen, i; + char *line; + + if (!wenv || !name || !value) return -1; + + /* Initialize if needed */ + if (!wenv->environ && wenv_init(wenv) < 0) + return -1; + + /* Build the environment line */ + namelen = strlen(name); + linelen = namelen + 1 + strlen(value); + line = malloc(linelen + 1); + if (!line) { + log_alloc("environment variable entry"); + return -1; + } + strncpy(line, name, namelen); + line[namelen] = '='; + strncpy(line + namelen + 1, value, linelen - (namelen + 1)); + line[linelen] = 0; + + /* Look for an existing entry for the name */ + i = 0; + while (wenv->environ[i]) { + if (strncmp(wenv->environ[i], line, namelen + 1) == 0) + break; + i++; + } + + /* If not found, insert the crafted line */ + if (wenv->environ[i] == 0) { + if (wenv_resize(wenv, wenv->size + 2) < 0) { + free(line); + return -1; + } + wenv->environ[wenv->size] = line; + wenv->size++; + return 0; + } + + /* Exit when environment variable exist but overwriting is forbidden */ + if (!overwrite) return 0; + + /* Replace found variable with new environment line */ + free((void *)wenv->environ[i]); + wenv->environ[i] = line; + return 0; +} + +/* wenv_get - lookup environment variable */ +const char * +wenv_get(struct watch_env *wenv, const char *name) { + size_t namelen, i; + + if (!wenv || !wenv->environ || !name) { + LOG_ASSERT(0); + return 0; + } + namelen = strlen(name); + + for (i = 0; wenv->environ[i]; i++) { + if (strncmp(wenv->environ[i], name, namelen) == 0 + && wenv->environ[i][namelen] == '=') + return wenv->environ[i] + (namelen + 1); + } + + return 0; +} + + +/* wenv_dup - deep copy environment strings */ +char ** +wenv_dup(struct watch_env *wenv) { + char **result; + size_t len, i; + int reported = 0; + + if (!wenv) return 0; + len = (wenv->environ ? wenv->size : 0); + result = malloc((len + 1) * sizeof *result); + if (!result) { + log_alloc("environment duplicate"); + return 0; + } + + for (i = 0; i < len; i++) { + result[i] = strdup(wenv->environ[i]); + if (!result[i] && !reported) { + log_alloc("environment item duplication"); + reported = 1; + } + } + result[len] = 0; + + return result; +} + + + +/********************** + * WATCHTAB INTERFACE * + **********************/ + +/* wtab_release - release children objects but not the struct watchtab */ +void +wtab_release(struct watchtab *tab) { + struct watch_entry *entry = 0; + + if (!tab) return; + + while ((entry = SLIST_FIRST(tab)) != 0) { + SLIST_REMOVE_HEAD(tab, next); + wentry_free(entry); + } +} + + +/* wtab_readfile - parse the given file to build a new watchtab */ +int +wtab_readfile(struct watchtab *tab, FILE *input, const char *filename) { + char *line = 0; + size_t linecap = 0; + ssize_t linelen; + unsigned line_no = 0; + struct watch_entry *entry = 0; + int result = 0; + size_t i, skip; + struct watch_env env; + + if (!tab) { + LOG_ASSERT(0); + return -1; + } + + /* Setup default environment */ + wenv_init(&env); + wenv_set(&env, "SHELL", "/bin/sh", 1); + wenv_set(&env, "PATH", "/usr/bin:/bin", 1); + + /* Read the input data */ + while ((linelen = getdelim(&line, &linecap, '\n', input)) >= 0) { + line_no++; + + /* Skip leading blanks */ + skip = 0; + while (line[skip] == ' ' || line[skip] == '\t') skip++; + + /* Trim trailing blanks */ + while ((size_t)linelen > skip && (line[linelen-1] == '\n' + || line[linelen-1] == '\r' || line[linelen-1] == ' ' + || line[linelen-1] == '\t')) + linelen--; + line[linelen] = 0; + + /* Ignore empty lines and comments */ + if ((size_t)linelen <= skip || line[skip] == '#') + continue; + + /* + * Define environment lines as lines having an '=' before any + * tabulation ('\t') or backslash ('\\'). + */ + + i = skip; + while (line[i] != 0 && line[i] != '=' + && line[i] != '\\' && line[i] != '\t') + i++; + + /* Record an environment variable */ + if (line[i] == '=') { + /* Compute bounds of variable name */ + size_t j = i - 1; + while (line[j] == ' ' && j > skip) j--; + if (j + 1 < i) line[j + 1] = 0; + + /* Compute bounds of variable value */ + j = i + 1; + while (line[j] == ' ') j++; + + /* Set the variable */ + wenv_set(&env, line + skip, line + j, 1); + continue; + } + + /* Parse an entry line */ + entry = malloc(sizeof *entry); + if (!entry) { + log_alloc("watchtab entry"); + return -1; + } + wentry_init(entry); + if (wentry_readline(entry, line + skip, &env, + filename, line_no) < 0) { + /* propagate an error but keep parsing */ + result = -1; + wentry_free(entry); + continue; + } + + /* Insert the entry in the list */ + SLIST_INSERT_HEAD(tab, entry, next); + } + + if (ferror(input)) { + log_watchtab_read(); + return -1; + } + + return result; +} diff --git a/watchtab.h b/watchtab.h @@ -0,0 +1,113 @@ +/* watchtab.h - configuration tables for file watches */ + +/* + * Copyright (c) 2013, Natacha Porté + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef FILEWATCHER_WATCHTAB_H +#define FILEWATCHER_WATCHTAB_H + +#include <stdio.h> +#include <sys/queue.h> +#include <sys/types.h> +#include <unistd.h> + + +/******************** + * TYPE DEFINITIONS * + ********************/ + +/* struct watch_entry - a single watch table entry */ +struct watch_entry { + const char *path; /* file path to watch */ + u_int events; /* vnode event set to watch */ + struct timespec delay; /* delay before running command */ + uid_t uid; /* uid to set before command */ + gid_t gid; /* gid to set before command */ + const char *chroot; /* path to chroot before command */ + const char *command; /* command to execute */ + char **envp; /* environment variables */ + int fd; /* file descriptor in kernel queue */ + SLIST_ENTRY(watch_entry) next; +}; + +/* struct watchtab - list of watchtab entries */ +SLIST_HEAD(watchtab, watch_entry); + +/* struct watch_env - dynamic table of environment variables */ +struct watch_env { + const char **environ; /* environment strings */ + size_t size; /* index of the last NULL pointer */ + size_t capacity; /* number of string slot available */ +}; + + +/******************** + * PUBLIC INTERFACE * + ********************/ + +/* wentry_init - initialize a watch_entry with null values */ +void +wentry_init(struct watch_entry *wentry); + +/* wentry_release - free internal objects from a watch_entry */ +void +wentry_release(struct watch_entry *wentry); + +/* wentry_free - free a watch_entry and the strinigs it contains */ +void +wentry_free(struct watch_entry *wentry); + +/* wentry_readline - parse a config file line and fill a struct watch_entry */ +int +wentry_readline(struct watch_entry *dest, char *line, + struct watch_env *base_env, const char *filename, unsigned line_no); + + +/* wenv_init - create an empty environment list */ +int +wenv_init(struct watch_env *wenv); + +/* wenv_release - free string memory in a struct watch_env but not the struct*/ +void +wenv_release(struct watch_env *wenv); + +/* wenv_add - append a string to an existing struct watch_env */ +int +wenv_add(struct watch_env *wenv, const char *env_str); + +/* wenv_set - insert or reset an environment variable */ +int +wenv_set(struct watch_env *wenv, const char *name, const char *value, + int overwrite); + +/* wenv_get - lookup environment variable */ +const char * +wenv_get(struct watch_env *wenv, const char *name); + +/* wenv_dup - deep copy environment strings */ +char ** +wenv_dup(struct watch_env *wenv); + + +/* wtab_release - release children objects but not the struct watchtab */ +void +wtab_release(struct watchtab *tab); + +/* wtab_readfile - parse the given file to build a new watchtab */ +int +wtab_readfile(struct watchtab *tab, FILE *input, const char *filename); + +#endif /* ndef FILEWATCHER_WATCHTAB_H */