/*	$CoreSDI: attr_classic.c,v 1.24 2001/11/16 22:52:59 claudio Exp $	*/

/*
 *   Copyright (c) 2000, 2001, Core SDI S.A., Argentina
 *   All rights reserved
 *  
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *  
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *   2. 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.
 *   3. Neither name of the Core SDI S.A. nor the names of its contributors
 *      may be used to endorse or promote products derived from this software
 *      without specific prior written permission.
 *  
 *   THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
 */

/*
 * Classic attribute module to handle standard syslog files.
 * Author: Claudio Castiglia
 */

#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/wait.h> /* XXX: remove when using built-in gzip */

#ifdef __linux__
#include <netinet/in.h>		/* in_addr_t */
#endif

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include <unistd.h>

#include "sysdep.h"
#include "resource.h"
#include "packet.h"
#include "modtypes.h"
#include "iaargs.h"
#include "log.h"

#define ATTR_CLASSIC	"attr classic: "
#define OFFSET_PREFIX	"FileOffset_"		/* offset resource prefix */

typedef struct _classic_ctx {
	char		*logset_name;		/* logfile name      */
	struct stat	 st;			/* logfile stat      */
	RESOURCE	*roffset;		/* offset resource   */
	off_t		 offset;		/* logfile offset    */
	off_t		 pos;			/* internal variable */
	off_t		 prev_size;		/* internal variable */
	int		 fd;			/* internal variable */
} CLASSIC_CTX;
#define GET_CLASSIC_CTX(c)	(CLASSIC_CTX *) (c + 1)


/*
 * _type():
 *	Fills a string with the logfile type: "regular" or "other".
 */
static char *
_type(char *dst, mode_t mode, size_t dst_size)
{
	snprintf(dst, dst_size, "%s", (S_ISREG(mode) ? "regular" : "other"));
	return (dst);
}


/*
 * _uperm():
 *	Fills a string with user permissions.
 */
static char *
_perm(char *dst, mode_t mode, size_t rflag, size_t wflag, size_t xflag)
{
	dst[0] = dst[1] = dst[2] = '-';
	if (mode & rflag)
		dst[0] = 'r';
	if (mode & wflag)
		dst[1] = 'w';
	if (mode & xflag)
		dst[2] = 'x';
	dst[3] = '\0';
	return (dst);
}


/*
 * _time():
 *	Do a ctime_r(3) and remove the ending '\n'.
 */
static char *
_time(time_t *t, char *dst)
{
	ctime_r(t, dst);
	dst[strlen(dst) - 1] = '\0';
	return (dst);
}


/*
 * _load_offset():
 *	Search offset resource data and update offset context member.
 */
static void
_load_offset(ATTRCON *context)
{
	char		 rname[MAXPATHLEN];
	CLASSIC_CTX	*cc;

	cc = GET_CLASSIC_CTX(context);
	if (cc->offset >= 0)
		return;

	snprintf(rname, sizeof(rname), OFFSET_PREFIX "%s", cc->logset_name);

	/* Get offset resource */
	if (context->s->s_rlist != NULL)
		cc->roffset = res_find(context->s->s_rlist, rname);

	/* Convert resource offset to off_t */
	if (cc->roffset == NULL)
		cc->offset = 0;
	else {
		if (cc->roffset->dsize == sizeof(cc->offset))
			cc->offset = *(off_t *)cc->roffset->data;
		else {
			log_warn(ATTR_CLASSIC "Invalid resource size: %s. "
			    "Ignoring.", rname);
			cc->offset = 0;
		}
	}
}

 
/*
 * _save_offset:
 *	Save current offset into the resource list.
 *	Return 0 on success and -1 on error and set errno.
 */
static int
_save_offset(ATTRCON *context)
{
	char		 rname[MAXPATHLEN];
	int		 error;
	CLASSIC_CTX	*cc;

	cc = GET_CLASSIC_CTX(context);
	if (cc->offset < 0)
		return (0);

	error = 0;
	if (context->s->s_rlist != NULL) {
		/* If resource does not exists create one */
		if (cc->roffset == NULL) {
			snprintf(rname, sizeof(rname), OFFSET_PREFIX "%s",
			    cc->logset_name);
			error = res_add(context->s->s_rlist, rname, &cc->offset,
			    sizeof(cc->offset));
		} else 
			/* If resource exists replace current data */
			error = res_replace(context->s->s_rlist, cc->roffset,
			    &cc->offset, sizeof(cc->offset));
	}	
	return (error);
}


