/*
 * $Id: 8cb6492b195c79ba5c94b801263f25146a44f69a $
 *
 * This file is part of nsmtp.
 *
 * Copyright (c) 2007, 2008 Niklas Olmes <niklas@noxa.de>
 *                          http://www.nevm.de/nsmtp/
 *                          NEVM-RIPE
 *
 * 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.
 *
 */

#include <stdio.h>

#ifdef VIRSCAN_CLAMD

#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

#include <sys/socket.h>
#include <arpa/inet.h>

#include "dns.h"

#else /* ! VIRSCAN_CLAMD */

#ifndef VIRSCAN_USE_POPEN
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#endif
#include <sys/wait.h>

#endif /* </VIRSCAN_CLAMD> */

#include "ntl.h"
#include "log.h"
#include "check.h"
#include "virscan.h"

#define E_OK 0
#define E_INFECTED 1
#define E_RES -1;
#define E_CON -2;
#define E_UNABLE -3;
#define E_LARGE -4;

#define MAX_SCANSIZE	5*1024*1024	/* 5 MB */

#ifdef VIRSCAN_CLAMD

#include "config.h"

#define CLAMD_PORT 	3310
#define C_STREAM	"STREAM\r\n"
#define A_STREAM	"stream: "
#define A_FOUND		" FOUND"
#define S_PORT		"PORT "

#endif

static const char rcsid[] = "$Id: 8cb6492b195c79ba5c94b801263f25146a44f69a $";

char virscan_last[128] = "";

/*
 * return codes:
 * [see E_* above]
 * < 0 - unable to check atm
 * 0   - nothing found
 * 1   - data is infected
 */
