/*
 * Copyright (C) 2012-2017 Andreas Steffen
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE
#include <getopt.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <syslog.h>
#include <time.h>
#include <sys/stat.h>
#include <stdlib.h>

#include <library.h>
#include <utils/debug.h>

#define EXIT_NO_UPDATES		80
#define TMP_DEB_FILE		"/tmp/sec-updater.deb"
#define TMP_TAG_FILE		"/tmp/sec-updater.tag"
#define SWID_GEN_CMD		"/usr/local/bin/swid_generator"
#define TNC_MANAGE_CMD		"/var/www/tnc/manage.py"

typedef enum sec_update_state_t sec_update_state_t;

enum sec_update_state_t {
	SEC_UPDATE_STATE_BEGIN_PACKAGE,
	SEC_UPDATE_STATE_VERSION,
	SEC_UPDATE_STATE_FILENAME,
	SEC_UPDATE_STATE_END_PACKAGE
};

typedef struct stats_t stats_t;

struct stats_t {
	time_t release;
	int product;
	int packages;
	int new_versions;
	int updated_versions;
};

/**
 * global debug output variables
 */
static int debug_level = 1;
static bool stderr_quiet = FALSE;

/**
 * sec_updater dbg function
 */
static void sec_updater_dbg(debug_t group, level_t level, char *fmt, ...)
{
	int priority = LOG_INFO;
	char buffer[8192];
	char *current = buffer, *next;
	va_list args;

	if (level <= debug_level)
	{
		if (!stderr_quiet)
		{
			va_start(args, fmt);
			vfprintf(stderr, fmt, args);
			fprintf(stderr, "\n");
			va_end(args);
		}

		/* write in memory buffer first */
		va_start(args, fmt);
		vsnprintf(buffer, sizeof(buffer), fmt, args);
		va_end(args);

		/* do a syslog with every line */
		while (current)
		{
			next = strchr(current, '\n');
			if (next)
			{
				*(next++) = '\0';
			}
			syslog(priority, "%s\n", current);
			current = next;
		}
	}
}

/**
 * atexit handler to close everything on shutdown
 */
static void cleanup(void)
{
	closelog();
	library_deinit();
}

static void usage(void)
{
	printf("\
Usage:\n\
  sec-updater --help\n\
  sec-updater [--debug <level>] [--quiet]  [--security] --os <string>\n\
               --arch <string> --uri <uri> --file <filename>\n\n\
  Options:\n\
    --help             print usage information\n\
    --debug <level>    set debug level\n\
    --quiet            suppress debug output to stderr\n\
    --os <string>      operating system\n\
    --arch <string>    hw architecture\n\
    --security         set when parsing a file with security updates\n\
    --file <filename>  package information file to parse\n\
    --uri <uri>        uri where to download deb package from\n");
 }

/**
 * Update the package database
 */
static bool update_database(database_t *db, char *package, char *version,
							bool security, stats_t *stats, bool *new)
{
	int pid = 0, vid = 0, sec_flag;
	bool first = TRUE, found = FALSE;
	char *release;
	enumerator_t *e;

	/* increment package count */
	stats->packages++;

	/* set new output variable */
	*new = FALSE;

	/* check if package is already in database */
	e = db->query(db, "SELECT id FROM packages WHERE name = ?",
					  DB_TEXT, package, DB_INT);
	if (!e)
	{
		return FALSE;
	}
	if (!e->enumerate(e, &pid))
	{
		pid = 0;
	}
	e->destroy(e);

	if (!pid)
	{
		return TRUE;
	}

	/* retrieve all package versions stored in database */
	e = db->query(db,
			"SELECT id, release, security FROM versions "
			"WHERE product = ? AND package = ?",
			 DB_INT, stats->product,  DB_INT, pid, DB_INT, DB_TEXT, DB_INT);
	if (!e)
	{
		return FALSE;
	}

	while (e->enumerate(e, &vid, &release, &sec_flag))
	{
		char command[BUF_LEN];
		char found_char DBG_UNUSED = ' ';
		bool update_version = FALSE;

		if (streq(version, release))
		{
			found = TRUE;
			found_char = '*';
		}
		else if (security)
		{
			 snprintf(command, BUF_LEN, "dpkg --compare-versions %s lt %s",
										 release, version);
			if (system(command) == 0)
			{
				found_char = '!';
				if (!sec_flag)
				{
					if (db->execute(db, NULL, "UPDATE versions "
						"SET security = 1 WHERE id = ?", DB_INT, vid) != 1)
					{
						DBG1(DBG_IMV, "  could not update version");
						e->destroy(e);
						return FALSE;
					}
					update_version = TRUE;
					stats->updated_versions++;
				}
			}
		}
		if (debug_level < 2 && !update_version)
		{
			continue;
		}
		if (first)
		{
			DBG1(DBG_IMV, "%s", package);
			first = FALSE;
		}
		DBG1(DBG_IMV, "  %c%s %s", found_char , sec_flag ? "s" : " ", release);
	}
	e->destroy(e);

	if (!found)
	{
		if (first)
		{
			DBG1(DBG_IMV, "%s", package);
		}
		DBG1(DBG_IMV, "  +  %s", version);

		if (db->execute(db, &vid,
			"INSERT INTO versions "
			"(package, product, release, security, time) "
			"VALUES (?, ?, ?, 0, ?)", DB_INT, pid, DB_INT, stats->product,
			DB_TEXT, version, DB_INT, stats->release) != 1)
		{
			DBG1(DBG_IMV, "  could not store version to database");
			return FALSE;
		}
		stats->new_versions++;
		*new = TRUE;
	}

	return TRUE;
}

