/*
 *  Copyright (C) 2015-2019 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 *
 *  Authors: Mickey Sola
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 */

#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif


#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <pthread.h>

#if defined(FANOTIFY)
#include <sys/fanotify.h>
#endif

#include "shared/optparser.h"
#include "shared/output.h"

#include "libclamav/others.h"
#include "../misc/priv_fts.h"
#include "../misc/utils.h"
#include "../client/client.h"
#include "thread.h"

static pthread_mutex_t onas_scan_lock = PTHREAD_MUTEX_INITIALIZER;


static int onas_scan(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code);
static cl_error_t onas_scan_safe(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code);
static cl_error_t onas_scan_thread_scanfile(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code);
static cl_error_t onas_scan_thread_handle_dir(struct onas_scan_event *event_data, const char *pathname);
static cl_error_t onas_scan_thread_handle_file(struct onas_scan_event *event_data, const char *pathname);

static void onas_scan_thread_exit(int sig);

static void onas_scan_thread_exit(int sig)
{
    logg("*ScanOnAccess: onas_scan_thread_exit(), signal %d\n", sig);

    pthread_exit(NULL);
}

/**
 * @brief Safe-scan wrapper, originally used by inotify and fanotify threads, now exists for error checking/convenience.
 *
 * Owned by scanthread to try and force multithreaded client archtiecture which better avoids kernel level deadlocks from
 * fanotify blocking/prevention.
 */
static int onas_scan(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code)
{
    int ret = 0;
    int i = 0;
    uint8_t retry_on_error = event_data->bool_opts & ONAS_SCTH_B_RETRY_ON_E;

    ret = onas_scan_safe(event_data, fname, sb, infected, err, ret_code);

    if (*err) {
        switch (*ret_code) {
            case CL_EACCES:
            case CL_ESTAT:

                logg("*ClamMisc: internal issue (daemon could not access directory/file %s)\n", fname);
                break;
                /* TODO: handle other errors */
            case CL_EPARSE:
            case CL_EREAD:
            case CL_EWRITE:
            case CL_EMEM:
            case CL_ENULLARG:
            default:
                logg("~ClamMisc: internal issue (client failed to scan)\n");
        }
	    if (retry_on_error) {
		    logg("*ClamMisc: reattempting scan ... \n");
		    while (err) {
			    ret = onas_scan_safe(event_data, fname, sb, infected, err, ret_code);

			    i++;
			    if (*err && i == event_data->retry_attempts) {
				    *err = 0;
			    }
		    }
	    }
    }

    return ret;
}

/**
 * @brief Thread-safe scan wrapper to ensure there's no processs contention over use of the socket.
 *
 * This is noticeably slower, and I had no issues running smaller scale tests with it off, but better than sorry until more testing can be done.
 *
 * TODO: make this configurable?
 */
static cl_error_t onas_scan_safe(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code) {

	int ret = 0;
	int fd = 0;

#if defined(FANOTIFY)
	uint8_t b_fanotify;

	b_fanotify = event_data->bool_opts & ONAS_SCTH_B_FANOTIFY ? 1 : 0;

	if (b_fanotify) {
		fd = event_data->fmd->fd;
	}
#endif

	pthread_mutex_lock(&onas_scan_lock);


	ret = onas_client_scan(event_data->tcpaddr, event_data->portnum, event_data->scantype, event_data->maxstream,
			fname, fd, event_data->timeout, sb, infected, err, ret_code);

	pthread_mutex_unlock(&onas_scan_lock);

	return ret;
}

