/* Simple daemon logger for RSBAC
 *
 * rklog (c) - Stanislav I. Ievlev, 2000-2001
 * RSBAC (c) - Amon Ott., 1995-2001
 * Some parts from klogd (c) 1995 Martin Schulze <Martin.Schulze@Linux.DE>
 * Some changes made by Amon Ott, 2000
 * Copyright (c) 2003 by Peter Busser <peter@trusteddebian.org>
 * Performed some code cleanup.
 */

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>
#include <string.h>
#include <pwd.h>
#include <signal.h>
#include <syslog.h>
#include <rsbac/syscalls.h>
#include <errno.h>
extern int errno;

#include "debug.h"
#include "pidfile.h"

char	*hostname=NULL;
struct sockaddr_in remote_server;
int	use_proc=1;

#define INPUT_LOG	"/proc/rsbac-info/rmsg"
#define OUTPUT_LOG	"/security-log"
#define SECOFF_UID	400
#define	PATH_VARRUN	"/var/run/"
static char *pidfile = PATH_VARRUN"rklogd.pid";

#define MAX_LINE	8100
#define PORTNUM		1500


int check_father( pid_t father )
{
	if( kill( father, 0 ) && errno == ESRCH ) {
		debugstr( "father process is dead\n" );
                return 0;
	}else{
		debugstr( "father process is alive\n" );
		return 1;
	}
}


void stop_work( int sig )
{
	syslog( LOG_INFO, "security kernel logger stopped" );
	
	debugstr( "exiting...\n" );

	remove_pid( pidfile );
	if( !use_proc ) {
		char tmpstring[0];

		/* Close kernel log buffer */
		rsbac_log( 0, tmpstring, sizeof( tmpstring ) );
	}

	exit( EXIT_SUCCESS );
}


char *getlogname( uid_t secoffuid )
{
	struct passwd *pwd = NULL;
	char *retval = NULL;

	pwd = getpwuid( secoffuid );
	if( pwd != NULL ) {
		if ( asprintf(&retval,"%s/log/"OUTPUT_LOG, pwd->pw_dir) <= 0){
			perror("asprintf");
			exit(EXIT_FAILURE);
		}
	} else {
		fprintf( stderr, "Non-existent user id %u\n",secoffuid );
		exit(EXIT_FAILURE);
	}

	return retval;
}


void open_out_net( char *servername )
{
	struct hostent *hp = NULL;

	if( (hp = gethostbyname( servername )) == NULL ) {
		fprintf(stderr, "cannot resolve hostname:%s",hstrerror(h_errno));
		exit( EXIT_FAILURE );
	}

	memset( &remote_server, 0, sizeof( remote_server ) );
	memcpy( &remote_server.sin_addr, hp->h_addr, hp->h_length );

	remote_server.sin_family = hp->h_addrtype;
	remote_server.sin_port = htons( PORTNUM );
}

void write_net( char *data )
{
	struct sockaddr_in clnt_addr;
	int fd;
	int count;
    
	debugstr( "writing to net...\n" );

	if( (fd = socket( AF_INET, SOCK_DGRAM, 0 )) < 0 ) {
		syslog( LOG_WARNING, "cannot open socket to send messages to remote host" );
	}

	memset( &clnt_addr, 0, sizeof( clnt_addr ) );
	clnt_addr.sin_family = AF_INET;
	clnt_addr.sin_addr.s_addr = INADDR_ANY;

	if( bind( fd, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr) ) < 0 ) {
		syslog( LOG_WARNING, "cannot bind socket to send messages to remote host" );
	}

	count = sendto( fd, data, strlen( data ), 0,
			(struct sockaddr*)&remote_server,
			sizeof( remote_server ) );
	if( count <= 0 ) {
		syslog( LOG_ERR, "error during sending messages to remote host" );
	}
}


int open_in( void )
{
	int fd;

	if( use_proc ) {
		if( (fd = open( INPUT_LOG, O_RDONLY|O_NOFOLLOW )) < 0 ) {
			perror( "open input log:");
			exit( EXIT_FAILURE );
		}
	} else {
		char tmpstring[0];

		/* open kernel log buffer */
		if( rsbac_log( 1, tmpstring, sizeof(tmpstring) ) < 0 ) {
			fputs("open of kernel log buffer", stderr);
			exit( EXIT_FAILURE );
		}
	}

	return fd;
}


int open_out( char *name )
{
	int fd = -1;
	if( (fd = open( name, O_WRONLY|O_APPEND|O_CREAT|O_NOFOLLOW, 00600 ) ) < 0 ) {
		perror("open output log:");
		exit( EXIT_FAILURE );
	}
	fchmod( fd, 0600 );

	return fd;
}