/*
 * _copy():
 *	Copy data from one fd to another or same fd.
 */
static int
_copy(int dstfd, int srcfd, off_t dstoff, off_t srcoff, off_t size)
{
	char	buf[BUFSIZ];
	ssize_t	to_read, to_write;

	if (lseek(dstfd, dstoff, SEEK_SET) < 0 ||
	    lseek(srcfd, srcoff, SEEK_SET) < 0)
		return (-1);

	while (size) {
		to_read = (size > sizeof(buf)) ? sizeof(buf) : size;
		to_write = read(srcfd, buf, to_read);
		if (to_write < 0)
			return (-1);
		if (dstfd == srcfd) {
			if (lseek(dstfd, dstoff, SEEK_SET) < 0)
				return (-1);
			srcoff += to_write;
		}
		if (write(dstfd, buf, to_write) < 0)
			return (-1);
		if (dstfd == srcfd) {
			if (lseek(srcfd, srcoff, SEEK_SET) < 0)
				return (-1);
			dstoff += to_write;
		}
		size -= to_write;
	}
	return (0);
}


/*
 * _compress:
 *	Gzip logfile.
 * XXX: use built-in gzip (auditd uses zlib)
 */
static void
_compress(const char *rpath)
{
	pid_t pid;

	if ( (pid = fork()) == 0) {
		execl("/usr/bin/gzip", "gzip", "-f" , rpath, NULL);
		exit(0);
	}
	waitpid(pid, NULL, 0);
}


/*
 * _get_logset_name()
 */
static void
_get_logset_name(ATTRCON *context, struct attrargs_ret *args)
{
	CLASSIC_CTX *cc;

	cc = GET_CLASSIC_CTX(context);
	args->data = cc->logset_name;
	args->size = strlen(cc->logset_name);
}


/*
 * _freeze():
 *	Freeze logfile status; return 0 on success and
 *	-1 on error (the error message is logged).
 */
static int
_freeze(ATTRCON *context)
{
	CLASSIC_CTX *cc;

	cc = GET_CLASSIC_CTX(context);
	if (stat(cc->logset_name, &cc->st) < 0) {
		log_err(ATTR_CLASSIC "Can't stat '%s': %s.", cc->logset_name,
		    strerror(errno));
		return (-1);
	}
	return (0);
}


/*
 * _info():
 *	Return logset information coded as a string:
 *	"classic
 *		name <logset_name>
 *		offset <offset>
 *		size <size>
 *		uid <uid>
 *		gid <gid>
 *		type <type>
 *		perms <uperm> <gperm> <operm>
 *		last_access <time>
 *		last_modification <time>
 *		last_status <time>"
 */
static void
_info(ATTRCON *context, struct attrargs_ret *args)
{
	CLASSIC_CTX *cc;
	char	     str_type[10],
		     str_uperm[5],
		     str_gperm[5],
		     str_operm[5],
		     str_atime[26],
		     str_mtime[26],
		     str_ctime[26];

	cc = GET_CLASSIC_CTX(context);
	_load_offset(context);
	snprintf(args->data, args->size,
	    "classic\n\tname\t%s\n\toffset\t%lu\n\tsize\t%lu\n"
	    "\tuid\t%u\n\tgid\t%u\n\ttype\t%s\n\tperms\tu%s g%s o%s\n\t"
	    "last_access\t%s\n\tlast_modifi\t%s\n\tlast_status\t%s\n",
	    cc->logset_name,
	    (unsigned long int) cc->offset,
	    (unsigned long int) cc->st.st_size,
	    (unsigned int) cc->st.st_uid,
	    (unsigned int) cc->st.st_gid,
	    _type(str_type, cc->st.st_mode, sizeof(str_type)),
	    _perm(str_uperm, cc->st.st_mode, S_IRUSR, S_IWUSR, S_IXUSR),
	    _perm(str_gperm, cc->st.st_mode, S_IRGRP, S_IWGRP, S_IXGRP),
	    _perm(str_operm, cc->st.st_mode, S_IROTH, S_IWOTH, S_IXOTH),
	    _time(&cc->st.st_atime, str_atime),
	    _time(&cc->st.st_mtime, str_mtime),
	    _time(&cc->st.st_ctime, str_ctime));
}