static cl_error_t onas_scan_thread_scanfile(struct onas_scan_event *event_data, const char *fname, STATBUF sb, int *infected, int *err, cl_error_t *ret_code) {


#if defined(FANOTIFY)
	struct fanotify_response res;
	uint8_t b_fanotify;
#endif

        int ret = 0;

	uint8_t b_scan;
	uint8_t b_deny_on_error;

	if (NULL == event_data || NULL == fname || NULL == infected || NULL == err || NULL == ret_code) {
		logg("!ClamWorker: scan failed (NULL arg given)\n");
		return CL_ENULLARG;
	}

	b_scan = event_data->bool_opts & ONAS_SCTH_B_SCAN ? 1 : 0;
        b_deny_on_error = event_data->bool_opts & ONAS_SCTH_B_DENY_ON_E ? 1 : 0;

#if defined(FANOTIFY)
        b_fanotify = event_data->bool_opts & ONAS_SCTH_B_FANOTIFY ? 1 : 0;
	if (b_fanotify) {
		res.fd = event_data->fmd->fd;
		res.response = FAN_ALLOW;
	}
#endif

	if (b_scan) {
		ret = onas_scan(event_data, fname, sb, infected, err, ret_code);

		if (*err && *ret_code != CL_SUCCESS) {
			logg("*ClamWorker: scan failed with error code %d\n", *ret_code);
		}

#if defined(FANOTIFY)
		if (b_fanotify) {
			if ((*err && *ret_code && b_deny_on_error) || *infected) {
				res.response = FAN_DENY;
			}
		}
#endif
	}

#if defined(FANOTIFY)
	if (b_fanotify) {
		if(event_data->fmd->mask & FAN_ALL_PERM_EVENTS) {
			ret = write(event_data->fan_fd, &res, sizeof(res));
			if(ret == -1) {
				logg("!ClamWorker: internal error (can't write to fanotify)\n");
				if (errno == ENOENT) {
					logg("*ClamWorker: permission event has already been written ... recovering ...\n");
				} else {
					ret = CL_EWRITE;
				}
			}
		}
	}

	if (b_fanotify) {

#ifdef ONAS_DEBUG
		logg("*ClamWorker: closing fd, %d)\n", event_data->fmd->fd);
#endif
		if (-1 == close(event_data->fmd->fd) ) {

			logg("!ClamWorker: internal error (can't close fanotify meta fd, %d)\n", event_data->fmd->fd);
			if (errno == EBADF) {
				logg("*ClamWorker: fd already closed ... recovering ...\n");
			} else {
				ret = CL_EUNLINK;
			}
		}
	}
#endif
	return ret;
}

static cl_error_t onas_scan_thread_handle_dir(struct onas_scan_event *event_data, const char *pathname) {
	FTS *ftsp = NULL;
	int32_t ftspopts = FTS_NOCHDIR | FTS_PHYSICAL | FTS_XDEV;
	FTSENT *curr = NULL;

	int32_t infected = 0;
	int32_t err = 0;
	cl_error_t ret_code = CL_SUCCESS;
	cl_error_t ret = CL_SUCCESS;

	int32_t fres = 0;
	STATBUF sb;

	char *const pathargv[] = {(char *)pathname, NULL};

	if (!(ftsp = _priv_fts_open(pathargv, ftspopts, NULL))) {
		ret = CL_EOPEN;
		goto out;
	}

	while ((curr = _priv_fts_read(ftsp))) {
		if (curr->fts_info != FTS_D) {

			fres = CLAMSTAT(curr->fts_path, &sb);

			if (event_data->sizelimit) {
				if (fres != 0 || sb.st_size > event_data->sizelimit)  {
					/* okay to skip w/o allow/deny since dir comes from inotify
					 * events and (probably) won't block w/ protection enabled */
					event_data->bool_opts &= ((uint16_t)  ~ONAS_SCTH_B_SCAN);
					logg("*ClamWorker: size limit surpassed while doing extra scanning ... skipping object ...\n");
				}
			}

			ret = onas_scan_thread_scanfile(event_data, curr->fts_path, sb, &infected, &err, &ret_code);
		}
	}

out:
	if(ftsp) {
		_priv_fts_close(ftsp);
	}

	return ret;
}

static cl_error_t onas_scan_thread_handle_file(struct onas_scan_event *event_data, const char *pathname) {

	STATBUF sb;
	int32_t infected = 0;
	int32_t err = 0;
	cl_error_t ret_code = CL_SUCCESS;
	int fres = 0;
	cl_error_t ret = 0;

	if (NULL == pathname || NULL == event_data) {
		return CL_ENULLARG;
	}

	fres = CLAMSTAT(pathname, &sb);
	if (event_data->sizelimit) {
		if (fres != 0 || sb.st_size > event_data->sizelimit)  {
			/* don't skip so we avoid lockups, but don't scan either;
			 * while it should be obvious, this will unconditionally set
			 * the bit in the map to 0 regardless of original orientation */
			event_data->bool_opts &= ((uint16_t) ~ONAS_SCTH_B_SCAN);
		}
	}

	ret = onas_scan_thread_scanfile(event_data, pathname, sb, &infected, &err, &ret_code);

#ifdef ONAS_DEBUG
	/* very noisy, debug only */
	if (event_data->bool_opts & ONAS_SCTH_B_INOTIFY) {
		logg("*ClamWorker: Inotify Scan Results ...\n\tret = %d ...\n\tinfected = %d ...\n\terr = %d ...\n\tret_code = %d\n",
				ret, infected, err, ret_code);
	} else {
		logg("*ClamWorker: Fanotify Scan Results ...\n\tret = %d ...\n\tinfected = %d ...\n\terr = %d ...\n\tret_code = %d\n\tfd = %d\n",
				ret, infected, err, ret_code, event_data->fmd->fd);
	}
#endif

	return ret;
}

