%{
/*-
 * Copyright (c) 2012 The FreeBSD Foundation
 * All rights reserved.
 *
 * This software was developed by Pawel Jakub Dawidek under sponsorship from
 * the FreeBSD Foundation.
 *
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS 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 AUTHORS 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.
 *
 * $P4: //depot/projects/trustedbsd/openbsm/bin/auditdistd/parse.y#5 $
 */

#include <config/config.h>

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/sysctl.h>

#include <arpa/inet.h>

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>
#ifndef HAVE_STRLCPY
#include <compat/strlcpy.h>
#endif

#include "auditdistd.h"
#include "pjdlog.h"

extern int depth;
extern int lineno;

extern FILE *yyin;
extern char *yytext;

static struct adist_config *lconfig;
static struct adist_host *curhost;
#define	SECTION_GLOBAL		0
#define	SECTION_SENDER		1
#define	SECTION_RECEIVER	2
static int cursection;

/* Sender section. */
static char depth1_source[ADIST_ADDRSIZE];
static int depth1_checksum;
static int depth1_compression;
/* Sender and receiver sections. */
static char depth1_directory[PATH_MAX];

static bool adjust_directory(char *path);
static bool family_supported(int family);

extern void yyrestart(FILE *);
%}

%token CB
%token CERTFILE
%token DIRECTORY
%token FINGERPRINT
%token HOST
%token KEYFILE
%token LISTEN
%token NAME
%token OB
%token PASSWORD
%token PIDFILE
%token RECEIVER REMOTE
%token SENDER SOURCE
%token TIMEOUT

/*
%type <num> checksum_type
%type <num> compression_type
*/

%union
{
	int num;
	char *str;
}

%token <num> NUM
%token <str> STR

%%

statements:
	|
	statements statement
	;

statement:
	name_statement
	|
	pidfile_statement
	|
	timeout_statement
	|
	sender_statement
	|
	receiver_statement
	;