/*
 * _get():
 *	Send logfile to the client;
 *	return 0 on success and -1 on error and set errno.
 */
static int
_get(ATTRCON *context)
{
	int		 fd;
	CLASSIC_CTX	*cc;

	cc = GET_CLASSIC_CTX(context);
	log_debug(ATTR_CLASSIC "Getting %s.", cc->logset_name);

	/* Open logfile */
	if ( (fd = open(cc->logset_name, O_RDONLY, 0)) < 0) {
		log_err("%s: %s.", cc->logset_name, strerror(errno));
		return (-1);
	}
		
	/* Send destination file name */
	packet_put_string(context->s->s_packet, cc->logset_name);

	/*
	 * If size < offset something wrong occurred,
	 * auditor should detect it.
	 */
	_load_offset(context);
	if (cc->st.st_size < cc->offset)
		cc->offset = 0;

	/* Send logs from current offset */
	cc->offset = packet_write(context->s->s_packet, fd, cc->offset);

	close(fd);
	return (0);
}


/*
 * _zap():
 *	Zap logfile and reset file offset.
 *	Return codes:
 *		-1 on error;
 *		 0 if on zap end;
 *		 1 if this should be called again
 *		   to complete the zap procedure.
 */
static int
_zap(ATTRCON *context, int last_call)
{
	CLASSIC_CTX	*cc;
	off_t		 len;

	cc = GET_CLASSIC_CTX(context);
	log_debug(ATTR_CLASSIC "Zapping %s, pass %d.", cc->logset_name,
	    last_call);

	/* First time ? */
	if (cc->prev_size < 0) {

		/* Return 0 if can't truncate */
		_load_offset(context);
		if (cc->offset == 0)
			return (0);

		/* Open logfile */
		if ( (cc->fd = open(cc->logset_name, O_RDWR, 0)) < 0) {
			log_err(ATTR_CLASSIC "Can't open '%s': %s.",
			    cc->logset_name, strerror(errno));
			return (-1);
		}

		/* Actualize internal variables */
		cc->prev_size = cc->offset;
		cc->offset = cc->pos = 0;
	}

	/* Move new unreaded data to the beginning */
	len = cc->st.st_size - cc->prev_size;
	if (len >= 0) {
		if (_copy(cc->fd, cc->fd, cc->pos, cc->prev_size, len) < 0) {
			log_err(ATTR_CLASSIC "Can't zap '%s': %s.",
			    cc->logset_name, strerror(errno));
			cc->prev_size = -1;
			close(cc->fd);
			return (-1);
		}

		cc->pos += len;
		if (last_call) {
			cc->prev_size = -1;
			close(cc->fd);
			if (truncate(cc->logset_name, cc->pos) < 0) {
				log_err(ATTR_CLASSIC "Can't truncate '%s': %s.",
				    cc->logset_name, strerror(errno));
				return (-1);
			}

			return (0);
		}

		cc->prev_size = cc->st.st_size;
	}

	/* Call me again */
	return (1);	//XXX
}


/*
 * _rotate():
 *	Rotate logfile and zap it.
 */