/**
 * @brief worker thread designed to work with the lovely c-thread-pool library to handle our scanning jobs after our queue thread consumes an event
 *
 * @param arg this should always be an onas_scan_event struct
 */
void *onas_scan_worker(void *arg) {

	struct onas_scan_event *event_data = (struct onas_scan_event *) arg;

	uint8_t b_dir;
	uint8_t b_file;
	uint8_t b_inotify;
	uint8_t b_fanotify;

	if (NULL == event_data || NULL == event_data->pathname) {
		logg("ClamWorker: invalid worker arguments for scanning thread\n");
		if (event_data) {
			logg("ClamWorker: pathname is null\n");
		}
		goto done;
	}

	/* load in boolean info from event struct; makes for easier reading--you're welcome */
	b_dir = event_data->bool_opts & ONAS_SCTH_B_DIR ? 1 : 0;
	b_file = event_data->bool_opts & ONAS_SCTH_B_FILE ? 1 : 0;
	b_inotify = event_data->bool_opts & ONAS_SCTH_B_INOTIFY ? 1 : 0;
	b_fanotify = event_data->bool_opts & ONAS_SCTH_B_FANOTIFY ? 1 : 0;

#if defined(FANOTIFY)
	if (b_inotify) {
		logg("*ClamWorker: handling inotify event ...\n");

		if (b_dir) {
			logg("*ClamWorker: performing (extra) scanning on directory '%s'\n", event_data->pathname);
			onas_scan_thread_handle_dir(event_data, event_data->pathname);
		} else if (b_file) {
			logg("*ClamWorker: performing (extra) scanning on file '%s'\n", event_data->pathname);
			onas_scan_thread_handle_file(event_data, event_data->pathname);
		}

	} else if (b_fanotify) {

		logg("*ClamWorker: performing scanning on file '%s'\n", event_data->pathname);
		onas_scan_thread_handle_file(event_data, event_data->pathname);
	} else {
		/* something went very wrong, so check if we have an open fd,
		 * try to close it to resolve any potential lingering permissions event,
		 * then move to cleanup */
		if (event_data->fmd) {
			if (event_data->fmd->fd) {
				close(event_data->fmd->fd);
				goto done;
			}
		}
	}
#endif
done:
	/* our job to cleanup event data: worker queue just kicks us off in a thread pool, drops the event object
	 * from the queue and forgets about us */

	if (NULL != event_data) {
		if (NULL != event_data->pathname) {
			free(event_data->pathname);
			event_data->pathname = NULL;
		}

#if defined(FANOTIFY)
		if (NULL != event_data->fmd) {
			free(event_data->fmd);
			event_data->fmd = NULL;
		}
#endif
		free(event_data);
		event_data = NULL;
	}

	return NULL;
}

/**
 * @brief Simple utility function for external interfaces to add relevant context information to scan_event struct.
 *
 * Doing this mapping cuts down significantly on memory overhead when queueing hundreds of these scan_event structs
 * especially vs using a copy of a raw context struct.
 *
 * Other potential design options include giving the event access to the "global" context struct address instead,
 * to further cut down on space used, but (among other thread safety concerns) I'd prefer the worker threads not
 * have the ability to modify it at all to keep down on potential maintenance headaches in the future.
 */
cl_error_t onas_map_context_info_to_event_data(struct onas_context *ctx, struct onas_scan_event **event_data) {

    if(NULL == ctx || NULL == event_data || NULL == *event_data) {
        logg("*ClamScThread: context and scan event struct are null ...\n");
        return CL_ENULLARG;
    }

    (*event_data)->scantype = ctx->scantype;
    (*event_data)->timeout = ctx->timeout;
    (*event_data)->maxstream = ctx->maxstream;
    (*event_data)->fan_fd = ctx->fan_fd;
    (*event_data)->sizelimit = ctx->sizelimit;
    (*event_data)->retry_attempts = ctx->retry_attempts;

    if (ctx->retry_on_error) {
        (*event_data)->bool_opts |= ONAS_SCTH_B_RETRY_ON_E;
    }

    if (ctx->deny_on_error) {
        (*event_data)->bool_opts |= ONAS_SCTH_B_DENY_ON_E;
    }

    if (ctx->isremote) {
        (*event_data)->bool_opts |= ONAS_SCTH_B_REMOTE;
        (*event_data)->tcpaddr = optget(ctx->clamdopts, "TCPAddr")->strarg;
        (*event_data)->portnum = ctx->portnum;
    } else {
        (*event_data)->tcpaddr = optget(ctx->clamdopts, "LocalSocket")->strarg;
    }

    return CL_SUCCESS;
}