name_statement:	NAME STR
	{
		PJDLOG_RASSERT(depth == 0,
		    "The name variable can only be specificed in the global section.");

		if (lconfig->adc_name[0] != '\0') {
			pjdlog_error("The name variable is specified twice.");
			free($2);
			return (1);
		}
		if (strlcpy(lconfig->adc_name, $2,
		    sizeof(lconfig->adc_name)) >=
		    sizeof(lconfig->adc_name)) {
			pjdlog_error("The name value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

pidfile_statement:	PIDFILE STR
	{
		PJDLOG_RASSERT(depth == 0,
		    "The pidfile variable can only be specificed in the global section.");

		if (lconfig->adc_pidfile[0] != '\0') {
			pjdlog_error("The pidfile variable is specified twice.");
			free($2);
			return (1);
		}
		if (strcmp($2, "none") != 0 && $2[0] != '/') {
			pjdlog_error("The pidfile variable must be set to absolute pathname or \"none\".");
			free($2);
			return (1);
		}
		if (strlcpy(lconfig->adc_pidfile, $2,
		    sizeof(lconfig->adc_pidfile)) >=
		    sizeof(lconfig->adc_pidfile)) {
			pjdlog_error("The pidfile value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

timeout_statement:	TIMEOUT NUM
	{
		PJDLOG_ASSERT(depth == 0);

		lconfig->adc_timeout = $2;
	}
	;

sender_statement:	SENDER sender_start sender_entries CB
	{
		PJDLOG_ASSERT(depth == 0);
		PJDLOG_ASSERT(cursection == SECTION_SENDER);

		/* Configure defaults. */
		if (depth1_checksum == -1)
			depth1_checksum = ADIST_CHECKSUM_NONE;
		if (depth1_compression == -1)
			depth1_compression = ADIST_COMPRESSION_NONE;
		if (depth1_directory[0] == '\0') {
			(void)strlcpy(depth1_directory, ADIST_DIRECTORY_SENDER,
			    sizeof(depth1_directory));
		}
		/* Empty depth1_source is ok. */
		TAILQ_FOREACH(curhost, &lconfig->adc_hosts, adh_next) {
			if (curhost->adh_role != ADIST_ROLE_SENDER)
				continue;
			if (curhost->adh_checksum == -1)
				curhost->adh_checksum = depth1_checksum;
			if (curhost->adh_compression == -1)
				curhost->adh_compression = depth1_compression;
			if (curhost->adh_directory[0] == '\0') {
				(void)strlcpy(curhost->adh_directory,
				    depth1_directory,
				    sizeof(curhost->adh_directory));
			}
			if (curhost->adh_localaddr[0] == '\0') {
				(void)strlcpy(curhost->adh_localaddr,
				    depth1_source,
				    sizeof(curhost->adh_localaddr));
			}
		}
		cursection = SECTION_GLOBAL;
	}
	;

sender_start:	OB
	{
		PJDLOG_ASSERT(depth == 1);
		PJDLOG_ASSERT(cursection == SECTION_GLOBAL);

		cursection = SECTION_SENDER;
		depth1_checksum = -1;
		depth1_compression = -1;
		depth1_source[0] = '\0';
		depth1_directory[0] = '\0';

#ifndef HAVE_AUDIT_SYSCALLS
		pjdlog_error("Sender functionality is not available.");
		return (1);
#endif
	}
	;

sender_entries:
	|
	sender_entries sender_entry
	;

sender_entry:
	source_statement
	|
	directory_statement
/*
	|
	checksum_statement
	|
	compression_statement
*/
	|
	sender_host_statement
	;

receiver_statement:	RECEIVER receiver_start receiver_entries CB
	{
		PJDLOG_ASSERT(depth == 0);
		PJDLOG_ASSERT(cursection == SECTION_RECEIVER);

		/*
		 * If not listen addresses were specified,
		 * configure default ones.
		 */
		if (TAILQ_EMPTY(&lconfig->adc_listen)) {
			struct adist_listen *lst;

			if (family_supported(AF_INET)) {
				lst = calloc(1, sizeof(*lst));
				if (lst == NULL) {
					pjdlog_error("Unable to allocate memory for listen address.");
					return (1);
				}
				(void)strlcpy(lst->adl_addr,
				    ADIST_LISTEN_TLS_TCP4,
				    sizeof(lst->adl_addr));
				TAILQ_INSERT_TAIL(&lconfig->adc_listen, lst, adl_next);
			} else {
				pjdlog_debug(1,
				    "No IPv4 support in the kernel, not listening on IPv4 address.");
			}
			if (family_supported(AF_INET6)) {
				lst = calloc(1, sizeof(*lst));
				if (lst == NULL) {
					pjdlog_error("Unable to allocate memory for listen address.");
					return (1);
				}
				(void)strlcpy(lst->adl_addr,
				    ADIST_LISTEN_TLS_TCP6,
				    sizeof(lst->adl_addr));
				TAILQ_INSERT_TAIL(&lconfig->adc_listen, lst, adl_next);
			} else {
				pjdlog_debug(1,
				    "No IPv6 support in the kernel, not listening on IPv6 address.");
			}
			if (TAILQ_EMPTY(&lconfig->adc_listen)) {
				pjdlog_error("No address to listen on.");
				return (1);
			}
		}
		/* Configure defaults. */
		if (depth1_directory[0] == '\0') {
			(void)strlcpy(depth1_directory,
			    ADIST_DIRECTORY_RECEIVER,
			    sizeof(depth1_directory));
		}
		TAILQ_FOREACH(curhost, &lconfig->adc_hosts, adh_next) {
			if (curhost->adh_role != ADIST_ROLE_RECEIVER)
				continue;
			if (curhost->adh_directory[0] == '\0') {
				if (snprintf(curhost->adh_directory,
				    sizeof(curhost->adh_directory), "%s/%s",
				    depth1_directory, curhost->adh_name) >=
				    (ssize_t)sizeof(curhost->adh_directory)) {
					pjdlog_error("Directory value is too long.");
					return (1);
				}
			}
		}
		cursection = SECTION_GLOBAL;
	}
	;

receiver_start:	OB
	{
		PJDLOG_ASSERT(depth == 1);
		PJDLOG_ASSERT(cursection == SECTION_GLOBAL);

		cursection = SECTION_RECEIVER;
		depth1_directory[0] = '\0';
	}
	;

receiver_entries:
	|
	receiver_entries receiver_entry
	;

receiver_entry:
	listen_statement
	|
	directory_statement
	|
	certfile_statement
	|
	keyfile_statement
	|
	receiver_host_statement
	;

/*
checksum_statement:	CHECKSUM checksum_type
	{
		PJDLOG_ASSERT(cursection == SECTION_SENDER);

		switch (depth) {
		case 1:
			depth1_checksum = $2;
			break;
		case 2:
			PJDLOG_ASSERT(curhost != NULL);
			curhost->adh_checksum = $2;
			break;
		default:
			PJDLOG_ABORT("checksum at wrong depth level");
		}
	}
	;

checksum_type:
	NONE		{ $$ = ADIST_CHECKSUM_NONE; }
	|
	CRC32		{ $$ = ADIST_CHECKSUM_CRC32; }
	|
	SHA256		{ $$ = ADIST_CHECKSUM_SHA256; }
	;

compression_statement:	COMPRESSION compression_type
	{
		PJDLOG_ASSERT(cursection == SECTION_SENDER);

		switch (depth) {
		case 1:
			depth1_compression = $2;
			break;
		case 2:
			PJDLOG_ASSERT(curhost != NULL);
			curhost->adh_compression = $2;
			break;
		default:
			PJDLOG_ABORT("compression at wrong depth level");
		}
	}
	;

compression_type:
	NONE		{ $$ = ADIST_COMPRESSION_NONE; }
	|
	LZF		{ $$ = ADIST_COMPRESSION_LZF; }
	;
*/

directory_statement:	DIRECTORY STR
	{
		PJDLOG_ASSERT(cursection == SECTION_SENDER ||
		    cursection == SECTION_RECEIVER);

		switch (depth) {
		case 1:
			if (strlcpy(depth1_directory, $2,
			    sizeof(depth1_directory)) >=
			    sizeof(depth1_directory)) {
				pjdlog_error("Directory value is too long.");
				free($2);
				return (1);
			}
			if (!adjust_directory(depth1_directory))
				return (1);
			break;
		case 2:
			if (cursection == SECTION_SENDER || $2[0] == '/') {
				if (strlcpy(curhost->adh_directory, $2,
				    sizeof(curhost->adh_directory)) >=
				    sizeof(curhost->adh_directory)) {
					pjdlog_error("Directory value is too long.");
					free($2);
					return (1);
				}
			} else /* if (cursection == SECTION_RECEIVER) */ {
				if (depth1_directory[0] == '\0') {
					pjdlog_error("Directory path must be absolute.");
					free($2);
					return (1);
				}
				if (snprintf(curhost->adh_directory,
				    sizeof(curhost->adh_directory), "%s/%s",
				    depth1_directory, $2) >=
				    (ssize_t)sizeof(curhost->adh_directory)) {
					pjdlog_error("Directory value is too long.");
					free($2);
					return (1);
				}
			}
			break;
		default:
			PJDLOG_ABORT("directory at wrong depth level");
		}
		free($2);
	}
	;

source_statement:	SOURCE STR
	{
		PJDLOG_RASSERT(cursection == SECTION_SENDER,
		    "The source variable must be in sender section.");

		switch (depth) {
		case 1:
			if (strlcpy(depth1_source, $2,
			    sizeof(depth1_source)) >=
			    sizeof(depth1_source)) {
				pjdlog_error("Source value is too long.");
				free($2);
				return (1);
			}
			break;
		case 2:
			if (strlcpy(curhost->adh_localaddr, $2,
			    sizeof(curhost->adh_localaddr)) >=
			    sizeof(curhost->adh_localaddr)) {
				pjdlog_error("Source value is too long.");
				free($2);
				return (1);
			}
			break;
		}
		free($2);
	}
	;

fingerprint_statement:	FINGERPRINT STR
	{
		PJDLOG_ASSERT(cursection == SECTION_SENDER);
		PJDLOG_ASSERT(depth == 2);

		if (strncasecmp($2, "SHA256=", 7) != 0) {
			pjdlog_error("Invalid fingerprint value.");
			free($2);
			return (1);
		}
		if (strlcpy(curhost->adh_fingerprint, $2,
		    sizeof(curhost->adh_fingerprint)) >=
		    sizeof(curhost->adh_fingerprint)) {
			pjdlog_error("Fingerprint value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

password_statement:	PASSWORD STR
	{
		PJDLOG_ASSERT(cursection == SECTION_SENDER ||
		    cursection == SECTION_RECEIVER);
		PJDLOG_ASSERT(depth == 2);

		if (strlcpy(curhost->adh_password, $2,
		    sizeof(curhost->adh_password)) >=
		    sizeof(curhost->adh_password)) {
			pjdlog_error("Password value is too long.");
			bzero($2, strlen($2));
			free($2);
			return (1);
		}
		bzero($2, strlen($2));
		free($2);
	}
	;

certfile_statement:	CERTFILE STR
	{
		PJDLOG_ASSERT(cursection == SECTION_RECEIVER);
		PJDLOG_ASSERT(depth == 1);

		if (strlcpy(lconfig->adc_certfile, $2,
		    sizeof(lconfig->adc_certfile)) >=
		    sizeof(lconfig->adc_certfile)) {
			pjdlog_error("Certfile value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

keyfile_statement:	KEYFILE STR
	{
		PJDLOG_ASSERT(cursection == SECTION_RECEIVER);
		PJDLOG_ASSERT(depth == 1);

		if (strlcpy(lconfig->adc_keyfile, $2,
		    sizeof(lconfig->adc_keyfile)) >=
		    sizeof(lconfig->adc_keyfile)) {
			pjdlog_error("Keyfile value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

listen_statement:	LISTEN STR
	{
		struct adist_listen *lst;

		PJDLOG_ASSERT(depth == 1);
		PJDLOG_ASSERT(cursection == SECTION_RECEIVER);

		lst = calloc(1, sizeof(*lst));
		if (lst == NULL) {
			pjdlog_error("Unable to allocate memory for listen address.");
			free($2);
			return (1);
		}
		if (strlcpy(lst->adl_addr, $2, sizeof(lst->adl_addr)) >=
		    sizeof(lst->adl_addr)) {
			pjdlog_error("listen argument is too long.");
			free($2);
			free(lst);
			return (1);
		}
		TAILQ_INSERT_TAIL(&lconfig->adc_listen, lst, adl_next);
		free($2);
	}
	;

sender_host_statement:	HOST host_start OB sender_host_entries CB
	{
		/* Put it onto host list. */
		TAILQ_INSERT_TAIL(&lconfig->adc_hosts, curhost, adh_next);
		curhost = NULL;
	}
	;

receiver_host_statement:	HOST host_start OB receiver_host_entries CB
	{
		/* Put it onto host list. */
		TAILQ_INSERT_TAIL(&lconfig->adc_hosts, curhost, adh_next);
		curhost = NULL;
	}
	;

host_start:	STR
	{
		/* Check if there is no duplicate entry. */
		TAILQ_FOREACH(curhost, &lconfig->adc_hosts, adh_next) {
			if (strcmp(curhost->adh_name, $1) != 0)
				continue;
			if (curhost->adh_role == ADIST_ROLE_SENDER &&
			    cursection == SECTION_RECEIVER) {
				continue;
			}
			if (curhost->adh_role == ADIST_ROLE_RECEIVER &&
			    cursection == SECTION_SENDER) {
				continue;
			}
			pjdlog_error("%s host %s is configured more than once.",
			    curhost->adh_role == ADIST_ROLE_SENDER ?
			    "Sender" : "Receiver", curhost->adh_name);
			free($1);
			return (1);
		}

		curhost = calloc(1, sizeof(*curhost));
		if (curhost == NULL) {
			pjdlog_error("Unable to allocate memory for host configuration.");
			free($1);
			return (1);
		}
		if (strlcpy(curhost->adh_name, $1, sizeof(curhost->adh_name)) >=
		    sizeof(curhost->adh_name)) {
			pjdlog_error("Host name is too long.");
			free($1);
			return (1);
		}
		free($1);
		curhost->adh_role = cursection == SECTION_SENDER ?
		    ADIST_ROLE_SENDER : ADIST_ROLE_RECEIVER;
		curhost->adh_version = ADIST_VERSION;
		curhost->adh_localaddr[0] = '\0';
		curhost->adh_remoteaddr[0] = '\0';
		curhost->adh_remote = NULL;
		curhost->adh_directory[0] = '\0';
		curhost->adh_password[0] = '\0';
		curhost->adh_fingerprint[0] = '\0';
		curhost->adh_worker_pid = 0;
		curhost->adh_conn = NULL;
	}
	;

sender_host_entries:
	|
	sender_host_entries sender_host_entry
	;

sender_host_entry:
	source_statement
	|
	remote_statement
	|
	directory_statement
	|
	fingerprint_statement
	|
	password_statement
/*
	|
	checksum_statement
	|
	compression_statement
*/
	;

receiver_host_entries:
	|
	receiver_host_entries receiver_host_entry
	;

receiver_host_entry:
	remote_statement
	|
	directory_statement
	|
	password_statement
	;

remote_statement:	REMOTE STR
	{
		PJDLOG_ASSERT(depth == 2);
		PJDLOG_ASSERT(cursection == SECTION_SENDER ||
		    cursection == SECTION_RECEIVER);

		if (strlcpy(curhost->adh_remoteaddr, $2,
		    sizeof(curhost->adh_remoteaddr)) >=
		    sizeof(curhost->adh_remoteaddr)) {
			pjdlog_error("Remote value is too long.");
			free($2);
			return (1);
		}
		free($2);
	}
	;

%%

static bool
family_supported(int family)
{
	int sock;

	sock = socket(family, SOCK_STREAM, 0);
	if (sock == -1 && errno == EPROTONOSUPPORT)
		return (false);
	if (sock >= 0)
		(void)close(sock);
	return (true);
}

static bool
adjust_directory(char *path)
{
	size_t len;

	len = strlen(path);
	for (;;) {
		if (len == 0) {
			pjdlog_error("Directory path is empty.");
			return (false);
		}
		if (path[len - 1] != '/')
			break;
		len--;
		path[len] = '\0';
	}
	if (path[0] != '/') {
		pjdlog_error("Directory path must be absolute.");
		return (false);
	}
	return (true);
}

static int
my_name(char *name, size_t size)
{
	char buf[MAXHOSTNAMELEN];
	char *pos;

	if (gethostname(buf, sizeof(buf)) < 0) {
		pjdlog_errno(LOG_ERR, "gethostname() failed");
		return (-1);
	}

	/* First component of the host name. */
	pos = strchr(buf, '.');
	if (pos == NULL)
		(void)strlcpy(name, buf, size);
	else
		(void)strlcpy(name, buf, MIN((size_t)(pos - buf + 1), size));

	if (name[0] == '\0') {
		pjdlog_error("Empty host name.");
		return (-1);
	}

	return (0);
}

void
yyerror(const char *str)
{

	pjdlog_error("Unable to parse configuration file at line %d near '%s': %s",
	    lineno, yytext, str);
}

struct adist_config *
yy_config_parse(const char *config, bool exitonerror)
{
	int ret;

	curhost = NULL;
	cursection = SECTION_GLOBAL;
	depth = 0;
	lineno = 0;

	lconfig = calloc(1, sizeof(*lconfig));
	if (lconfig == NULL) {
		pjdlog_error("Unable to allocate memory for configuration.");
		if (exitonerror)
			exit(EX_TEMPFAIL);
		return (NULL);
	}
	TAILQ_INIT(&lconfig->adc_hosts);
	TAILQ_INIT(&lconfig->adc_listen);
	lconfig->adc_name[0] = '\0';
	lconfig->adc_timeout = -1;
	lconfig->adc_pidfile[0] = '\0';
	lconfig->adc_certfile[0] = '\0';
	lconfig->adc_keyfile[0] = '\0';

	yyin = fopen(config, "r");
	if (yyin == NULL) {
		pjdlog_errno(LOG_ERR, "Unable to open configuration file %s",
		    config);
		yy_config_free(lconfig);
		if (exitonerror)
			exit(EX_OSFILE);
		return (NULL);
	}
	yyrestart(yyin);
	ret = yyparse();
	fclose(yyin);
	if (ret != 0) {
		yy_config_free(lconfig);
		if (exitonerror)
			exit(EX_CONFIG);
		return (NULL);
	}

	/*
	 * Let's see if everything is set up.
	 */
	if (lconfig->adc_name[0] == '\0' && my_name(lconfig->adc_name,
	    sizeof(lconfig->adc_name)) == -1) {
		yy_config_free(lconfig);
		if (exitonerror)
			exit(EX_CONFIG);
		return (NULL);
	}
	if (lconfig->adc_timeout == -1)
		lconfig->adc_timeout = ADIST_TIMEOUT;
	if (lconfig->adc_pidfile[0] == '\0') {
		(void)strlcpy(lconfig->adc_pidfile, ADIST_PIDFILE,
		    sizeof(lconfig->adc_pidfile));
	}
	if (lconfig->adc_certfile[0] == '\0') {
		(void)strlcpy(lconfig->adc_certfile, ADIST_CERTFILE,
		    sizeof(lconfig->adc_certfile));
	}
	if (lconfig->adc_keyfile[0] == '\0') {
		(void)strlcpy(lconfig->adc_keyfile, ADIST_KEYFILE,
		    sizeof(lconfig->adc_keyfile));
	}

	return (lconfig);
}

void
yy_config_free(struct adist_config *config)
{
	struct adist_host *adhost;
	struct adist_listen *lst;

	while ((lst = TAILQ_FIRST(&config->adc_listen)) != NULL) {
		TAILQ_REMOVE(&config->adc_listen, lst, adl_next);
		free(lst);
	}
	while ((adhost = TAILQ_FIRST(&config->adc_hosts)) != NULL) {
		TAILQ_REMOVE(&config->adc_hosts, adhost, adh_next);
		bzero(adhost, sizeof(*adhost));
		free(adhost);
	}
	free(config);
}