/**
 * Process a package file and store updates in the database
 */
static int process_packages(char *path, char *os, char *arch, char *uri,
							bool security)
{
	char line[BUF_LEN], product[BUF_LEN], command[BUF_LEN];
	char *db_uri, *download_uri = NULL, *swid_regid, *swid_entity;
	char *pos, *package = NULL, *version = NULL, *filename = NULL;
	char *swid_gen_cmd, *tnc_manage_cmd, *tmp_deb_file, *tmp_tag_file;
	sec_update_state_t state;
	enumerator_t *e;
	database_t *db;
	int len, pid;
	chunk_t deb = chunk_empty;
	FILE *file;
	stats_t stats;
	bool success = TRUE, new;

	/* initialize statistics */
	memset(&stats, 0x00, sizeof(stats_t));

	/* Set release date to current time */
	stats.release = time(NULL);

	/* opening package file */
	file = fopen(path, "r");
	if (!file)
	{
		DBG1(DBG_IMV, "  could not open \"%s\"", path);
		exit(EXIT_FAILURE);
	}

	/* connect package database */
	db_uri = lib->settings->get_str(lib->settings, "sec-updater.database", NULL);
	if (!db_uri)
	{
		DBG1(DBG_IMV, "database URI sec-updater.database not set");
		fclose(file);
		exit(EXIT_FAILURE);
	}
	db = lib->db->create(lib->db, db_uri);
	if (!db)
	{
		DBG1(DBG_IMV, "could not connect to database '%s'", db_uri);
		fclose(file);
		exit(EXIT_FAILURE);
	}

	/* form product name by concatenating os and arch strings */
	snprintf(product, BUF_LEN, "%s %s", os, arch);

	/* check if product is already in database */
	e = db->query(db, "SELECT id FROM products WHERE name = ?",
				  DB_TEXT, product, DB_INT);
	if (e)
	{
		if (e->enumerate(e, &pid))
		{
			stats.product = pid;
		}
		e->destroy(e);
	}
	if (!stats.product)
	{
		if (db->execute(db, &pid, "INSERT INTO products (name) VALUES (?)",
						DB_TEXT, product) != 1)
		{
			DBG1(DBG_IMV, "could not store product '%s' to database",
							 product);
			fclose(file);
			db->destroy(db);
			exit(EXIT_FAILURE);
		}
		stats.product = pid;
	}

	/* get settings for the loop */
	swid_regid = lib->settings->get_str(lib->settings,
						"sec-updater.swid_gen.tag_creator.regid",
						"strongswan.org");
	swid_entity = lib->settings->get_str(lib->settings,
						"sec-updater.swid_gen.tag_creator.name",
						"strongSwan Project");
	swid_gen_cmd = lib->settings->get_str(lib->settings,
						"sec-updater.swid_gen.command", SWID_GEN_CMD);
	tnc_manage_cmd = lib->settings->get_str(lib->settings,
						"sec-updater.tnc_manage_command", TNC_MANAGE_CMD);
	tmp_deb_file = lib->settings->get_str(lib->settings,
						"sec-updater.tmp.deb_file", TMP_DEB_FILE);
	tmp_tag_file = lib->settings->get_str(lib->settings,
						"sec-updater.tmp.tag_file", TMP_TAG_FILE);

	state = SEC_UPDATE_STATE_BEGIN_PACKAGE;

	while (fgets(line, sizeof(line), file))
	{
		/* set read pointer to beginning of line */
		pos = line;

		switch (state)
		{
			case SEC_UPDATE_STATE_BEGIN_PACKAGE:
				pos = strstr(pos, "Package: ");
				if (!pos)
				{
					continue;
				}
				pos += 9;
				package = pos;
				pos = strchr(pos, '\n');
				if (pos)
				{
					package = strndup(package, pos - package);
					state = SEC_UPDATE_STATE_VERSION;
				}
				break;
			case SEC_UPDATE_STATE_VERSION:
				pos = strstr(pos, "Version: ");
				if (!pos)
				{
					continue;
				}
				pos += 9;
				version = pos;
				pos = strchr(pos, '\n');
				if (pos)
				{
					version = strndup(version, pos - version);
					success = update_database(db, package, version, security,
											  &stats, &new);
					state = (success && new) ? SEC_UPDATE_STATE_FILENAME :
											   SEC_UPDATE_STATE_END_PACKAGE;
				}
				break;
			case SEC_UPDATE_STATE_FILENAME:
				pos = strstr(pos, "Filename: ");
				if (!pos)
				{
					continue;
				}
				state = SEC_UPDATE_STATE_END_PACKAGE;

				pos += 10;
				filename = pos;
				pos = strchr(pos, '\n');
				if (!pos)
				{
					break;
				}
				len = pos - filename;
				if (asprintf(&download_uri, "%s/%.*s", uri, len, filename) == -1)
				{
					break;
				}

				/* retrieve deb package file from linux repository */
				if (lib->fetcher->fetch(lib->fetcher, download_uri,
												&deb, FETCH_END) != SUCCESS)
				{
					DBG1(DBG_IMV, "     %s failed", download_uri);
					break;
				}
				DBG1(DBG_IMV, "     %s (%u bytes)", download_uri, deb.len);

				/* store deb package file to temporary location */
				if (!chunk_write(deb, tmp_deb_file, 0022, TRUE))
				{
					DBG1(DBG_IMV, "     save to '%s' failed", tmp_deb_file);
					break;
				}

				/* generate SWID tag for downloaded deb package */
				snprintf(command, BUF_LEN, "%s swid --full --package-file %s "
						 "--regid %s --entity-name '%s' --os '%s' --arch '%s' "
						 ">> %s", swid_gen_cmd, tmp_deb_file, swid_regid,
						 swid_entity, os, arch, tmp_tag_file);
				if (system(command) != 0)
				{
					DBG1(DBG_IMV, "     tag generation failed");
					break;
				}
				break;
			case SEC_UPDATE_STATE_END_PACKAGE:
				if (*pos != '\n')
				{
					continue;
				}
				free(package);
				free(version);
				free(download_uri);
				chunk_free(&deb);
				package = version = download_uri = NULL;

				if (!success)
				{
					fclose(file);
					db->destroy(db);
					exit(EXIT_FAILURE);
				}
				state = SEC_UPDATE_STATE_BEGIN_PACKAGE;
		}
	}

	free(package);
	free(version);
	free(download_uri);
	fclose(file);
	db->destroy(db);

	/* import swid tags into strongTNC */
	if (stats.new_versions > 0)
	{
		snprintf(command, BUF_LEN, "%s importswid %s",
				 tnc_manage_cmd, tmp_tag_file);
		if (system(command) != 0)
		{
			DBG1(DBG_IMV, "tag import failed");
		}
		snprintf(command, BUF_LEN, "rm %s %s",
				 tmp_deb_file, tmp_tag_file);
		if (system(command) != 0)
		{
			DBG1(DBG_IMV, "removing temporary files failed");
		}
	}

	DBG1(DBG_IMV, "processed \"%s\": %d packages, %d new versions, "
				  "%d updated versions", path, stats.packages,
				   stats.new_versions, stats.updated_versions);

	return (stats.new_versions + stats.updated_versions) ?
			EXIT_SUCCESS : EXIT_NO_UPDATES;
}

