Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions cf-reactor/file_watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,20 @@
#include <watcher.h>
#include <logging.h>
#include <alloc.h>
#include <file_lib.h> /* safe_fopen() */
#include <hash.h> /* HashDigestFromId() */
#include <conversion.h> /* IntFromString() */
#include <prototypes3.h> /* cf_strtimestamp_local() */
#include <sys/stat.h>
#include <errno.h>
#include <openssl/evp.h>

/* Minimum time between two logs of a persisting failure to check the file,
* since the file is checked on every poll */
#define CHECK_ERROR_LOG_INTERVAL_SECS 60

#define CONTENT_HASH_METHOD HASH_METHOD_SHA256

typedef struct
{
char *path;
Expand All @@ -42,6 +48,37 @@

/* What the last successful check observed */
bool last_exists;

/* What the last successful check observed, depending on the event type */
union
{
/* FileWatcherCheckFileModified() */
struct timespec mtime;

/* FileWatcherCheckFileContentModified() */
struct
{
bool hashed; /* Whether it was a regular file, that was hashed */
unsigned char digest[EVP_MAX_MD_SIZE + 1];
} content;

/* FileWatcherCheckFilePermissionsChanged() */
mode_t mode;

/* FileWatcherCheckFileOwnerChanged() */
struct
{
uid_t uid;
gid_t gid;
} owner;

/* FileWatcherCheckFileSizeExceeds() */
struct
{
off_t threshold; /* Not observed, set when the state is created */
off_t size;
} file_size;
} last;
} FileWatcherState;


Expand All @@ -56,6 +93,19 @@
return fws;
}

void *FileWatcherSizeStateNew(const Rlist *args)
{
assert(args != NULL && args->next != NULL);
const char *path = RlistScalarValue(args);
const long size_threshold = IntFromString(RlistScalarValue(args->next));

FileWatcherState *fws = (FileWatcherState *) xcalloc(1, sizeof(FileWatcherState));
fws->path = xstrdup(path);
fws->last.file_size.threshold = size_threshold;

return fws;
}

void FileWatcherStateDestroy(void *state)
{
FileWatcherState *fws = state;
Expand Down Expand Up @@ -173,3 +223,211 @@
return FileWatcherRecordObservation(fws, exists, deleted, "deletion");
}

// =========== code for the file_created event ===========

WatcherCheckResult FileWatcherCheckFileCreated(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
if (!FileLstat(fws, &exists, NULL))
{
return WATCHER_CHECK_ERROR;
}

const bool created = !fws->last_exists && exists;
return FileWatcherRecordObservation(fws, exists, created, "creation");
}

// =========== code for the file_modified event ===========

static struct timespec StatMtime(const struct stat *sb)
{
assert(sb != NULL);

#if defined(HAVE_STRUCT_STAT_ST_MTIM)
return sb->st_mtim;
#elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
return sb->st_mtimespec;
#else
return (struct timespec) { .tv_sec = sb->st_mtime, .tv_nsec = 0 };
#endif
}

WatcherCheckResult FileWatcherCheckFileModified(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
struct stat sb;
if (!FileLstat(fws, &exists, &sb))
{
return WATCHER_CHECK_ERROR;
}

const struct timespec mtime = exists ? StatMtime(&sb) : (struct timespec) { 0 };

/* Only modifications of a file present at both checks */
const bool modified = fws->last_exists && exists &&
((fws->last.mtime.tv_sec != mtime.tv_sec) ||
(fws->last.mtime.tv_nsec != mtime.tv_nsec));

fws->last.mtime = mtime;
return FileWatcherRecordObservation(fws, exists, modified, "modification");
}

// =========== code for the file_content_modified event ===========

/* Hashes the content of the file into digest. Returns false and sets errno
* on failure. */
static bool HashFileContent(const char *path, unsigned char digest[EVP_MAX_MD_SIZE + 1])
{
assert(path != NULL);

FILE *file = safe_fopen(path, "rb");
if (file == NULL)
{
return false;
}

EVP_MD_CTX *context = EVP_MD_CTX_create();
if (context == NULL ||
EVP_DigestInit_ex(context, HashDigestFromId(CONTENT_HASH_METHOD), NULL) != 1)
{
EVP_MD_CTX_destroy(context);
fclose(file);
errno = ENOMEM;
return false;
}

unsigned char buffer[4096];
size_t read_count;
while ((read_count = fread(buffer, 1, sizeof(buffer), file)) > 0)
{
EVP_DigestUpdate(context, buffer, read_count);
}

const bool success = !ferror(file);
const int read_errno = errno;
fclose(file);

memset(digest, 0, EVP_MAX_MD_SIZE + 1);
EVP_DigestFinal_ex(context, digest, NULL);
EVP_MD_CTX_destroy(context);

errno = read_errno;
return success;
}