static int
_rotate(ATTRCON *context, int last_call)
{
	CLASSIC_CTX	*cc;
	char		 rpath[MAXPATHLEN];
	int		 destfd, i;
	off_t		 len;

	cc = GET_CLASSIC_CTX(context);
	log_debug(ATTR_CLASSIC "Rotating %s, pass %d.", cc->logset_name,
	    last_call);

	/* First time ? */
	if (cc->prev_size < 0) {

		/* Return 0 if can't rotate */
		_load_offset(context);
		if (cc->offset == 0)
			return (0);

		/* Get logfile.x.gz name */
		i = 0;
		do {
			snprintf(rpath, sizeof(rpath), "%s.%d.gz",
			    cc->logset_name, i++);
		} while (!access(rpath, F_OK));
		rpath[strlen(rpath) - 3] = '\0';

		/* Open logfile and destination file */
		cc->fd = open(cc->logset_name, O_RDWR, 0);
		destfd = open(rpath, O_CREAT | O_RDWR,
		    cc->st.st_mode | S_IWUSR | S_IRUSR);
		if (cc->fd < 0 || destfd < 0) {
			if (cc->fd >= 0)
				close(cc->fd);
			if (destfd >= 0)
				close(destfd);
			log_err(ATTR_CLASSIC "Can't open '%s' or '%s': %s.",
			    cc->logset_name, rpath, strerror(errno));
			return (-1);
		}
		
		/* Move audited data to destination file and compress it */
		if (_copy(destfd, cc->fd, 0, 0, cc->offset) < 0) {
			log_err(ATTR_CLASSIC "Can't copy '%s' into '%s': %s.",
			    cc->logset_name, rpath, strerror(errno));
			cc->prev_size = -1;
			close(cc->fd);
			close(destfd);
			return (-1);
		}

		close(destfd);
		_compress(rpath);

		/* Actualize internal variables */
		cc->prev_size = cc->offset;
		cc->offset = cc->pos = 0;
	}

	/* Normal zap process */

	/* Move unreaded data to the beginning of logfile */
	len = cc->st.st_size - cc->prev_size;
	if (len >= 0) {
		if (_copy(cc->fd, cc->fd, cc->pos, cc->prev_size, len) < 0) {
			log_err(ATTR_CLASSIC "Can't rotate '%s': %s.",
			    cc->logset_name, strerror(errno));
			cc->prev_size = -1;
			close(cc->fd);
			return (-1);
		}

		cc->pos += len;
		if (last_call) {
			cc->prev_size = -1;
			close(cc->fd);
			if (truncate(cc->logset_name, cc->pos) < 0) {
				log_err(ATTR_CLASSIC "Can't truncate '%s': %s.",
				    cc->logset_name, strerror(errno));
				return (-1);
			}
		}

		cc->prev_size = cc->st.st_size;
	}

	/* Call me again */
	return (1);	//XXX
}


/*
 * _sign():
 *	Sign logfile.
 */
static int
_sign(ATTRCON *context)
{
	errno = EOPNOTSUPP;	/* XXX */
	log_err(ATTR_CLASSIC "SIGN: %s.", strerror(errno));
	return (-1);
}


/*
 * release():
 *	Release context (ATTRCON members are not released).
 */
void
release(ATTRCON *context)
{
	CLASSIC_CTX *cc;

	if (context != NULL) {
		cc = GET_CLASSIC_CTX(context);
		if (_save_offset(context) < 0)
			log_err(ATTR_CLASSIC "Can't release context: %s.",
			    strerror(errno));
		if (cc->logset_name != NULL)
			free(cc->logset_name);
		free(context);
	}
}


/*
 * init():
 *	Initialize module;
 *	on success return a pointer to the attribute context or NULL on error.
 */
ATTRCON *
init(struct attrargs_init *args)
{
	static const char	*module_name = "classic";
	ATTRCON			*context;
	CLASSIC_CTX		*cc;

	log_debug(ATTR_CLASSIC "Initializing.");

	if (args->argc < 2) {
		errno = EINVAL;
		log_err(ATTR_CLASSIC "Can't initialize %s.", strerror(errno));
		return (NULL);
	}

	context = (ATTRCON *) calloc(1, sizeof(ATTRCON) + sizeof(CLASSIC_CTX));
	if (context == NULL) {
		log_err(ATTR_CLASSIC "Can't create context: %s.",
		    strerror(errno));
		return (NULL);
	}

	cc = GET_CLASSIC_CTX(context);
	cc->logset_name = strdup(args->argv[1]);
	if (cc->logset_name == NULL) {
		log_err(ATTR_CLASSIC "Can't create context: %s.",
		    strerror(errno));
		free(context);
		return (NULL);
	}

	cc->offset = -1;
	cc->prev_size = -1;
	context->attr_name = module_name;
	return (context);
}


/*
 * proc_entry()
 */
int
proc_entry(int opcode, ATTRCON *context, void *args)
{
	switch (opcode) {
	case ATTR_GET_LOGSET_NAME:
		_get_logset_name(context, args);
		return (0);
	case ATTR_FREEZE:
		return (_freeze(context));
	case ATTR_INFO:
		_info(context, args);
		return (0);
	case ATTR_GET:
		return (_get(context));
	case ATTR_ZAP:
		return (_zap(context, (int) args));
	case ATTR_ROTATE:
		return (_rotate(context, (int) args));
	case ATTR_SIGN:
		return (_sign(context));
	default:
		errno = EINVAL;
	}

	log_err(ATTR_CLASSIC "Invalid '%d' command.", opcode);
	return (-1);
}