int open_in_net( void )
{
	int fd = -1;
	struct sockaddr_in serv_addr;
	
	if( (fd = socket( AF_INET, SOCK_DGRAM, 0 )) < 0 ) {
		syslog( LOG_ERR, "open of network socket failed" );
		exit( EXIT_FAILURE );
	}

	memset( &serv_addr, 0, sizeof(serv_addr) );
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = INADDR_ANY;
	serv_addr.sin_port = htons( PORTNUM );
    
	if( bind( fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr) ) == -1 ) {
		syslog( LOG_ERR, "bind failed" );
		exit( EXIT_FAILURE );
	}

	return fd;
}

void write_out( int fd, char* prefix, char *buf, int count )
{
	char *point;
	char tmpstr[MAX_LINE];
	char *found;
	struct flock lock ;

	point = buf;

	/* Lock logfile */
	lock.l_type = F_WRLCK;
        lock.l_whence = SEEK_SET;
        lock.l_start = 0;
        lock.l_len = 0;

        if (fcntl(fd, F_SETLKW, &lock ))
        {
            syslog( LOG_ERR,"cannot lock output file" );
        }

    
	debugstr( "writing to log ...\n" );

	do{
		found = strchr( point, '\n' );
		if( found != NULL ) { 
			strncpy( tmpstr, point, (size_t)(found-point) + 1 );
			tmpstr[found-point + 1] = '\0';
			write( fd, prefix, strlen( prefix ) ); 
			write( fd, tmpstr, found-point + 1 );

			if( hostname != NULL ) {
				write_net( tmpstr );
			}

			found++;
			count -= found-point;
		} else {
			if( count > 0 ) {
				strncpy( tmpstr, point, count );
				tmpstr[count] = 0;
				write( fd, prefix, strlen( prefix ) );
				write( fd, tmpstr, count );

				if( hostname != NULL ) {
					write_net( tmpstr );
				}
			}
		}

		point=found;
	} while( point != NULL );

	/* ulock logfile */
	lock.l_type = F_UNLCK;

	if (fcntl(fd, F_SETLKW, &lock)){
	    syslog( LOG_ERR,"cannot lock output file" );
	}
}
 
void read_in( int fd, int out )
{
	char buf[MAX_LINE];
	char prefix[MAX_LINE];
	size_t count;
	time_t now;
	char *timestamp;

	now = time( NULL );
	timestamp = ctime( &now );
    
	memset( buf, 0, sizeof( buf ) );
	memset( prefix, 0, sizeof( prefix ) );
    
	debugstr( "reading log...\n" );

	if( use_proc ) {
		/* Read from proc */
		count = read( fd, buf, sizeof( buf ) );
	} else {
		/* Read from log kernel buffer */
		count = rsbac_log( 2, buf, sizeof( buf ) );
	}
    
	strncpy( prefix, timestamp, strlen( timestamp ) - 1 );
	strncat( prefix," :", 3 );
	write_out( out, prefix, buf, count );
}

void read_net( int fd, int out )
{
	struct sockaddr_in clnt_addr;
	socklen_t maxaddrlen;
	int count;
	char read_buf[MAX_LINE];
	char log_buf[MAX_LINE];
	char prefix[MAX_LINE];
	time_t now;
	char *timestamp;
    
	now = time( NULL );
	timestamp = ctime( &now );


	maxaddrlen = sizeof( clnt_addr );

	memset( read_buf, 0, sizeof( read_buf ) );
	memset( log_buf, 0, sizeof( log_buf ) );
	memset( prefix, 0, sizeof( prefix ) );

	debugstr( "reading net ...\n" );

	count = recvfrom( fd, read_buf, sizeof( read_buf ), 0,
			(struct sockaddr *)&clnt_addr, &maxaddrlen );
	snprintf( log_buf, MAX_LINE - 1, "from host %s :%s",
		  inet_ntoa( clnt_addr.sin_addr ), read_buf );
	log_buf[MAX_LINE - 1] = '\0';
    
	snprintf( prefix, MAX_LINE - 1, "%s :", timestamp );
	prefix[MAX_LINE - 1] = '\0';

	write_out( out, prefix, log_buf, strlen( log_buf ) );
}