int
virscan(const char *data, size_t len)
{
#ifdef VIRSCAN_CLAMD

	struct sockaddr_in addr;
	char buf[2048];
	ssize_t answlen;
	unsigned long ip;
	char *p;
	int foo;
	int s, sd, dp;

#else /* ! VIRSCAN_CLAMD */

	FILE *fd;
#ifndef VIRSCAN_USE_POPEN
	char fn[64];
	char cmdbuf[256];
	int ifd;
#endif
	int e;

#endif /* </VIRSCAN_CLAMD> */

	DEBUG(("virscan() => checking (%lu) chars of data", (unsigned long)len));

	*virscan_last = '\0';

	if (len > MAX_SCANSIZE) {
		xinfo("info: virscan() => data to be scanned is too large (%u > %d bytes), not scanning", len, MAX_SCANSIZE);
		return E_LARGE;
	}

#ifdef VIRSCAN_CLAMD

	if (nsmc_virscan_hosts == NULL || nsmc_virscan_hosts[0]) {
		xinfo("info: virscan() => no virscan hosts defined");
		return E_UNABLE;
	}

	if ((ip = resolve(nsmc_virscan_hosts[0])) == INADDR_NONE) {
		xwarn("warn: virscan() => unable to resolve first clamd host <%s>", nsmc_virscan_hosts[0]);
		return E_RES;
	}

	/*
	 * (1) Connect to clamd control port and retrieve data port number to use
	 */
	(void)memset(&addr, 0, sizeof(addr));
	addr.sin_addr.s_addr = ip;
	addr.sin_port = htons(CLAMD_PORT);
	addr.sin_family = AF_INET;
	foo = 1;

	if (
		((s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		|| (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &foo, sizeof(foo)) != 0)
		|| (connect(s, (struct sockaddr *) & addr, sizeof(addr)) == -1)
	) {
		xwarn("warn: virscan() => unable to connect to clamd server <%s> port (%d)", nsmc_virscan_hosts[0], CLAMD_PORT);
		return E_CON;
	}

	if (write(s, C_STREAM, strlen(C_STREAM)) == -1) {
		xwarn("warn: virscan() => write() failed: \"%s\"", strerror(errno));
		return E_UNABLE;
	}
	if ((answlen = read(s, buf, sizeof(buf) - 1)) == -1) {
		xwarn("warn: virscan() => read() failed: \"%s\"", strerror(errno));
		return E_UNABLE;
	}
	buf[answlen] = '\0';
	if (answlen > 1)
		buf[answlen - 1] = '\0';
	DEBUG(("virscan() => server answered <%s> on STREAM", buf));

	if (strncasecmp(buf, S_PORT, strlen(S_PORT)) != 0) {
		xwarn("warn: virscan() => error talking to clamd server <%s> port (%d), did not provide port", nsmc_virscan_hosts[0], CLAMD_PORT);
		return E_UNABLE;
	}

	dp = atoi(buf + strlen(S_PORT));
	DEBUG(("virscan() => server gave us data port (%d)", dp));

	/*
	 * (2) Connect to data port and send data
	 */
	(void)memset(&addr, 0, sizeof(addr));
	addr.sin_addr.s_addr = ip;
	addr.sin_port = htons(dp);
	addr.sin_family = AF_INET;
	foo = 1;

	if (
		((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		|| (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &foo, sizeof(foo)) != 0)
		|| (connect(sd, (struct sockaddr *) & addr, sizeof(addr)) == -1)
	) {
		xwarn("warn: virscan() => unable to connect to data port (%d) of clamd server <%s> port (%d)", dp, nsmc_virscan_hosts[0], CLAMD_PORT);
		return E_UNABLE;
	}

	if (write(sd, data, len) == -1) {
		xwarn("warn: virscan() => write() on data port (%d) of clamd server <%s> port (%d) failed: \"%s\"", strerror(errno));
		return E_UNABLE;
	}
	(void)close(sd);

	/*
	 * (3) Read result on control port
	 */
	if ((answlen = read(s, buf, sizeof(buf) - 1)) == -1) {
		xwarn("warn: virscan() => read() result on control port of clamd server <%s> port (%d) failed: \"%s\"", strerror(errno));
		return E_UNABLE;
	}
	buf[answlen] = '\0';
	if (answlen > 1)
		buf[answlen - 1] = '\0';
	DEBUG(("virscan() => server said <%s> about our data", buf));

	(void)close(s);

	if (strncasecmp(buf, A_STREAM, strlen(A_STREAM)) != 0) {
		xwarn("warn: virscan() => clamd server <%s> port (%d) returned unknown answer to virscan", nsmc_virscan_hosts[0], CLAMD_PORT);
		return E_UNABLE;
	}

	if ((p = strstr(buf + strlen(A_STREAM), A_FOUND)) != NULL) {
		*p = '\0';
		xinfo("info: virscan() => found <%s> in data", buf + strlen(A_STREAM));
		(void)snprintf(virscan_last, sizeof(virscan_last), "%s", buf + strlen(A_STREAM));
		return E_INFECTED;
	}

#else /* ! VIRSCAN_CLAMD */

#ifdef VIRSCAN_USE_POPEN
	(void)fflush(stdin);
	if ((fd = popen("clamscan --mail --max-space=20m --no-summary --quiet -", "r+")) == NULL)
		return E_UNABLE;
#else
	ifd = -1;
	(void)snprintf(fn, sizeof(fn), "/tmp/nsmtp.virscan.XXXXXXX");
	if ((ifd = mkstemp(fn)) == -1
	    || (fd = fdopen(ifd, "w+")) == NULL) {
		if (ifd != -1) {
			(void)unlink(fn);
			(void)close(ifd);
		}
		xwarn("warn: virscan() => unable to create temporary file for virus scan: \"%s\"", strerror(errno));
		return E_UNABLE;
	}
#endif

	if (fwrite(data, len, 1, fd) != 1) {
		xwarn("warn: virscan() => fwrite() failed: \"%s\"", ferror(fd));
		(void)pclose(fd);
		return E_UNABLE;
	}

	(void)fflush(fd);

#ifdef VIRSCAN_USE_POPEN
	e = pclose(fd);
	if (WEXITSTATUS(e) == 1) {
		DEBUG(("virscan() => found virus"));
		return E_INFECTED;
	}
#else
	(void)fclose(fd);
	(void)snprintf(cmdbuf, sizeof(cmdbuf), "clamscan --mail --max-space=20m --no-summary --quiet %s", fn);
	e = system(cmdbuf);
	(void)unlink(fn);
	if (WEXITSTATUS(e) == 1) {
		DEBUG(("virscan() => found virus"));
		return E_INFECTED;
	}
#endif

#endif /* </VIRSCAN_CLAMD> */

	return E_OK;
}

/*
 * return codes:
 * -1 - unable to determine atm
 * 0  - no
 * 1  - "hell yeah!"
 */
int
want_onlinevirscan(const char *address, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	const char *attrs[] = { "noOnlineVirscan", NULL };
	int e;

	if ((e = want_generic("onlinevirscan", address, attrs, d_attrs, u_attrs, ret)) == -1)
		return -1;
	return !e;
}