static int do_args(int argc, char *argv[])
{
	char *filename = NULL, *arch = NULL, *os = NULL, *uri = NULL;
	bool security = FALSE;

	/* reinit getopt state */
	optind = 0;

	while (TRUE)
	{
		int c;

		struct option long_opts[] = {
			{ "help", no_argument, NULL, 'h' },
			{ "arch", required_argument, NULL, 'a' },
			{ "debug", required_argument, NULL, 'd' },
			{ "file", required_argument, NULL, 'f' },
			{ "os", required_argument, NULL, 'o' },
			{ "quiet", no_argument, NULL, 'q' },
			{ "security", no_argument, NULL, 's' },
			{ "uri", required_argument, NULL, 'u' },
			{ 0,0,0,0 }
		};

		c = getopt_long(argc, argv, "ha:d:f:o:qsu:", long_opts, NULL);
		switch (c)
		{
			case EOF:
				break;
			case 'h':
				usage();
				exit(EXIT_SUCCESS);
			case 'a':
				arch = optarg;
				continue;
			case 'd':
				debug_level = atoi(optarg);
				continue;
			case 'f':
				filename = optarg;
				continue;
			case 'o':
				os = optarg;
				continue;
			case 'q':
				stderr_quiet = TRUE;
				continue;
			case 's':
				security = TRUE;
				continue;
			case 'u':
				uri = optarg;
				continue;
		}
		break;
	}

	if (filename && os && arch && uri)
	{
		return process_packages(filename, os, arch, uri, security);
	}
	else
	{
		usage();
		exit(EXIT_FAILURE);
	}
}

int main(int argc, char *argv[])
{
	/* enable attest debugging hook */
	dbg = sec_updater_dbg;
	openlog("sec-updater", 0, LOG_DEBUG);

	atexit(cleanup);

	/* initialize library */
	if (!library_init(NULL, "sec-updater"))
	{
		exit(SS_RC_LIBSTRONGSWAN_INTEGRITY);
	}
	if (!lib->plugins->load(lib->plugins,
			lib->settings->get_str(lib->settings, "sec-updater.load",
												  "sqlite curl")))
	{
		exit(SS_RC_INITIALIZATION_FAILED);
	}
	exit(do_args(argc, argv));
}