WatcherCheckResult FileWatcherCheckFileContentModified(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
struct stat sb;
if (!FileLstat(fws, &exists, &sb))
{
return WATCHER_CHECK_ERROR;
}

bool hashed = false;
unsigned char digest[EVP_MAX_MD_SIZE + 1] = { 0 };
if (exists && S_ISREG(sb.st_mode))
{
if (HashFileContent(fws->path, digest))
{
hashed = true;
}
else if (errno != ENOENT && errno != ENOTDIR) /* Not deleted since lstat() */
{
LogCheckError(fws, "hash", errno);
return WATCHER_CHECK_ERROR;
}
}

/* Only content changes of a regular file present at both checks */
const bool modified = fws->last.content.hashed && hashed &&
(memcmp(fws->last.content.digest, digest, sizeof(digest)) != 0);

fws->last.content.hashed = hashed;
memcpy(fws->last.content.digest, digest, sizeof(digest));
return FileWatcherRecordObservation(fws, exists, modified, "content modification");
}

// =========== code for the file_permissions_changed event ===========

WatcherCheckResult FileWatcherCheckFilePermissionsChanged(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
struct stat sb;
if (!FileLstat(fws, &exists, &sb))
{
return WATCHER_CHECK_ERROR;
}

const mode_t mode = exists ? sb.st_mode : 0;

/* Only changes of a file present at both checks */
const bool changed = fws->last_exists && exists &&
((fws->last.mode & 07777) != (mode & 07777));

fws->last.mode = mode;
return FileWatcherRecordObservation(fws, exists, changed, "permissions change");
}

// =========== code for the file_owner_changed event ===========

WatcherCheckResult FileWatcherCheckFileOwnerChanged(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
struct stat sb;
if (!FileLstat(fws, &exists, &sb))
{
return WATCHER_CHECK_ERROR;
}

const uid_t uid = exists ? sb.st_uid : 0;
const gid_t gid = exists ? sb.st_gid : 0;

/* Only changes of a file present at both checks */
const bool changed = fws->last_exists && exists &&
((fws->last.owner.uid != uid) || (fws->last.owner.gid != gid));

fws->last.owner.uid = uid;
fws->last.owner.gid = gid;
return FileWatcherRecordObservation(fws, exists, changed, "owner change");
}

// =========== code for the file_size_exceeds event ===========

WatcherCheckResult FileWatcherCheckFileSizeExceeds(void *state)
{
assert(state != NULL);
FileWatcherState *fws = state;

bool exists;
struct stat sb;
if (!FileLstat(fws, &exists, &sb))
{
return WATCHER_CHECK_ERROR;
}

const off_t size = exists ? sb.st_size : 0;

/* Only when the size grows above the threshold, including a file created
* above it, not every time it is checked above it */
const bool exceeded = fws->last_exists && (fws->last.file_size.size > fws->last.file_size.threshold);
const bool exceeds = exists && (size > fws->last.file_size.threshold);

fws->last.file_size.size = size;
return FileWatcherRecordObservation(fws, exists, !exceeded && exceeds, "size exceeding threshold");
}
10 changes: 9 additions & 1 deletion cf-reactor/file_watcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@


void *FileWatcherStateNew(const Rlist *args);
WatcherCheckResult FileWatcherCheckFileDeleted(void *state);
void *FileWatcherSizeStateNew(const Rlist *args);
void FileWatcherStateDestroy(void *state);

WatcherCheckResult FileWatcherCheckFileDeleted(void *state);
WatcherCheckResult FileWatcherCheckFileCreated(void *state);
WatcherCheckResult FileWatcherCheckFileModified(void *state);
WatcherCheckResult FileWatcherCheckFileContentModified(void *state);
WatcherCheckResult FileWatcherCheckFilePermissionsChanged(void *state);
WatcherCheckResult FileWatcherCheckFileOwnerChanged(void *state);
WatcherCheckResult FileWatcherCheckFileSizeExceeds(void *state);