int main( int argc, char *argv[] )
{
	fd_set 	read_fds;
	int 	log,net,out,null_fd;
	int 	ch,i;
	pid_t	child_pid;
	char	*logname = NULL;
	int	secoffuid = SECOFF_UID;
	int	netlisten = 0;
	int	background = 1;
    
	/* Open central system log for bug errors */
	openlog( "rklogd", LOG_PID | LOG_CONS, LOG_DAEMON );

	while( (ch = getopt( argc, argv, "bdf:ln:su:v") ) != EOF ) {
		switch( (char)ch ) {
		/* Do not switch to the background */
		case 'b':
			background = 0;
			break;

		/* Enable debugging */
		case 'd':
			enable_debug();
			break;

		/* Define an output file. */
		case 'f':
			logname = optarg;
			break;

		case 'l':
			netlisten = 1;
			break;

		case 'n':
			hostname = optarg;
			break;

		case 's':
			use_proc = 0;
			break;

		/* Define an secoff uid. */
		case 'u':
			secoffuid = strtol( optarg, 0, 0 );
			break;

		default:
			fprintf( stderr, "Unknown option in switch() (%c)\n", ch);
			exit( 1 );
		}
	}
    
	if( logname == NULL ) {
		logname = getlogname( secoffuid );
	}
			  
	debug( "secoff uid:%u\n", secoffuid );
	debug( "logname:%s\n", logname );
	debug( "netlisten:%u\n", netlisten );
	debug( "use_proc:%u\n", use_proc );

	if( hostname != NULL ) {
		debug( "remote:%s\n", hostname );
	}

    
	for( i = 1; i < NSIG; i++ ) {
		signal( i, SIG_IGN );
	}

	signal( SIGTERM, stop_work );

	child_pid = fork();
	if (child_pid <0){
	    perror("fork");
	    exit(EXIT_FAILURE);
	}
	/* Fork process, stay in child */
	if( child_pid != 0 ) {
		exit(EXIT_SUCCESS);
	}

	/* We need stay in root for create pid file */	 
	if( !check_pid( pidfile ) ) {
		if( mkdir( PATH_VARRUN "rklogd", 0700 ) && errno != EEXIST ) {
		    perror("mkdir");
		    exit(EXIT_FAILURE);
		}

		if( chown( PATH_VARRUN"rklogd", secoffuid, 0 ) ) {
		    perror("chown");
		    exit(EXIT_FAILURE);
		}

		if ((unlink(pidfile) < 0) && errno != ENOENT ){
		    perror("unlink");
		    exit(EXIT_FAILURE);
		}
		
		if( symlink( "rklogd/rklogd.pid", pidfile ) < 0) {
		    perror("symlink");
		    exit(EXIT_FAILURE);
		}

		pidfile = PATH_VARRUN"rklogd/rklogd.pid";
		if( !write_pid( pidfile ) ) {
		    fprintf(stderr,"unable to write_pid: %s\n",pidfile);
		    exit(EXIT_FAILURE);
		}
	}else{
		fputs( "rklogd: Already running.\n", stderr );
		exit(EXIT_FAILURE);
	}


	/* Change uid to secoff */
	if( setuid( secoffuid ) < 0 ) {
		perror("setuid");
		exit(EXIT_FAILURE);
	}

	/*open input and output descriptors*/
	log = open_in();
	if( netlisten ) {
		net = open_in_net();
	}

	out = open_out( logname );
	if( hostname != NULL ) {
		open_out_net( hostname );
	}

	/* redirect standart file descriptors into /dev/null */
	if ((null_fd = open("/dev/null",O_RDWR|O_NOFOLLOW))<0){
	    perror("open /dev/null for reading and writing");
	    exit(EXIT_FAILURE);
	}
	dup2(null_fd, STDIN_FILENO);
	dup2(null_fd, STDOUT_FILENO);
	dup2(null_fd, STDERR_FILENO);

	/* Finally go to daemon mode */
	setsid();

	syslog( LOG_INFO, "RSBAC system logger started" );

	/* I cannot use select() for syscalls */
	if( use_proc ) {
		/* Go to infinity loop */
		for(;;) {
			FD_ZERO( &read_fds );
			FD_SET( log, &read_fds );
			if( netlisten ) {
				FD_SET( net, &read_fds );
			}

			debugstr( "selecting...\n" );

			if( select( 20, &read_fds, NULL, NULL, NULL ) > 0 ) {
				if( FD_ISSET( log, &read_fds ) ) {
					read_in( log, out );
				}

				if( (netlisten)
				    && (FD_ISSET( net, &read_fds )) ) {
					read_net( net,out );
				}
			}
		}
	} else {
		/* Separate thread to read messages from the network */
		child_pid = fork();
		if (child_pid < 0){
		    syslog( LOG_ERR, "unable second fork" );
		    exit(EXIT_FAILURE);
		}
		if( !child_pid ) {
			pid_t parent;

			parent=getppid();
			if( !netlisten ) {
				exit( EXIT_SUCCESS );
			}

			debugstr( "running second thread...\n" );
			debug( "parent pid is %d\n", parent );

			for(;;) {
				/*read messages from the network*/
				read_net( net, out );
				if( !check_father( parent ) ) {
					/* Father process is dead */
					exit( EXIT_FAILURE );
				}
			}
		} else {    
			/* Read messages from the localhost */
			for(;;) {
				read_in( 0, out );
			}
		}
	}
}