#endif
13 changes: 13 additions & 0 deletions cf-reactor/reactor_transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ static const EventArg NO_EXTRA_ARGS[] =
{ NULL, RVAL_TYPE_NOPROMISEE }
};

static const EventArg FILE_SIZE_EXCEEDS_ARGS[] =
{
{ REACTOR_SIZE_THRESHOLD, RVAL_TYPE_SCALAR },
{ NULL, RVAL_TYPE_NOPROMISEE }
};

#define EventTypeNew(name, type, args, new_state, destroy_state, check) \
{ name, type, args, new_state, destroy_state, check }
#define EventTypeNewNull() EventTypeNew(NULL, RVAL_TYPE_NOPROMISEE, NULL, NULL, NULL, NULL)
Expand All @@ -169,6 +175,13 @@ static const EventArg NO_EXTRA_ARGS[] =
static const EventType EVENT_TABLE[] =
{
FileEventTypeNew(REACTOR_FILE_DELETED, FileWatcherCheckFileDeleted),
FileEventTypeNew(REACTOR_FILE_CREATED, FileWatcherCheckFileCreated),
FileEventTypeNew(REACTOR_FILE_MODIFIED, FileWatcherCheckFileModified),
FileEventTypeNew(REACTOR_FILE_CONTENT_MODIFIED, FileWatcherCheckFileContentModified),
FileEventTypeNew(REACTOR_FILE_PERMISSIONS_CHANGED, FileWatcherCheckFilePermissionsChanged),
FileEventTypeNew(REACTOR_FILE_OWNER_CHANGED, FileWatcherCheckFileOwnerChanged),
EventTypeNew(REACTOR_FILE_SIZE_EXCEEDS, RVAL_TYPE_SCALAR, FILE_SIZE_EXCEEDS_ARGS,
FileWatcherSizeStateNew, FileWatcherStateDestroy, FileWatcherCheckFileSizeExceeds),
EventTypeNewNull()
};

Expand Down
7 changes: 7 additions & 0 deletions libpromises/mod_reactor.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ static const ConstraintSyntax when_constraints[] =

/* Row models */
ConstraintSyntaxNewString(REACTOR_FILE_DELETED, CF_ANYSTRING, "File to react for on deletion", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_CREATED, CF_ANYSTRING, "File to react for on creation", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_MODIFIED, CF_ANYSTRING, "File to react for on change of its modification time", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_CONTENT_MODIFIED, CF_ANYSTRING, "File to react for on change of its content hash", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_PERMISSIONS_CHANGED, CF_ANYSTRING, "File to react for on change of its permissions", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_OWNER_CHANGED, CF_ANYSTRING, "File to react for on change of its owning user or group", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewString(REACTOR_FILE_SIZE_EXCEEDS, CF_ANYSTRING, "File to react for when its size grows above '" REACTOR_SIZE_THRESHOLD "'", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewInt(REACTOR_SIZE_THRESHOLD, CF_VALRANGE, "Size in bytes above which '" REACTOR_FILE_SIZE_EXCEEDS "' fires", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewInt("interval", REACTOR_INTERVAL_RANGE, "Seconds between two checks for the event", SYNTAX_STATUS_NORMAL),
ConstraintSyntaxNewNull()
};
Expand Down
7 changes: 7 additions & 0 deletions libpromises/mod_reactor.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
#include <cf3.defs.h>

#define REACTOR_FILE_DELETED "file_deleted"
#define REACTOR_FILE_CREATED "file_created"
#define REACTOR_FILE_MODIFIED "file_modified"
#define REACTOR_FILE_CONTENT_MODIFIED "file_content_modified"
#define REACTOR_FILE_PERMISSIONS_CHANGED "file_permissions_changed"
#define REACTOR_FILE_OWNER_CHANGED "file_owner_changed"
#define REACTOR_FILE_SIZE_EXCEEDS "file_size_exceeds"
#define REACTOR_SIZE_THRESHOLD "size_threshold"

/* Range of the 'interval' attribute of the 'when' body of events promises */
#define REACTOR_INTERVAL_RANGE "1,99999999999"
Expand Down
Loading
Loading